Die kurze Antwort ist ja, ja, es gibt eine Möglichkeit, mysql_real_escape_string()
zu umgehen .#Für sehr obskure Randfälle!!!
Die lange Antwort ist nicht so einfach. Es basiert auf einem Angriff hier demonstriert .
Der Angriff
Fangen wir also damit an, 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 dadurch mehr als eine 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, benötigen wir die Codierung, die der Server für die Verbindung erwartet, um sowohl
'
zu codieren wie in ASCII, also0x27
und ein Zeichen zu haben, dessen letztes Byte ein ASCII\
ist also0x5c
. Wie sich herausstellt, werden in MySQL 5.6 standardmäßig 5 solcher Kodierungen unterstützt:big5
,cp932
,gb2312
,gbk
undsjis
. Wir wählengbk
aus hier.Nun ist es sehr wichtig, die Verwendung von
SET NAMES
zu beachten hier. Damit wird der Zeichensatz AUF DEM SERVER festgelegt . Wenn wir den Aufruf der C-API-Funktionmysql_set_charset()
verwenden , wir wären in Ordnung (bei MySQL-Versionen seit 2006). Aber dazu gleich mehr... -
Die Nutzlast
Die Nutzlast, die wir für diese Injektion verwenden werden, beginnt mit der Bytesequenz
0xbf27
. Imgbk
, das ist ein ungültiges Multibyte-Zeichen; inlatin1
, es ist die Zeichenfolge¿'
. Beachten Sie das inlatin1
undgbk
,0x27
allein ist ein wörtlicher'
Zeichen.Wir haben diese Payload gewählt, weil wir
addslashes()
aufgerufen haben darauf würden wir einen ASCII\
einfügen also0x5c
, vor dem'
Charakter. Wir würden also bei0xbf5c27
landen , die ingbk
ist eine Folge aus zwei Zeichen:0xbf5c
gefolgt von0x27
. Oder mit anderen Worten, ein gültiges Zeichen gefolgt von einem'
ohne Escapezeichen . Aber wir verwenden nichtaddslashes()
. Also auf zum nächsten Schritt... -
mysql_real_escape_string()
Der C-API-Aufruf von
mysql_real_escape_string()
unterscheidet sich vonaddslashes()
, dass es den Verbindungszeichensatz kennt. So kann es das Escaping für den Zeichensatz, den der Server erwartet, ordnungsgemäß durchführen. Bis zu diesem Punkt denkt der Client jedoch, dass wir immer nochlatin1
verwenden für die Verbindung, weil wir es nie anders gesagt haben. Wir haben es dem Server mitgeteilt wir verwendengbk
, sondern der Client denkt immer noch, dass eslatin1
ist .Daher der Aufruf von
mysql_real_escape_string()
fügt den Backslash ein, und wir haben ein frei hängendes'
Charakter in unserem "entkommenen" Inhalt! In der Tat, wenn wir uns$var
ansehen würden imgbk
Zeichensatz, würden wir sehen:縗' OR 1=1 /*
Das ist genau was 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 gerade erfolgreich ein Programm mit mysql_real_escape_string()
angegriffen ...
Das Böse
Es wird schlimmer. PDO
standardmäßig auf emulieren vorbereitete Anweisungen mit MySQL. Das bedeutet, dass es auf der Clientseite im Grunde einen Sprint durch mysql_real_escape_string()
macht (in der C-Bibliothek), was bedeutet, dass Folgendes 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 /*"));
Nun ist es erwähnenswert, dass Sie dies verhindern können, indem Sie emulierte vorbereitete Anweisungen deaktivieren:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Dies wird normalerweise führen zu einer echten vorbereiteten Anweisung (d. h. die Daten werden in einem separaten Paket von der Abfrage gesendet). Beachten Sie jedoch, dass PDO stillschweigend Fallback wird Anweisungen zu emulieren, die MySQL nicht nativ vorbereiten kann:diejenigen, die es kann, sind aufgelistet im Handbuch, aber achten Sie darauf, die richtige Serverversion auszuwählen).
Das Hässliche
Ich habe ganz am Anfang gesagt, dass wir das alles hätten verhindern können, wenn wir mysql_set_charset('gbk')
verwendet hätten statt SET NAMES gbk
. Und das gilt, sofern Sie eine MySQL-Version seit 2006 verwenden.
Wenn Sie eine frühere MySQL-Version verwenden, dann ein Fehler
in mysql_real_escape_string()
bedeutete, dass ungültige Multibyte-Zeichen wie die in unserer Nutzlast als einzelne Bytes für Escaping-Zwecke behandelt wurden, auch wenn der Client korrekt über die Verbindungscodierung informiert wurde und so würde dieser Angriff noch gelingen. Der Fehler wurde in MySQL 4.1.20 behoben
, 5.0.22 und 5.1.11 .
Aber das Schlimmste ist dieses PDO
hat die C-API für mysql_set_charset()
nicht verfügbar gemacht bis 5.3.6, also in früheren Versionen nicht Verhindern Sie diesen Angriff für jeden möglichen Befehl! Er ist jetzt als verfügbar DSN-Parameter
.
Die rettende Gnade
Wie wir eingangs gesagt haben, muss die Datenbankverbindung mit einem anfälligen Zeichensatz codiert werden, damit dieser Angriff funktioniert. utf8mb4
ist nicht angreifbar und kann doch jeden unterstützen Unicode-Zeichen:Sie könnten es also stattdessen verwenden – aber es ist erst seit MySQL 5.5.3 verfügbar. Eine Alternative ist utf8
, die ebenfalls nicht anfällig ist und kann den gesamten Unicode Basic Multilingual Plane
unterstützen .
Alternativ können Sie NO_BACKSLASH_ESCAPES
aktivieren
SQL-Modus, der (unter anderem) die Funktionsweise von mysql_real_escape_string()
verändert . Wenn dieser Modus aktiviert ist, 0x27
wird durch 0x2727
ersetzt statt 0x5c27
und somit kann der Escape-Prozess nicht Erstellen Sie gültige Zeichen in einer der anfälligen Codierungen, in denen sie zuvor nicht existierten (z. B. 0xbf27
ist immer noch 0xbf27
usw.) – der Server weist die Zeichenfolge also weiterhin als ungültig zurück. Siehe jedoch @eggyals Antwort
für eine andere Schwachstelle, 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 utf8
erwartet ...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Weil wir den Zeichensatz richtig eingestellt haben, damit 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 emulierte vorbereitete Anweisungen deaktiviert 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 die ganze Zeit echte vorbereitete Anweisungen macht.
Abschluss
Wenn Sie:
- Verwenden Sie moderne Versionen von MySQL (später 5.1, alle 5.5, 5.6 usw.) UND
mysql_set_charset()
/$mysqli->set_charset()
/ DSN-Zeichensatzparameter von PDO (in PHP ≥ 5.3.6)
ODER
- Verwenden Sie keinen anfälligen Zeichensatz für die Verbindungscodierung (Sie verwenden nur
utf8
/latin1
/ascii
/ usw.)
Sie sind 100 % sicher.
Andernfalls sind Sie anfällig, obwohl Sie mysql_real_escape_string()
verwenden ...