php query - SQL injection che aggira mysql_real_escape_string ()





quotes encode (6)


TL; DR

mysql_real_escape_string() non fornirà alcuna protezione (e potrebbe anche munge i tuoi dati) se:

  • La modalità SQL di NO_BACKSLASH_ESCAPES di MySQL è abilitata (che potrebbe essere, a meno che non si selezioni esplicitamente un'altra modalità SQL ogni volta che ci si connette ); e

  • i valori letterali delle stringhe SQL sono quotati utilizzando i caratteri " virgolette doppie " .

Questo è stato archiviato come bug # 72458 ed è stato corretto in MySQL v5.7.6 (vedere la sezione intitolata " The Saving Grace ", sotto).

Questo è un altro, (forse meno?) CASO EDGE oscuro !!!

In omaggio alla share di share (in realtà, si suppone che sia adulazione e non plagio!), share suo formato:

L'attacco

Cominciando con una dimostrazione ...

mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

Ciò restituirà tutti i record dalla tabella di test . Una dissezione:

  1. Selezione di una modalità SQL

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
    

    Come documentato sotto String Literals :

    Esistono diversi modi per includere i caratteri di citazione all'interno di una stringa:

    • Un "" all'interno di una stringa quotata con " ' " può essere scritto come " '' ".

    • Un " " "all'interno di una stringa quotata con" " " può essere scritto come " "" ".

    • Precedere il carattere di citazione con un carattere di escape (" \ ").

    • Un "" all'interno di una stringa citata con " " "non richiede alcun trattamento speciale e non deve essere duplicato o scappato. Allo stesso modo," " " all'interno di una stringa citata con "" non richiede alcun trattamento speciale.

    Se la modalità SQL del server include NO_BACKSLASH_ESCAPES , la terza di queste opzioni, che è il solito approccio adottato da mysql_real_escape_string() , non è disponibile: una delle prime due opzioni deve essere invece utilizzata. Si noti che l'effetto del quarto punto è che si deve necessariamente conoscere il carattere che verrà utilizzato per citare il valore letterale per evitare di mungere i propri dati.

  2. Il carico utile

    " OR 1=1 -- 
    

    Il payload avvia questa iniezione letteralmente con il " personaggio " , nessuna codifica particolare, nessun carattere speciale, nessun byte strano.

  3. mysql_real_escape_string ()

    $var = mysql_real_escape_string('" OR 1=1 -- ');
    

    Fortunatamente, mysql_real_escape_string() controlla la modalità SQL e regola di conseguenza il suo comportamento. Vedi libmysql.c :

    ulong STDCALL
    mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
                 ulong length)
    {
      if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
        return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
      return escape_string_for_mysql(mysql->charset, to, 0, from, length);
    }
    

    Pertanto una funzione sottostante diversa, escape_quotes_for_mysql() , viene invocata se è in uso la modalità SQL NO_BACKSLASH_ESCAPES . Come accennato in precedenza, tale funzione deve sapere quale carattere sarà usato per citare il letterale per ripeterlo senza che l'altro carattere di quotatura venga ripetuto alla lettera.

    Tuttavia, questa funzione assume arbitrariamente che la stringa sarà quotata usando il carattere ' citazione singola ' . Vedi charset.c :

    /*
      Escape apostrophes by doubling them up
    
    // [ deletia 839-845 ]
    
      DESCRIPTION
        This escapes the contents of a string by doubling up any apostrophes that
        it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
        effect on the server.
    
    // [ deletia 852-858 ]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to, size_t to_length,
                                   const char *from, size_t length)
    {
    // [ deletia 865-892 ]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            overflow= TRUE;
            break;
          }
          *to++= '\'';
          *to++= '\'';
        }
    

    Quindi, lascia " virgolette doppie " caratteri " intoccati (e raddoppia tutti ' caratteri di virgolette ' ) indipendentemente dal carattere reale che viene usato per citare il letterale ! Nel nostro caso $var rimane esattamente uguale all'argomento che è stato fornito a mysql_real_escape_string() - È come se nessuna fuga sia mai avvenuta.

  4. La domanda

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
    

    Qualcosa di una formalità, la query renderizzata è:

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
    

Come ha detto il mio dotto amico: congratulazioni, hai appena attaccato con successo un programma usando mysql_real_escape_string() ...

Il cattivo

mysql_set_charset() non può aiutare, in quanto ciò non ha nulla a che fare con i set di caratteri; né può mysqli::real_escape_string() , dal momento che è solo un involucro diverso attorno a questa stessa funzione.

Il problema, se non già ovvio, è che la chiamata a mysql_real_escape_string() non può sapere con quale carattere verrà quotato il letterale, in quanto è lasciato allo sviluppatore decidere in un secondo momento. Quindi, nella modalità NO_BACKSLASH_ESCAPES , non c'è letteralmente modo in cui questa funzione possa sfuggire in modo sicuro ad ogni input per l'uso con quotazioni arbitrarie (almeno, non senza raddoppiare i caratteri che non richiedono il raddoppio e quindi il munging dei dati).

Il brutto

Peggiora. NO_BACKSLASH_ESCAPES potrebbe non essere così raro in natura a causa della necessità del suo utilizzo per compatibilità con SQL standard (ad esempio, vedere la sezione 5.3 della specifica SQL-92 , ovvero la <quote symbol> ::= <quote><quote> grammatica produzione e mancanza di ogni significato speciale dato al backslash). Inoltre, il suo uso è stato 5.1.11 esplicitamente 5.1.11 al bug (da tempo fisso) che descrive il post di ircmaxell. Chissà, alcuni DBA potrebbero persino configurarlo come addslashes() per impostazione predefinita come mezzo per scoraggiare l'uso di metodi di escaping errati come addslashes() .

Inoltre, la modalità SQL di una nuova connessione viene impostata dal server in base alla sua configurazione (che un utente SUPER può modificare in qualsiasi momento); quindi, per essere certi del comportamento del server, è necessario specificare sempre la modalità desiderata dopo la connessione.

La grazia salvifica

Finché si imposta sempre esplicitamente la modalità SQL per non includere NO_BACKSLASH_ESCAPES , o si cita letterali di stringa MySQL usando il carattere a virgoletta singola, questo errore non può escape_quotes_for_mysql() sua brutta testa: rispettivamente escape_quotes_for_mysql() non sarà usato, o la sua ipotesi su quale citazione i caratteri richiedono la ripetizione saranno corretti.

Per questo motivo, raccomando che chiunque utilizzi NO_BACKSLASH_ESCAPES anche la modalità ANSI_QUOTES , poiché ANSI_QUOTES uso abituale di valori letterali di stringa con quotatura singola. Si noti che ciò non impedisce l'iniezione SQL nel caso in cui si utilizzino letterali a doppia virgola, ma riduce semplicemente la probabilità che ciò accada (perché le normali query non dannose fallirebbero).

In PDO, sia la sua funzione equivalente PDO::quote() che il suo emulatore di istruzioni preparato richiamano mysql_handle_quoter() -che fa esattamente questo: assicura che il letterale di escape sia quotato tra virgolette singole, così puoi essere certo che PDO è sempre immune da questo bug.

A partire da MySQL v5.7.6, questo bug è stato corretto. Guarda il registro delle modifiche :

Funzionalità aggiunta o modificata

Esempi sicuri

Presi insieme al bug spiegato da ircmaxell, i seguenti esempi sono completamente sicuri (assumendo che si stia utilizzando MySQL più tardi di 4.1.20, 5.0.22, 5.1.11 o che non si usi una codifica di connessione GBK / Big5) :

mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

... perché abbiamo esplicitamente selezionato una modalità SQL che non include NO_BACKSLASH_ESCAPES .

mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

... perché stiamo citando la nostra stringa letterale con virgolette singole.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

... perché le istruzioni preparate da PDO sono immuni da questa vulnerabilità (e anche da ircmaxell, purché si utilizzi PHP≥5.3.6 e il set di caratteri sia stato impostato correttamente nel DSN, o che l'emulazione di istruzione preparata sia stata disabilitata) .

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

... perché la funzione quote() di PDO non solo scappa il letterale, ma lo cita anche (in caratteri ' virgoletta singola ' ); nota che per evitare il bug di ircmaxell in questo caso, devi usare PHP≥5.3.6 e aver impostato correttamente il set di caratteri nel DSN.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

... perché le dichiarazioni preparate da MySQLi sono al sicuro.

Avvolgendo

Quindi, se tu:

  • usa istruzioni preparate nativi

O

  • utilizzare MySQL v5.7.6 o successivo

O

  • oltre ad utilizzare una delle soluzioni nel sommario di ircmaxell, utilizzare almeno uno dei seguenti:

    • DOP;
    • valori letterali stringa con quotatura singola; o
    • una modalità SQL impostata in modo esplicito che non include NO_BACKSLASH_ESCAPES

... quindi dovresti essere completamente al sicuro (vulnerabilità al di fuori della portata della stringa che sfugge a parte).

Esiste una possibilità di iniezione SQL anche quando si utilizza la funzione mysql_real_escape_string() ?

Considera questa situazione campione. SQL è costruito in PHP come questo:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Ho sentito numerose persone dirmi che un codice del genere è ancora pericoloso e che è possibile violare anche con la funzione mysql_real_escape_string() utilizzata. Ma non riesco a pensare a qualche possibile exploit?

Iniezioni classiche come questa:

aaa' OR 1=1 --

non lavorare.

Sei a conoscenza di una possibile iniezione che potrebbe superare il codice PHP sopra?




Considera la seguente query:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() non ti proteggerà da questo. Il fatto di utilizzare virgolette singole ( ' ' ) attorno alle variabili all'interno della query è ciò che ti protegge da questo. Il seguente è anche un'opzione:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";



mi sono trovato di fronte a questo e ti suggerisco di lavorare con PDO ma in alcuni casi potresti provare questo metodo. Funziona e molto semplice. Mi chiedo perché la gente l'abbia trascurato.

Codice di esempio // Usando il mio framework Moorexa

supporta un ricco ORM e molto altro. Ma ho dovuto eseguire questo test case per essere sicuro di un'alternativa alle persone che scrivono le dichiarazioni raw sql.

esempio.

 // checking from a user table $check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run(); // expected output SELECT * FROM api_users WHERE username='admin' or password='1' //sql generated output SELECT * FROM api_users WHERE username='admin\' or password=\'1\'' // lets try something heavy $check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run(); // expected output SELECT * FROM api_users WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1 // this would pass and fail SELECT * FROM api_users WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1' 

quindi qual è il succo.

  1. Controlla il tipo
  2. convalida l'input dell'utente. <TRUST NO ONE>
  3. frasi di fuga per stringhe

ti mostrerò un esempio di codice che esegue una query di selezione.

 // let's assume. would all work $input = ['username' => "moorexa"]; //or $_POST or $_GET $sql = 'SELECT * FROM '.$table.' '; $safe = ""; // let's grab the user input from the array foreach ($input as $key => $val) { switch($val) { case is_string($val): $safe .= $key .'=\''.addslashes($val).'\' AND '; break; case is_int($val): $safe .= $key .'='.((int) $val).' AND '; break; case is_float($val): case is_double($val): $safe .= $key .'='.(double) $val.' AND '; break; default: // this failed } } $safe = rtrim($safe, "AND "); $sql .= ' WHERE '. $safe .' '; // now sql contains a valid statement. and would only fail when terms are not met. // Hope you can apply this and also use more test cases. 



Beh, non c'è nulla che possa attraversarlo, oltre alla wildcard % . Potrebbe essere pericoloso se si stesse usando l'istruzione LIKE poichè l'autore dell'attacco potrebbe mettere solo % come login se non lo si filtra, e dovrebbe semplicemente applicare una password a bruteforce a qualcuno dei propri utenti. Le persone spesso suggeriscono di utilizzare dichiarazioni preparate per renderlo sicuro al 100%, poiché i dati non possono interferire con la query stessa in quel modo. Ma per domande così semplici probabilmente sarebbe più efficiente fare qualcosa come $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);




La risposta breve è sì, sì, c'è un modo per aggirare mysql_real_escape_string() .

Per CASI DI BORDO OSSCURE !!!

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 alcune circostanze, questo restituirà più di 1 riga. Analizziamo cosa sta succedendo qui:

  1. Selezione di un set di caratteri

    mysql_query('SET NAMES gbk');
    

    Perché questo attacco funzioni, abbiamo bisogno della codifica che il server si aspetta dalla connessione sia per codificare ' come in ASCII, cioè 0x27 sia per avere un carattere il cui byte finale è un ASCII \ ie 0x5c . Come risulta, ci sono 5 codifiche supportate in MySQL 5.6 di default: cp932 , gb2312 , gbk , 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 mysql_set_charset() la chiamata alla funzione API C mysql_set_charset() , mysql_set_charset() bene (sulle versioni di MySQL dal 2006). Ma più sul perché in un minuto ...

  2. Il carico utile

    Il carico utile che useremo per questa iniezione inizia con la sequenza di byte 0xbf27 . In gbk , questo è un carattere multibyte non valido; in latin1 , è la stringa ¿' . Notare che in latin1 e gbk , 0x27 da solo è un carattere letterale.

    Abbiamo scelto questo payload perché, se avessimo chiamato addslashes() , inseriremmo un ASCII \ ie 0x5c , prima del carattere. Quindi ci 0xbf5c27 con 0xbf5c27 , che in gbk è una sequenza di due caratteri: 0xbf5c seguita da 0x27 . O in altre parole, un carattere valido seguito da un carattere senza caratteri ' . Ma non stiamo usando addslashes() . Quindi, al prossimo passo ...

  3. mysql_real_escape_string ()

    La chiamata API C a mysql_real_escape_string() differisce da addslashes() in quanto conosce il set di caratteri di connessione. Quindi può eseguire correttamente l'escaping per il set di caratteri che il server si aspetta. Tuttavia, fino a questo punto, il cliente pensa che stiamo ancora usando latin1 per la connessione, perché non l'abbiamo mai detto diversamente. Abbiamo detto al server che stiamo usando gbk , ma il client pensa ancora che sia latin1 .

    Quindi la chiamata a mysql_real_escape_string() inserisce la barra rovesciata, e abbiamo un carattere ' libero ' nel nostro contenuto "escapato"! In effetti, se dovessimo guardare $var nel set di caratteri gbk , gbk :

    縗' OR 1=1 /*

    Quale è esattamente ciò che richiede l'attacco.

  4. La domanda

    Questa parte è solo una formalità, ma ecco la query resa:

    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

Peggiora. PDO impostazione predefinita, PDO emula le istruzioni preparate con MySQL. Ciò significa che dal punto di vista del client, fondamentalmente fa uno sprintf attraverso mysql_real_escape_string() (nella libreria C), il che significa che il seguente risultato sarà 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 è possibile prevenire ciò disabilitando le dichiarazioni preparate emulate:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Questo di solito si tradurrà in una vera dichiarazione preparata (cioè i dati inviati in un pacchetto separato dalla query). Tuttavia, si tenga presente che PDO eseguirà silenziosamente il fallback delle istruzioni di emulazione che MySQL non è in grado di preparare in modo nativo: quelle che possono essere listed nel manuale, ma attenzione a selezionare la versione del server appropriata).

Il brutto

All'inizio ho detto che avremmo potuto prevenire tutto ciò se avessimo usato mysql_set_charset('gbk') invece di SET NAMES gbk . E questo è vero se si utilizza una versione di MySQL dal 2006.

Se usi una versione precedente di MySQL, un bug in mysql_real_escape_string() significa che caratteri multibyte non validi come quelli nel nostro payload sono stati trattati come byte singoli per scopi di escape anche se il client era stato correttamente informato della codifica della connessione e così questo attacco avrebbe comunque avuto 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 versione 5.3.6, quindi nelle versioni precedenti non può impedire questo attacco per ogni comando possibile! 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 vulnerabili. utf8mb4 non è vulnerabile e tuttavia può supportare ogni carattere Unicode: quindi puoi scegliere di usarlo, ma è disponibile solo da MySQL 5.5.3. Un'alternativa è utf8 , che non è vulnerabile e può supportare l'intero piano multilingue multilingue di Unicode.

In alternativa, puoi abilitare la modalità SQL NO_BACKSLASH_ESCAPES , che (tra le altre cose) altera l'operazione di mysql_real_escape_string() . Con questa modalità abilitata, 0x27 verrà sostituito con 0x2727 anziché 0x5c27 e quindi il processo di escape non può creare caratteri validi in nessuna delle codifiche vulnerabili in cui non esistevano in precedenza (cioè 0xbf27 è ancora 0xbf27 ecc.) - quindi il server continuerà a funzionare rifiuta la stringa come non valida. Tuttavia, vedere la risposta di @ eggyal per una diversa vulnerabilità che può sorgere 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 le dichiarazioni preparate.

Avvolgendo

Se tu:

  • Utilizzare Modern Versions of MySQL (late 5.1, all 5.5, 5.6, etc) AND mysql_set_charset() / $mysqli->set_charset() / parametro charset DSN di PDO (in PHP ≥ 5.3.6)

O

  • Non utilizzare un set di caratteri vulnerabili per la codifica della connessione (si usa solo utf8 / latin1 / ascii / etc)

Sei al sicuro al 100%.

Altrimenti, sei vulnerabile anche se stai usando mysql_real_escape_string() ...




Una buona idea è usare un 'mappatore di oggetti relazionale' come Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

Non ti salva solo da iniezioni SQL ma anche da errori di sintassi! Supporta anche raccolte di modelli con concatenamento di metodi per filtrare o applicare azioni a più risultati contemporaneamente e connessioni multiple.





php mysql sql security sql-injection