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:
-
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...
-
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...
-
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.
-
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()
...