Det korta svaret är ja, ja det finns ett sätt att komma runt mysql_real_escape_string()
.#För mycket OBSKURA KANTFODRAL!!!
Det långa svaret är inte så lätt. Den är baserad på en attack demonstrerad här .
Attacken
Så låt oss börja med att visa attacken...
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");
Under vissa omständigheter kommer det att returnera mer än 1 rad. Låt oss dissekera vad som händer här:
-
Välja en teckenuppsättning
mysql_query('SET NAMES gbk');
För att denna attack ska fungera behöver vi kodningen som servern förväntar sig på anslutningen både för att koda
'
som i ASCII, dvs0x27
och att ha något tecken vars sista byte är en ASCII\
dvs0x5c
. Som det visar sig finns det 5 sådana kodningar som stöds i MySQL 5.6 som standard:big5
,cp932
,gb2312
,gbk
ochsjis
. Vi väljergbk
här.Nu är det mycket viktigt att notera användningen av
SET NAMES
här. Detta ställer in teckenuppsättningen PÅ SERVERN . Om vi använde anropet till C API-funktionenmysql_set_charset()
, vi skulle klara oss (på MySQL-versioner sedan 2006). Men mer om varför om en minut... -
Lasten
Nyttolasten vi ska använda för denna injektion börjar med bytesekvensen
0xbf27
. Igbk
, det är ett ogiltigt multibyte-tecken; ilatin1
, det är strängen¿'
. Observera att ilatin1
ochgbk
,0x27
i sig är en bokstavlig'
tecken.Vi har valt denna nyttolast eftersom, om vi anropade
addslashes()
på den skulle vi infoga en ASCII\
dvs0x5c
, före'
karaktär. Så vi skulle avsluta med0xbf5c27
, som igbk
är en sekvens med två tecken:0xbf5c
följt av0x27
. Eller med andra ord, en giltig tecken följt av en okodad'
. Men vi använder inteaddslashes()
. Så vidare till nästa steg... -
mysql_real_escape_string()
C API-anropet till
mysql_real_escape_string()
skiljer sig frånaddslashes()
genom att den känner till anslutningsteckenuppsättningen. Så den kan utföra escapen korrekt för den teckenuppsättning som servern förväntar sig. Men fram till denna punkt tror klienten att vi fortfarande använderlatin1
för anslutningen, eftersom vi aldrig berättade något annat. Vi berättade för servern vi användergbk
, men klienten tror fortfarande att det ärlatin1
.Därför anropet till
mysql_real_escape_string()
infogar snedstrecket, och vi har en fritt hängande'
karaktär i vårt "rymda" innehåll! Faktum är att om vi skulle titta på$var
igbk
teckenuppsättning, skulle vi se:縗' OR 1=1 /*
Vilket är exakt vad attacken kräver.
-
Frågan
Den här delen är bara en formalitet, men här är den renderade frågan:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Grattis, du har precis attackerat ett program med mysql_real_escape_string()
...
Den dåliga
Det blir värre. PDO
standard till emulering förberedda uttalanden med MySQL. Det betyder att på klientsidan gör den i princip en sprintf genom mysql_real_escape_string()
(i C-biblioteket), vilket betyder att följande kommer att resultera i en lyckad injektion:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Nu är det värt att notera att du kan förhindra detta genom att inaktivera emulerade förberedda uttalanden:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Detta kommer vanligtvis resultera i en äkta förberedd sats (dvs. data skickas över i ett separat paket från frågan). Var dock medveten om att PDO tyst kommer att fallback till att emulera uttalanden som MySQL inte kan förbereda inbyggt:de som det kan är listade i manualen, men var försiktig med att välja lämplig serverversion).
Den fula
Jag sa redan i början att vi kunde ha förhindrat allt detta om vi hade använt mysql_set_charset('gbk')
istället för SET NAMES gbk
. Och det är sant förutsatt att du använder en MySQL-version sedan 2006.
Om du använder en tidigare MySQL-version, då är en bugg
i mysql_real_escape_string()
innebar att ogiltiga multibyte-tecken som de i vår nyttolast behandlades som enkla byte i flyktsyften även om klienten hade blivit korrekt informerad om anslutningskodningen och så skulle denna attack ändå lyckas. Felet fixades i MySQL 4.1.20
, 5.0.22 och 5.1.11 .
Men det värsta är att PDO
exponerade inte C API för mysql_set_charset()
fram till 5.3.6, så i tidigare versioner kan det inte förhindra denna attack för alla möjliga kommandon! Den är nu exponerad som en DSN-parameter
.
The Saving Grace
Som vi sa i början, för att denna attack ska fungera måste databasanslutningen kodas med en sårbar teckenuppsättning. utf8mb4
är inte sårbar och ändå kan stödja alla Unicode-tecken:så du kan välja att använda det istället – men det har bara varit tillgängligt sedan MySQL 5.5.3. Ett alternativ är utf8
, vilket inte heller är inte sårbart och kan stödja hela Unicode Basic Multilingual Plane
.
Alternativt kan du aktivera NO_BACKSLASH_ESCAPES
SQL-läge, som (bland annat) ändrar driften av mysql_real_escape_string()
. Med detta läge aktiverat, 0x27
kommer att ersättas med 0x2727
istället för 0x5c27
och därför kan inte flyktprocessen skapa giltiga tecken i någon av de sårbara kodningarna där de inte fanns tidigare (t.ex. 0xbf27
är fortfarande 0xbf27
etc.)—så att servern fortfarande avvisar strängen som ogiltig. Se dock @eggyals svar
för en annan sårbarhet som kan uppstå vid användning av detta SQL-läge.
Säkra exempel
Följande exempel är säkra:
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");
Eftersom servern förväntar sig 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");
Eftersom vi har ställt in teckenuppsättningen korrekt så att klienten och servern matchar.
$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 /*"));
Eftersom vi har stängt av emulerade förberedda uttalanden.
$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 /*"));
Eftersom vi har ställt in teckenuppsättningen korrekt.
$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();
Eftersom MySQLi gör sanna förberedda uttalanden hela tiden.
Avsluta
Om du:
- Använd moderna versioner av MySQL (sen 5.1, alla 5.5, 5.6, etc) OCH
mysql_set_charset()
/$mysqli->set_charset()
/ PDO:s DSN-teckenuppsättningsparameter (i PHP ≥ 5.3.6)
ELLER
- Använd inte en sårbar teckenuppsättning för anslutningskodning (du använder bara
utf8
/latin1
/ascii
/ etc)
Du är 100 % säker.
Annars är du sårbar även om du använder mysql_real_escape_string()
...