Mysql
 sql >> Database >  >> RDS >> Mysql

SQL injection che aggira mysql_real_escape_string()

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:

  1. 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 e sjis . Selezioneremo gbk 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 API mysql_set_charset() , staremmo bene (sulle versioni di MySQL dal 2006). Ma più sul perché tra un minuto...

  2. Il carico utile

    Il payload che useremo per questa injection inizia con la sequenza di byte 0xbf27 . In gbk , questo è un carattere multibyte non valido; in latin1 , è la stringa ¿' . Nota che in latin1 e gbk , 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 con 0xbf5c27 , che in gbk è una sequenza di due caratteri:0xbf5c seguito da 0x27 . O in altre parole, un valido carattere seguito da un ' senza caratteri di escape . Ma non stiamo usando addslashes() . Quindi, al passaggio successivo...

  3. mysql_real_escape_string()

    La chiamata dell'API C a mysql_real_escape_string() differisce da addslashes() 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 utilizzando latin1 per la connessione, perché non abbiamo mai detto il contrario. L'abbiamo detto al server stiamo usando gbk , ma il cliente pensa ancora che sia latin1 .

    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 nel gbk set di caratteri, vedremmo:

    縗' OR 1=1 /*

    Che è esattamente cosa l'attacco richiede.

  4. 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() ...