776 Stimmen

SQL-Injection, die mysql_real_escape_string() umgeht

Besteht die Möglichkeit einer SQL-Injektion auch bei Verwendung von mysql_real_escape_string() Funktion?

Betrachten Sie die folgende Beispielsituation. SQL ist in PHP wie folgt aufgebaut:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Ich habe von zahlreichen Leuten gehört, dass solcher Code immer noch gefährlich ist und auch mit Hilfe von Software gehackt werden kann. mysql_real_escape_string() Funktion verwendet. Aber mir fällt kein möglicher Exploit ein?

Klassische Injektionen wie diese:

aaa' OR 1=1 --

funktionieren nicht.

Kennen Sie eine mögliche Injektion, die durch den obigen PHP-Code hindurchgehen würde?

744voto

ircmaxell Punkte 159431

Die kurze Antwort lautet Ja, ja, es gibt einen Weg, das zu umgehen. mysql_real_escape_string() . #Für sehr OBSCURE EDGE CASES!!!

Die lange Antwort ist nicht so einfach. Sie basiert auf einem Angriff hier demonstriert .

Die Attacke

Beginnen wir also damit, den Angriff zu zeigen...

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Unter bestimmten Umständen wird mehr als 1 Zeile zurückgegeben. Sehen wir uns an, was hier vor sich geht:

  1. Auswählen eines Zeichensatzes

    mysql_query('SET NAMES gbk');

    Damit dieser Angriff funktioniert, müssen wir die Kodierung, die der Server auf der Verbindung erwartet, sowohl für die Kodierung ' wie bei ASCII, d.h. 0x27 y ein Zeichen zu haben, dessen letztes Byte ein ASCII-Zeichen ist \ d.h. 0x5c . Wie sich herausstellt, werden in MySQL 5.6 standardmäßig 5 solcher Kodierungen unterstützt: big5 , cp932 , gb2312 , gbk y sjis . Wir wählen aus gbk hier.

    Nun ist es sehr wichtig, die Verwendung von SET NAMES hier. Damit wird der Zeichensatz festgelegt AUF DEM SERVER . Wenn wir den Aufruf der C-API-Funktion mysql_set_charset() wäre das kein Problem (bei MySQL-Versionen seit 2006). Aber mehr dazu, warum in einer Minute...

  2. Die Nutzlast

    Die Nutzlast, die wir für diese Injektion verwenden werden, beginnt mit der Byte-Sequenz 0xbf27 . Unter gbk ist ein ungültiges Multibyte-Zeichen; in latin1 ist es die Zeichenkette ¿' . Beachten Sie, dass in latin1 y gbk , 0x27 ist für sich genommen eine wörtliche ' Charakter.

    Wir haben diese Nutzlast gewählt, weil wir bei einem Aufruf von addslashes() darauf, würden wir ein ASCII \ d.h. 0x5c , vor dem ' Zeichen. Also würden wir am Ende 0xbf5c27 die in gbk ist eine Folge von zwei Zeichen: 0xbf5c gefolgt von 0x27 . Oder mit anderen Worten, ein gültig Zeichen, gefolgt von einem nicht abgeschnittenen ' . Aber wir benutzen nicht addslashes() . Also weiter zum nächsten Schritt...

  3. mysql_real_escape_string()

    Der C-API-Aufruf an mysql_real_escape_string() unterscheidet sich von addslashes() dass es den Zeichensatz der Verbindung kennt. Daher kann er das Escaping für den Zeichensatz, den der Server erwartet, korrekt durchführen. Bis zu diesem Punkt denkt der Client jedoch, dass wir immer noch mit latin1 für die Verbindung, weil wir ihm nie etwas anderes gesagt haben. Wir haben dem Server wir verwenden gbk aber die Kunde denkt immer noch, es sei latin1 .

    Daher die Aufforderung an mysql_real_escape_string() fügt den Backslash ein, und wir haben eine frei hängende ' Zeichen in unserem "entkommenen" Inhalt! In der Tat, wenn wir uns ansehen würden $var im gbk Zeichensatz, würden wir sehen:

    ' OR 1=1 /\*

    Das ist was genau der Angriff erfordert.

  4. Die Abfrage

    Dieser Teil ist nur eine Formalität, aber hier ist die gerenderte Abfrage:

    SELECT * FROM test WHERE name = '' OR 1=1 /*' LIMIT 1

Herzlichen Glückwunsch, Sie haben soeben erfolgreich ein Programm angegriffen, indem Sie mysql_real_escape_string() ...

Das Schlechte

Es kommt noch schlimmer. PDO wird standardmäßig auf Nachahmung vorbereitete Anweisungen mit MySQL. Das bedeutet, dass es auf der Client-Seite im Grunde ein sprintf durch mysql_real_escape_string() (in der C-Bibliothek), was bedeutet, dass das Folgende zu einer erfolgreichen Injektion führt:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Es ist erwähnenswert, dass Sie dies verhindern können, indem Sie emulierte vorbereitete Anweisungen deaktivieren:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Dies wird in der Regel zu einer echten vorbereiteten Anweisung führen (d. h. die Daten werden in einem von der Abfrage getrennten Paket übermittelt). Beachten Sie jedoch, dass PDO stillschweigend Fallback um Statements zu emulieren, die MySQL nicht nativ vorbereiten kann: Diejenigen, die es kann, sind aufgelistet im Handbuch, aber achten Sie darauf, dass Sie die richtige Serverversion auswählen).

Das Hässliche

Ich habe gleich zu Beginn gesagt, dass wir das alles hätten verhindern können, wenn wir die mysql_set_charset('gbk') 代わりに SET NAMES gbk . Und das gilt, sofern Sie eine MySQL-Version ab 2006 verwenden.

Wenn Sie eine frühere Version von MySQL verwenden, dann ist ein Fehler en mysql_real_escape_string() bedeutete, dass ungültige Multibyte-Zeichen, wie die in unserer Nutzlast, als einzelne Bytes behandelt wurden, um sie zu entschlüsseln selbst wenn der Kunde korrekt über die Verbindungskodierung informiert wurde und so würde dieser Angriff trotzdem gelingen. Der Fehler wurde in MySQL behoben 4.1.20 , 5.0.22 y 5.1.11 .

Aber das Schlimmste ist, dass PDO nicht die C-API für mysql_set_charset() bis 5.3.6, also in früheren Versionen kann nicht verhindern Sie diesen Angriff für jeden möglichen Befehl! Es ist jetzt als ein DSN-Parameter .

Die rettende Gnade

Wie eingangs erwähnt, muss die Datenbankverbindung mit einem angreifbaren Zeichensatz kodiert sein, damit dieser Angriff funktioniert. utf8mb4 es nicht anfällig und kann dennoch unterstützen jede Unicode-Zeichen: Sie könnten sich also dafür entscheiden, stattdessen dieses Zeichen zu verwenden - es ist aber erst seit MySQL 5.5.3 verfügbar. Eine Alternative ist utf8 die auch nicht anfällig und kann den gesamten Unicode unterstützen Basic Multilingual Plane .

Alternativ können Sie auch die Option NO_BACKSLASH_ESCAPES SQL-Modus, der (unter anderem) die Funktionsweise von mysql_real_escape_string() . Wenn dieser Modus aktiviert ist, 0x27 wird ersetzt durch 0x2727 statt 0x5c27 und damit der Fluchtprozess kann nicht gültige Zeichen in einer der gefährdeten Kodierungen zu erzeugen, wo sie vorher nicht existierten (d. h. 0xbf27 ist noch 0xbf27 usw.), so dass der Server die Zeichenfolge trotzdem als ungültig zurückweist. Siehe jedoch @eggyal's Antwort für eine andere Sicherheitslücke, die durch die Verwendung dieses SQL-Modus entstehen kann.

Sichere Beispiele

Die folgenden Beispiele sind sicher:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Weil der Server erwartet, dass utf8 ...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Denn wir haben den Zeichensatz richtig eingestellt, so dass der Client und der Server übereinstimmen.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir die emulierten vorbereiteten Erklärungen ausgeschaltet haben.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir den Zeichensatz richtig eingestellt haben.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Weil MySQLi ständig echte vorbereitete Anweisungen ausführt.

Einpacken

Wenn Sie:

  • Verwenden Sie moderne Versionen von MySQL (spätes 5.1, alle 5.5, 5.6, etc) UND mysql_set_charset() / $mysqli->set_charset() / PDOs DSN-Charset-Parameter (in PHP 5.3.6)

OR

  • Verwenden Sie keinen anfälligen Zeichensatz für die Verbindungskodierung (Sie verwenden nur utf8 / latin1 / ascii / etc)

Sie sind 100% sicher.

Andernfalls sind Sie angreifbar. auch wenn Sie mit mysql_real_escape_string() ...

421voto

Wesley van Opdorp Punkte 14770

Betrachten Sie die folgende Abfrage:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() wird Sie nicht davor schützen. Die Tatsache, dass Sie einfache Anführungszeichen verwenden ( ' ' ) um Ihre Variablen innerhalb Ihrer Abfrage schützt Sie davor. Auch die folgenden Optionen sind möglich:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";

208voto

eggyal Punkte 117991

TL;DR

mysql_real_escape_string() wird keinerlei Schutz bieten (und könnte darüber hinaus Ihre Daten verfälschen), wenn:

  • MySQLs NO_BACKSLASH_ESCAPES SQL-Modus aktiviert ist (was er könnte sein, es sei denn, Sie ausdrücklich einen anderen SQL-Modus auswählen jedes Mal, wenn Sie eine Verbindung herstellen ); und

  • Ihre SQL-String-Literale werden mit doppelten Anführungszeichen versehen " Zeichen.

Dies wurde abgelegt als Fehler #72458 und wurde in MySQL v5.7.6 behoben (siehe den Abschnitt mit der Überschrift " Die rettende Gnade ", unten).

Dies ist ein weiterer, (vielleicht weniger?) obskurer EDGE-FALL!!!

Als Hommage an @ircmaxell's ausgezeichnete Antwort (dies soll wirklich eine Schmeichelei und kein Plagiat sein!), werde ich sein Format übernehmen:

Die Attacke

Wir beginnen mit einer Demonstration...

mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

Dies gibt alle Datensätze aus der Datei test Tisch. Eine Sezierung:

  1. Auswählen eines SQL-Modus

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');

    Wie dokumentiert unter String-Literale :

    Es gibt mehrere Möglichkeiten, Anführungszeichen in eine Zeichenkette einzufügen:

    • A " ' " innerhalb einer Zeichenkette, die mit " ' " kann geschrieben werden als " '' ".

    • A " " " innerhalb einer Zeichenkette, die mit " " " kann geschrieben werden als " "" ".

    • Setzen Sie vor das Anführungszeichen ein Escape-Zeichen (" \ ").

    • A " ' " innerhalb einer Zeichenkette, die mit " " " bedarf keiner besonderen Behandlung und muss nicht verdoppelt werden oder entgehen. Auf die gleiche Weise wird " " " innerhalb einer Zeichenkette, die mit " ' " braucht keine besondere Behandlung.

    Wenn der SQL-Modus des Servers Folgendes enthält NO_BACKSLASH_ESCAPES dann ist die dritte dieser Optionen - die übliche Vorgehensweise der mysql_real_escape_string() -nicht verfügbar ist: stattdessen muss eine der ersten beiden Optionen verwendet werden. Beachten Sie, dass der vierte Aufzählungspunkt zur Folge hat, dass man unbedingt das Zeichen kennen muss, mit dem das Literal zitiert wird, um zu vermeiden, dass man seine Daten verfälscht.

  2. Die Nutzlast

    " OR 1=1 -- 

    Die Nutzlast initiiert diese Injektion wortwörtlich mit dem " Zeichen. Keine besondere Kodierung. Keine besonderen Zeichen. Keine seltsamen Bytes.

  3. mysql_real_escape_string()

    $var = mysql_real_escape_string('" OR 1=1 -- ');

    Zum Glück, mysql_real_escape_string() überprüft den SQL-Modus und passt sein Verhalten entsprechend an. Siehe libmysql.c :

    ulong STDCALL
    mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
                 ulong length)
    {
      if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
        return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
      return escape_string_for_mysql(mysql->charset, to, 0, from, length);
    }

    Es liegt also eine andere Funktion zugrunde, escape_quotes_for_mysql() wird aufgerufen, wenn die NO_BACKSLASH_ESCAPES Der SQL-Modus wird verwendet. Wie bereits erwähnt, muss eine solche Funktion wissen, mit welchem Zeichen das Literal zitiert wird, um es zu wiederholen, ohne dass das andere Anführungszeichen wörtlich wiederholt wird.

    Diese Funktion ist jedoch willkürlich setzt voraus. dass die Zeichenkette mit dem einfachen Anführungszeichen ' Zeichen. Siehe charset.c :

    /*
      Escape apostrophes by doubling them up
    
    // [ deletia 839-845 ]
    
      DESCRIPTION
        This escapes the contents of a string by doubling up any apostrophes that
        it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
        effect on the server.
    
    // [ deletia 852-858 ]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to, size_t to_length,
                                   const char *from, size_t length)
    {
    // [ deletia 865-892 ]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            overflow= TRUE;
            break;
          }
          *to++= '\'';
          *to++= '\'';
        }

    Es bleiben also doppelte Anführungszeichen " unangetastet (und verdoppelt alle einfachen Anführungszeichen ' Zeichen) unabhängig von dem tatsächlichen Zeichen, das für das wörtliche Zitat verwendet wird ! In unserem Fall $var bleibt genau dasselbe wie das Argument, das für mysql_real_escape_string() -als ob es kein Entkommen gegeben hätte überhaupt .

  4. Die Abfrage

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

    Die gerenderte Abfrage ist eher eine Formalität:

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1

Wie mein gelehrter Freund es ausdrückte: Herzlichen Glückwunsch, Sie haben gerade erfolgreich ein Programm angegriffen, indem Sie mysql_real_escape_string() ...

Das Schlechte

mysql_set_charset() kann nicht helfen, da dies nichts mit Zeichensätzen zu tun hat; auch kann mysqli::real_escape_string() da dies nur eine andere Umhüllung für dieselbe Funktion ist.

Das Problem ist, falls es nicht schon offensichtlich ist, dass der Aufruf von mysql_real_escape_string() kann nicht wissen mit welchem Zeichen das Literal in Anführungszeichen gesetzt wird, da dies dem Entwickler zu einem späteren Zeitpunkt überlassen wird. Also, in NO_BACKSLASH_ESCAPES Modus, gibt es buchstäblich auf keinen Fall dass diese Funktion jede Eingabe für die Verwendung mit beliebigen Anführungszeichen sicher entschlüsseln kann (zumindest nicht, ohne Zeichen zu verdoppeln, die nicht verdoppelt werden müssen, und damit Ihre Daten zu vermischen).

Das Hässliche

Es kommt noch schlimmer. NO_BACKSLASH_ESCAPES ist in der freien Wildbahn gar nicht so unüblich, da es aus Gründen der Kompatibilität mit Standard-SQL verwendet werden muss (siehe z. B. Abschnitt 5.3 des SQL-92-Spezifikation nämlich die <quote symbol> ::= <quote><quote> Grammatikproduktion und das Fehlen einer besonderen Bedeutung des Backslash). Außerdem wurde seine Verwendung ausdrücklich als Abhilfe empfohlen auf die (längst behobene) Fehler die im Beitrag von ircmaxell beschrieben wird. Wer weiß, vielleicht konfigurieren einige DBAs es sogar so, dass es standardmäßig aktiviert ist, um die Verwendung falscher Escaping-Methoden wie addslashes() .

Auch die SQL-Modus einer neuen Verbindung wird vom Server entsprechend seiner Konfiguration gesetzt (die eine SUPER Benutzer jederzeit ändern kann); um also sicher zu sein, wie sich der Server verhält, müssen Sie immer geben Sie den gewünschten Modus nach der Verbindung ausdrücklich an.

Die rettende Gnade

Solange Sie immer ausdrücklich den SQL-Modus auf nicht einschließen setzen NO_BACKSLASH_ESCAPES oder MySQL-Stringliterale mit dem Anführungszeichen in Anführungszeichen setzen, kann dieser Fehler nicht auftauchen: bzw. escape_quotes_for_mysql() nicht verwendet wird, oder seine Annahme darüber, welche Anführungszeichen wiederholt werden müssen, ist korrekt.

Aus diesem Grund empfehle ich jedem, der die NO_BACKSLASH_ESCAPES ermöglicht auch ANSI_QUOTES Modus, da er die gewohnte Verwendung von String-Literalen in einfachen Anführungszeichen erzwingen wird. Beachten Sie, dass dies keine SQL-Injektion verhindert, falls zufällig Literale in doppelten Anführungszeichen verwendet werden - es verringert lediglich die Wahrscheinlichkeit, dass dies geschieht (da normale, nicht bösartige Abfragen fehlschlagen würden).

In PDO ist sowohl die entsprechende Funktion PDO::quote() und sein Emulator für vorbereitete Anweisungen rufen auf mysql_handle_quoter() -, die genau das tut: Sie stellt sicher, dass das escapte Literal in einfachen Anführungszeichen steht, so dass Sie sicher sein können, dass PDO immer vor diesem Fehler gefeit ist.

Mit MySQL v5.7.6 wurde dieser Fehler behoben. Siehe Änderungsprotokoll :

Hinzugefügte oder geänderte Funktionalitäten

Sichere Beispiele

Zusammen mit dem von ircmaxell erklärten Fehler sind die folgenden Beispiele völlig sicher (vorausgesetzt, dass man entweder MySQL später als 4.1.20, 5.0.22, 5.1.11 verwendet; oder dass man keine GBK/Big5-Verbindungskodierung verwendet):

mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

...weil wir ausdrücklich einen SQL-Modus gewählt haben, der keine NO_BACKSLASH_ESCAPES .

mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

...weil wir unser String-Literal in einfache Anführungszeichen setzen.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

...weil PDO Prepared Statements gegen diese Schwachstelle immun sind (und ircmaxell's auch, vorausgesetzt, dass Sie entweder PHP5.3.6 verwenden und der Zeichensatz im DSN korrekt gesetzt wurde; oder dass die Emulation von Prepared Statements deaktiviert wurde).

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

...weil die PDO's quote() Funktion nicht nur das Literal aus, sondern setzt es auch in Anführungszeichen (in einfachen Anführungszeichen ' Zeichen); beachten Sie, dass Sie den Fehler von ircmaxell in diesem Fall vermeiden, wenn Sie muss PHP5.3.6 verwenden y den Zeichensatz im DSN korrekt eingestellt haben.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

...weil MySQLi Prepared Statements sicher sind.

Einpacken

Also, wenn Sie:

  • Verwendung nativer vorbereiteter Erklärungen

OR

  • MySQL v5.7.6 oder höher verwenden

OR

  • in Zusatz um eine der Lösungen aus der Zusammenfassung von ircmaxell zu verwenden, verwenden Sie mindestens eine der folgenden Möglichkeiten:

    • PDO;
    • String-Literale in einfachen Anführungszeichen; oder
    • einen explizit eingestellten SQL-Modus, der nicht die NO_BACKSLASH_ESCAPES

...dann werden Sie sollte völlig sicher sein (abgesehen von Schwachstellen außerhalb des Bereichs des String-Escapings).

22voto

Slava Punkte 2011

Nun, es gibt eigentlich nichts, was da durchpasst, abgesehen von % Joker. Es könnte gefährlich sein, wenn Sie LIKE Anweisung, denn der Angreifer könnte nur % als Login, wenn Sie das nicht herausfiltern, und müssten dann einfach ein Passwort eines beliebigen Benutzers erzwingen. Oft wird vorgeschlagen, vorbereitete Anweisungen zu verwenden, um eine 100%ige Sicherheit zu gewährleisten, da die Daten auf diese Weise nicht mit der Abfrage selbst interferieren können. Aber für solch einfache Abfragen wäre es wahrscheinlich effizienter, etwas zu tun wie $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X