Krátká odpověď je ano, ano, existuje způsob, jak obejít mysql_real_escape_string()
.#Pro velmi NEJASNÉ PŘÍPADY EDGE!!!
Dlouhá odpověď není tak snadná. Je založeno na útoku demonstrovaném zde .
Útok
Začněme tedy ukázkou útoku...
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");
Za určitých okolností to vrátí více než 1 řádek. Pojďme si rozebrat, co se tady děje:
-
Výběr znakové sady
mysql_query('SET NAMES gbk');
Aby tento útok fungoval, potřebujeme kódování, které server očekává u připojení, aby zakódovalo
'
jako v ASCII, tj.0x27
a mít nějaký znak, jehož konečný bajt je ASCII\
tj.0x5c
. Jak se ukázalo, v MySQL 5.6 je standardně podporováno 5 takových kódování:big5
,cp932
,gb2312
,gbk
asjis
. Vyberemegbk
zde.Nyní je velmi důležité poznamenat použití
SET NAMES
tady. Tím nastavíte znakovou sadu NA SERVERU . Pokud bychom použili volání funkce C APImysql_set_charset()
, byli bychom v pořádku (na vydáních MySQL od roku 2006). Ale více o tom, proč za minutu... -
Úžitková zátěž
Užitná zátěž, kterou pro tuto injekci použijeme, začíná sekvencí bajtů
0xbf27
. Vgbk
, to je neplatný vícebajtový znak; vlatin1
, je to řetězec¿'
. Všimněte si, že vlatin1
agbk
,0x27
sám o sobě je doslovný'
postava.Toto užitečné zatížení jsme zvolili, protože pokud jsme zavolali
addslashes()
na něj bychom vložili ASCII\
tj.0x5c
, před'
charakter. Takže bychom skončili s0xbf5c27
, což je vgbk
je sekvence dvou znaků:0xbf5c
následuje0x27
. Nebo jinými slovy platný znak následovaný neescapovaným'
. Nepoužíváme všakaddslashes()
. Takže k dalšímu kroku... -
mysql_real_escape_string()
Volání C API pro
mysql_real_escape_string()
se liší odaddslashes()
v tom, že zná znakovou sadu připojení. Takže může správně provést escapování pro znakovou sadu, kterou server očekává. Až do tohoto okamžiku si však klient myslí, že stále používámelatin1
za spojení, protože jsme to nikdy neřekli jinak. Řekli jsme to serveru používámegbk
, ale klient stále si myslí, že je tolatin1
.Proto volání
mysql_real_escape_string()
vloží zpětné lomítko a máme volné'
postava v našem "uniklém" obsahu! Ve skutečnosti, pokud bychom se měli podívat na$var
vgbk
znakovou sadu, viděli bychom:縗' OR 1=1 /*
Což je přesně co útok vyžaduje.
-
Dotaz
Tato část je pouze formalita, ale zde je vykreslený dotaz:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Gratulujeme, právě jste úspěšně zaútočili na program pomocí mysql_real_escape_string()
...
Špatný
Zhoršuje se to. PDO
výchozí je emulující připravené příkazy s MySQL. To znamená, že na straně klienta v podstatě provádí sprintf prostřednictvím mysql_real_escape_string()
(v knihovně C), což znamená, že následující bude mít za následek úspěšnou injekci:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Nyní stojí za zmínku, že tomu můžete zabránit vypnutím emulovaných připravených příkazů:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
To bude obvykle výsledkem je pravdivý připravený výpis (tj. data jsou odesílána v samostatném paketu z dotazu). Mějte však na paměti, že PDO tiše ustoupí k emulaci příkazů, které MySQL neumí nativně připravit:ty, které umí, jsou uvedené v příručce, ale dejte pozor na výběr vhodné verze serveru).
Ošklivý
Hned na začátku jsem řekl, že bychom tomu všemu mohli zabránit, kdybychom použili mysql_set_charset('gbk')
místo SET NAMES gbk
. A to platí za předpokladu, že používáte verzi MySQL od roku 2006.
Pokud používáte dřívější verzi MySQL, objeví se chyba
v mysql_real_escape_string()
znamenalo, že neplatné vícebajtové znaky, jako jsou ty v naší užitečné zátěži, byly pro účely escapování považovány za jednotlivé bajty i když byl klient správně informován o kódování připojení a tak by tento útok stále uspěl. Chyba byla opravena v MySQL 4.1.20
, 5.0.22 a 5.1.11 .
Ale nejhorší na tom je, že PDO
nezveřejnil C API pro mysql_set_charset()
do 5.3.6, takže v předchozích verzích to nemůže zabránit tomuto útoku u každého možného příkazu! Nyní je vystaven jako Parametr DSN
.
The Saving Grace
Jak jsme si řekli na začátku, aby tento útok fungoval, musí být připojení k databázi zakódováno pomocí zranitelné znakové sady. utf8mb4
není zranitelný a přesto může podporovat každého Znak Unicode:takže byste se mohli rozhodnout použít tento znak – ale ten je k dispozici teprve od verze MySQL 5.5.3. Alternativou je utf8
, který také není zranitelný a může podporovat celý Unicode Základní vícejazyčná rovina
.
Případně můžete povolit NO_BACKSLASH_ESCAPES
Režim SQL, který (mimo jiné) mění činnost mysql_real_escape_string()
. Pokud je tento režim povolen, 0x27
bude nahrazeno 0x2727
spíše než 0x5c27
a tudíž proces úniku nemůže vytvořit platné znaky v kterémkoli ze zranitelných kódování, kde dříve neexistovaly (tj. 0xbf27
je stále 0xbf27
atd.) – server tedy řetězec stále odmítne jako neplatný. Podívejte se však na odpověď @eggyal
pro jinou zranitelnost, která může nastat při použití tohoto režimu SQL.
Bezpečné příklady
Následující příklady jsou bezpečné:
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");
Protože server očekává 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");
Protože jsme správně nastavili znakovou sadu tak, aby se klient a server shodovali.
$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 /*"));
Protože jsme vypnuli emulované připravené příkazy.
$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 /*"));
Protože jsme správně nastavili znakovou sadu.
$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();
Protože MySQLi neustále dělá pravdivá připravená prohlášení.
Zabalení
Pokud:
- Používejte moderní verze MySQL (pozdější 5.1, všechny 5.5, 5.6 atd.) A
mysql_set_charset()
/$mysqli->set_charset()
/ parametr znakové sady DSN PDO (v PHP ≥ 5.3.6)
NEBO
- Pro kódování připojení nepoužívejte zranitelnou znakovou sadu (používáte pouze
utf8
/latin1
/ascii
/ atd.)
Jste 100% v bezpečí.
Jinak jste zranitelní, i když používáte mysql_real_escape_string()
...