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è0x27e 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,gbkesjis. Selezioneremogbkqui.Ora, è molto importante notare l'uso di
SET NAMESqui. 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 inlatin1egbk,0x27di 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:0xbf5cseguito 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 utilizzandolatin1per 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$varnelgbkset 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() ...