La risposta breve è sì, sì c'è un modo per aggirare mysql_real_escape_string()
.#Per CASI CON BORDO MOLTO OSCURI!!!
La lunga risposta non è così facile. Si basa su un attacco dimostrato qui .
L'attacco
Quindi, iniziamo mostrando l'attacco...
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 determinate circostanze, restituirà più di 1 riga. Analizziamo cosa sta succedendo qui:
-
Selezione di un set di caratteri
mysql_query('SET NAMES gbk');
Affinché questo attacco funzioni, abbiamo bisogno della codifica che il server si aspetta sulla connessione per codificare
'
come in ASCII cioè0x27
e per avere un carattere il cui byte finale è un\
ASCII cioè0x5c
. A quanto pare, ci sono 5 codifiche di questo tipo supportate in MySQL 5.6 per impostazione predefinita:big5
,cp932
,gb2312
,gbk
esjis
. Selezioneremogbk
qui.Ora, è molto importante notare l'uso di
SET NAMES
qui. Questo imposta il set di caratteri SUL SERVER . Se usiamo la chiamata alla funzione C APImysql_set_charset()
, staremmo bene (sulle versioni di MySQL dal 2006). Ma più sul perché tra un minuto... -
Il carico utile
Il payload che useremo per questa injection inizia con la sequenza di byte
0xbf27
. Ingbk
, questo è un carattere multibyte non valido; inlatin1
, è la stringa¿'
. Nota che inlatin1
egbk
,0x27
di per sé è un letterale'
carattere.Abbiamo scelto questo payload perché, se chiamassimo
addslashes()
su di esso, inseriremo un\
ASCII cioè0x5c
, prima del'
carattere. Quindi avremmo finito con0xbf5c27
, che ingbk
è una sequenza di due caratteri:0xbf5c
seguito da0x27
. O in altre parole, un valido carattere seguito da un'
senza caratteri di escape . Ma non stiamo usandoaddslashes()
. Quindi, al passaggio successivo... -
mysql_real_escape_string()
La chiamata dell'API C a
mysql_real_escape_string()
differisce daaddslashes()
in quanto conosce il set di caratteri di connessione. Quindi può eseguire correttamente l'escape per il set di caratteri che il server si aspetta. Tuttavia, fino a questo punto, il cliente pensa che stiamo ancora utilizzandolatin1
per la connessione, perché non abbiamo mai detto il contrario. L'abbiamo detto al server stiamo usandogbk
, ma il cliente pensa ancora che sialatin1
.Quindi la chiamata a
mysql_real_escape_string()
inserisce la barra rovesciata e abbiamo un'
sospeso personaggio nel nostro contenuto "evitato"! In effetti, se dovessimo guardare$var
nelgbk
set di caratteri, vedremmo:縗' OR 1=1 /*
Che è esattamente cosa l'attacco richiede.
-
La domanda
Questa parte è solo una formalità, ma ecco la query renderizzata:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Congratulazioni, hai appena attaccato con successo un programma usando mysql_real_escape_string()
...
Il cattivo
La situazione peggiora. PDO
l'impostazione predefinita è emulazione istruzioni preparate con MySQL. Ciò significa che sul lato client, fondamentalmente esegue uno sprintf tramite mysql_real_escape_string()
(nella libreria C), il che significa che quanto segue risulterà in un'iniezione riuscita:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Ora, vale la pena notare che puoi impedirlo disabilitando le istruzioni preparate emulate:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Questo solitamente risultato in una vera istruzione preparata (cioè i dati inviati in un pacchetto separato dalla query). Tuttavia, tieni presente che PDO fallback all'emulazione di istruzioni che MySQL non può preparare in modo nativo:quelle che può essere elencate nel manuale, ma attenzione a selezionare la versione del server appropriata).
Il brutto
All'inizio ho detto che avremmo potuto prevenire tutto questo se avessimo usato mysql_set_charset('gbk')
invece di SET NAMES gbk
. E questo è vero a condizione che tu stia utilizzando una versione MySQL dal 2006.
Se stai utilizzando una versione precedente di MySQL, allora un bug
in mysql_real_escape_string()
significava che i caratteri multibyte non validi come quelli nel nostro payload venivano trattati come byte singoli per scopi di escape anche se il client era stato correttamente informato della codifica della connessione e quindi questo attacco avrebbe comunque successo. Il bug è stato corretto in MySQL 4.1.20
, 5.0.22 e 5.1.11 .
Ma la parte peggiore è che PDO
non ha esposto l'API C per mysql_set_charset()
fino alla 5.3.6, quindi nelle versioni precedenti non può previeni questo attacco per ogni possibile comando! Ora è esposto come parametro DSN
.
La grazia salvifica
Come abbiamo detto all'inizio, affinché questo attacco funzioni, la connessione al database deve essere codificata utilizzando un set di caratteri vulnerabile. utf8mb4
è non vulnerabile eppure può supportare tutti Carattere Unicode:quindi potresti scegliere di usarlo invece, ma è disponibile solo da MySQL 5.5.3. Un'alternativa è utf8
, che inoltre non è vulnerabile e può supportare l'intero Aereo multilingue di base
Unicode .
In alternativa, puoi abilitare NO_BACKSLASH_ESCAPES
Modalità SQL, che (tra le altre cose) altera il funzionamento di mysql_real_escape_string()
. Con questa modalità abilitata, 0x27
sarà sostituito con 0x2727
anziché 0x5c27
e quindi il processo di evasione non può creare caratteri validi in una qualsiasi delle codifiche vulnerabili dove non esistevano in precedenza (ad esempio 0xbf27
è ancora 0xbf27
ecc.), quindi il server rifiuterà comunque la stringa come non valida. Tuttavia, vedi risposta di @eggyal
per una diversa vulnerabilità che può derivare dall'utilizzo di questa modalità SQL.
Esempi sicuri
I seguenti esempi sono sicuri:
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");
Perché il server si aspetta 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");
Perché abbiamo impostato correttamente il set di caratteri in modo che il client e il server corrispondano.
$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 /*"));
Perché abbiamo disattivato le istruzioni preparate emulate.
$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 /*"));
Perché abbiamo impostato correttamente il set di caratteri.
$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();
Perché MySQLi fa sempre affermazioni vere preparate.
Conclusione
Se tu:
- Utilizza le versioni moderne di MySQL (fine 5.1, tutte le 5.5, 5.6, ecc.) E
mysql_set_charset()
/$mysqli->set_charset()
/ Parametro del set di caratteri DSN di PDO (in PHP ≥ 5.3.6)
O
- Non utilizzare un set di caratteri vulnerabile per la codifica della connessione (utilizza solo
utf8
/latin1
/ascii
/ ecc)
Sei al sicuro al 100%.
Altrimenti, sei vulnerabile anche se stai usando mysql_real_escape_string()
...