Het korte antwoord is ja, ja, er is een manier om mysql_real_escape_string()
te omzeilen .#Voor zeer OBSCURE EDGE GEVALLEN!!!
Het lange antwoord is niet zo eenvoudig. Het is gebaseerd op een aanval die hier wordt gedemonstreerd .
De aanval
Dus laten we beginnen met het tonen van de aanval...
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");
In bepaalde omstandigheden levert dat meer dan 1 rij op. Laten we ontleden wat hier aan de hand is:
-
Een tekenset selecteren
mysql_query('SET NAMES gbk');
Om deze aanval te laten werken, hebben we de codering nodig die de server op de verbinding verwacht om beide
'
te coderen. zoals in ASCII, d.w.z.0x27
en om een karakter te hebben waarvan de laatste byte een ASCII is\
bijv.0x5c
. Het blijkt dat er standaard 5 van dergelijke coderingen worden ondersteund in MySQL 5.6:big5
,cp932
,gb2312
,gbk
ensjis
. We selecterengbk
hier.Nu is het erg belangrijk om te letten op het gebruik van
SET NAMES
hier. Dit zet de tekenset OP DE SERVER . Als we de aanroep van de C API-functiemysql_set_charset()
, zouden we in orde zijn (op MySQL-releases sinds 2006). Maar meer over waarom zo... -
De lading
De payload die we voor deze injectie gaan gebruiken, begint met de bytereeks
0xbf27
. Ingbk
, dat is een ongeldig multibyte-teken; inlatin1
, het is de string¿'
. Merk op dat inlatin1
engbk
,0x27
op zichzelf is een letterlijke'
karakter.We hebben deze payload gekozen omdat, als we
addslashes()
aanroepen, erop zouden we een ASCII\
. invoegen bijv.0x5c
, voor de'
karakter. Dus we zouden eindigen met0xbf5c27
, die ingbk
is een reeks van twee tekens:0xbf5c
gevolgd door0x27
. Of met andere woorden, een geldige teken gevolgd door een unescaped'
. Maar we gebruiken geenaddslashes()
. Dus op naar de volgende stap... -
mysql_real_escape_string()
De C API-aanroep naar
mysql_real_escape_string()
verschilt vanaddslashes()
in die zin dat het de verbindingstekenset kent. Dus het kan de escape goed uitvoeren voor de tekenset die de server verwacht. Tot nu toe denkt de klant echter dat we nog steedslatin1
. gebruiken voor de connectie, want we hebben het nooit anders verteld. We hebben het de server wel verteld we gebruikengbk
, maar de klant denkt nog steeds dat hetlatin1
is .Daarom is de aanroep naar
mysql_real_escape_string()
voegt de backslash in, en we hebben een vrijhangende'
karakter in onze "ontsnapte" inhoud! Sterker nog, als we zouden kijken naar$var
in degbk
tekenset, we zouden zien:縗' OR 1=1 /*
Dat is precies wat de aanval vereist.
-
De zoekopdracht
Dit deel is slechts een formaliteit, maar hier is de weergegeven vraag:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Gefeliciteerd, je hebt zojuist een programma met succes aangevallen met mysql_real_escape_string()
...
Het Slechte
Het wordt erger. PDO
standaard ingesteld op emuleren verklaringen voorbereid met MySQL. Dat betekent dat het aan de clientzijde in feite een sprintf doet via mysql_real_escape_string()
(in de C-bibliotheek), wat betekent dat het volgende zal resulteren in een succesvolle injectie:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Nu is het vermeldenswaard dat u dit kunt voorkomen door geëmuleerde voorbereide verklaringen uit te schakelen:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Dit zal meestal resulteren in een echte voorbereide verklaring (d.w.z. de gegevens worden verzonden in een apart pakket van de query). Houd er echter rekening mee dat PDO stilletjes terugvalt om verklaringen na te bootsen die MySQL niet native kan voorbereiden:degenen die het wel kan zijn vermeld in de handleiding, maar let op om de juiste serverversie te selecteren).
De lelijke
Ik zei helemaal aan het begin dat we dit allemaal hadden kunnen voorkomen als we mysql_set_charset('gbk')
hadden gebruikt in plaats van SET NAMES gbk
. En dat is waar, op voorwaarde dat u sinds 2006 een MySQL-release gebruikt.
Als je een eerdere MySQL-release gebruikt, dan is een bug
in mysql_real_escape_string()
betekende dat ongeldige multibyte-tekens zoals die in onze payload werden behandeld als enkele bytes voor ontsnappingsdoeleinden zelfs als de klant correct was geïnformeerd over de verbindingscodering en dus zou deze aanval toch slagen. De bug is opgelost in MySQL 4.1.20
, 5.0.22 en 5.1.11 .
Maar het ergste is dat PDO
heeft de C API voor mysql_set_charset()
niet vrijgegeven tot 5.3.6, dus in eerdere versies kan het niet voorkom deze aanval voor elk mogelijk commando! Het wordt nu weergegeven als een DSN-parameter
.
De reddende genade
Zoals we aan het begin al zeiden, moet deze aanval alleen werken als de databaseverbinding is gecodeerd met een kwetsbare tekenset. utf8mb4
is niet kwetsbaar en toch kan elke . ondersteunen Unicode-teken:dus je zou ervoor kunnen kiezen om dat in plaats daarvan te gebruiken, maar het is pas beschikbaar sinds MySQL 5.5.3. Een alternatief is utf8
, wat ook niet kwetsbaar . is en kan het hele Unicode Basic Multilingual Plane
ondersteunen .
Als alternatief kunt u de NO_BACKSLASH_ESCAPES
inschakelen
SQL-modus, die (onder andere) de werking van mysql_real_escape_string()
wijzigt . Met deze modus ingeschakeld, 0x27
wordt vervangen door 0x2727
in plaats van 0x5c27
en dus kan het ontsnappingsproces niet maak geldige tekens in een van de kwetsbare coderingen waar ze voorheen niet bestonden (d.w.z. 0xbf27
is nog steeds 0xbf27
etc.)—zodat de server de string nog steeds als ongeldig zal afwijzen. Zie echter @eggyal's antwoord
voor een andere kwetsbaarheid die kan ontstaan door het gebruik van deze SQL-modus.
Veilige voorbeelden
De volgende voorbeelden zijn veilig:
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");
Omdat de server utf8
. verwacht ...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Omdat we de tekenset zo hebben ingesteld dat de client en de server overeenkomen.
$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 /*"));
Omdat we geëmuleerde voorbereide verklaringen hebben uitgeschakeld.
$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 /*"));
Omdat we de tekenset goed hebben ingesteld.
$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();
Omdat MySQLi de hele tijd waarheidsgetrouwe uitspraken doet.
Afronden
Als u:
- Gebruik moderne versies van MySQL (eind 5.1, alle 5.5, 5.6, enz.) EN
mysql_set_charset()
/$mysqli->set_charset()
/ PDO's DSN-tekensetparameter (in PHP ≥ 5.3.6)
OF
- Gebruik geen kwetsbare tekenset voor verbindingscodering (u gebruikt alleen
utf8
/latin1
/ascii
/ enz.)
Je bent 100% veilig.
Anders ben je kwetsbaar ook al gebruik je mysql_real_escape_string()
...