SQL injection, który krąży wokół mysql real escape string()

Czy istnieje możliwość SQL injection nawet przy użyciu funkcji mysql_real_escape_string()?

Rozważ tę przykładową sytuację. SQL jest skonstruowany w PHP Tak:

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

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

Słyszałem, jak wiele osób mówiło mi, że taki kod jest nadal niebezpieczny i możliwy do zhakowania nawet z mysql_real_escape_string() użytą funkcją. Ale nie mogę wymyślić żadnego możliwego wyzysku?

Klasyczne zastrzyki jak to:

aaa' OR 1=1 --
Nie działa.

Czy wiesz o jakimkolwiek możliwym zastrzyku, który mógłby przedostać się przez powyższy kod PHP?

Author: Brad Larson, 2011-04-21

4 answers

Rozważ następujące zapytanie:

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

mysql_real_escape_string() nie ochroni Cię przed tym. to, że używasz pojedynczych cudzysłowów (' ') wokół zmiennych wewnątrz zapytania, chroni cię przed tym. opcja jest również następująca:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
 318
Author: Wesley van Opdorp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-16 12:45:41

Krótka odpowiedź brzmi tak, tak jest sposób na obejście mysql_real_escape_string().

Dla bardzo niejasnych przypadków krawędzi!!!

Długa odpowiedź nie jest taka łatwa. Opiera się na ataku pokazanym tutaj .

Atak

Zacznijmy od pokazania ataku...
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");

W pewnych okolicznościach zwróci więcej niż 1 wiersz. Zastanówmy się, co tu się dzieje:

  1. Wybór postaci Zestaw

    mysql_query('SET NAMES gbk');
    

    Aby ten atak zadziałał, potrzebujemy kodowania, którego serwer oczekuje na połączeniu, aby zakodować ' jak w ASCII tzn. 0x27 i mieć jakiś znak, którego końcowy bajt to ASCII \ tzn. 0x5c. Jak się okazuje, w MySQL 5.6 domyślnie wspieranych jest 5 takich kodowań: big5, cp932, gb2312, gbk i sjis. Wybierzemy gbk tutaj.

    Teraz, to bardzo ważne, aby zwrócić uwagę na użycie SET NAMES tutaj. To ustawia zestaw znaków na serwerze . Gdybyśmy użyli wywołania funkcji C API mysql_set_charset(), byłoby dobrze (w wydaniach MySQL od 2006 roku). Ale więcej o tym, dlaczego za chwilę...

  2. Ładowność

    Ładunek, którego użyjemy do tego zastrzyku, zaczyna się od sekwencji bajtów 0xbf27. W gbk jest to nieprawidłowy znak wielobajtowy; w latin1 jest to ciąg znaków ¿'. Zauważ, że w latin1 oraz gbk, 0x27 sam w sobie jest literalnym znakiem '.

    Wybraliśmy ten ładunek, ponieważ gdybyśmy wywołali {[32] } na nim, wstawilibyśmy znak ASCII \, tj. 0x5c, przed znakiem '. Tak więc kończymy 0xbf5c27, który w gbk jest ciągiem dwuznakowym: 0xbf5c, po którym następuje 0x27. Lub innymi słowy, znak valid , po którym następuje unescaped '. Ale nie używamy addslashes(). Więc przejdź do następnego kroku...

  3. Mysql_real_escape_string()

    Wywołanie C API do mysql_real_escape_string() różni się z addslashes() w tym, że zna zestaw znaków połączenia. W ten sposób może poprawnie wykonać ucieczkę dla zestawu znaków, którego oczekuje serwer. Jednak do tej pory klient myśli, że nadal Używamy latin1 do połączenia, ponieważ nigdy nie powiedzieliśmy inaczej. Powiedzieliśmy serwerowi , że używamy gbk, ale klient nadal myśli, że to latin1.

    Dlatego wywołanie mysql_real_escape_string() wstawia ukośnik wsteczny i mamy znak free hanging ' w naszej" ucieczce " treści! W rzeczywistości, gdybyśmy spojrzeli na $var W zestawie znaków gbk, zobaczylibyśmy:

    縗' OR 1=1 /*

    Czyli dokładnie to, czego wymaga atak.

  4. Zapytanie

    Ta część to tylko formalność, ale oto renderowane zapytanie:]}
    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Gratulacje, właśnie z powodzeniem zaatakowałeś program używając mysql_real_escape_string()...

The Bad

Jest jeszcze gorzej. PDO domyślnie emulowanie przygotowanych instrukcji za pomocą MySQL. Oznacza to, że po stronie klienta, w zasadzie wykonuje sprintf przez mysql_real_escape_string() (w bibliotece C), co oznacza, że następujące spowoduje pomyślne wstrzyknięcie:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Warto zauważyć, że można temu zapobiec, wyłączając emulowane, przygotowane instrukcje:]}
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

To spowoduje Zwykle wynik prawdziwego, przygotowanego Oświadczenia (tzn. dane są przesyłane w oddzielnym pakiecie od zapytania). Jednak być świadomość, że PDO będzie po cichu fallback do emulacji instrukcji, których MySQL nie może przygotować natywnie: te, które może być wymienione w podręczniku, ale uważaj, aby wybrać odpowiednią wersję serwera).

Brzydka

Na samym początku powiedziałem, że moglibyśmy temu zapobiec, gdybyśmy użyli mysql_set_charset('gbk') zamiast SET NAMES gbk. I to prawda, pod warunkiem, że używasz Wydania MySQL od 2006 roku.

Jeśli używasz wcześniejszego wydania MySQL, to błąd w mysql_real_escape_string() oznaczał, że niepoprawne znaki wielobajtowe, takie jak te w naszym ładunku, były traktowane jako pojedyncze bajty dla celów ucieczki , nawet jeśli klient został prawidłowo poinformowany o kodowaniu połączenia, a więc atak nadal się powiedzie. Błąd został naprawiony w MySQL 4.1.20, 5.0.22 oraz 5.1.11.

Ale najgorsze jest to, że PDO nie ujawnił API C dla mysql_set_charset() aż do 5.3.6, więc w poprzednich wersjach nie można zapobiec temu atakowi dla każdego możliwego rozkazu! Jest teraz wyświetlany jako parametr DSN .

The Saving Grace

Jak powiedzieliśmy na początku, aby ten atak zadziałał, połączenie z bazą danych musi być zakodowane przy użyciu zestawu znaków podatnych na ataki. utf8mb4 jest nie podatny na ataki , a mimo to może obsługiwać każdy znak Unicode: więc możesz go użyć zamiast tego-ale jest on dostępny dopiero od MySQL 5.5.3. Alternatywa na utf8, który jest również nie podatny i może obsługiwać cały Unicode Podstawowa płaszczyzna Wielojęzyczna.

Alternatywnie, możesz włączyć NO_BACKSLASH_ESCAPES tryb SQL, który (między innymi) zmienia działanie mysql_real_escape_string(). Gdy ten tryb jest włączony, 0x27 zostanie zastąpiony 0x2727 zamiast 0x5c27, a zatem proces wycinający nie może utworzyć poprawnych znaków w żadnym z podatnych kodowań, gdzie nie istniały poprzednio (tzn. 0xbf27 jest nadal 0xbf27 itd.)- więc serwer nadal odrzuci łańcuch jako nieprawidłowy. Jednak zobacz @eggyal ' s answer dla innej luki, która może wynikać z używania tego trybu SQL.

Bezpieczne Przykłady

Poniższe przykłady są bezpieczne:

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");

Ponieważ serwer oczekuje 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");

Ponieważ odpowiednio ustawiliśmy zestaw znaków tak, aby klient i serwer pasowały.

$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 /*"));

Ponieważ wyłączyliśmy emulowane przygotowane wypowiedzi.

$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 /*"));

Ponieważ ustawiliśmy odpowiednio zestaw znaków.

$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();

Ponieważ MySQLi cały czas robi prawdziwe przygotowane wypowiedzi.

Owijanie

Jeśli:

  • użyj nowoczesnych wersji MySQL (późne 5.1, wszystkie 5.5, 5.6, itp.) I mysql_set_charset() / $mysqli->set_charset() / PDO ' s DSN charset parameter (w PHP ≥ 5.3.6)

Lub

  • nie używaj podatnego zestawu znaków do kodowania połączenia (używasz tylkoutf8 / latin1 / ascii / etc)
Jesteś w 100% bezpieczny.

W Przeciwnym Razie, jesteś bezbronny nawet jeśli używasz mysql_real_escape_string()...

 548
Author: ircmaxell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:55:01

TL;DR

mysql_real_escape_string() czy nie zapewnia żadnej ochrony (a ponadto może munge Twoje dane), jeśli:

  • MySQL ' s NO_BACKSLASH_ESCAPES tryb SQL jest włączony (który może być, chyba że jawnie wybierzesz inny tryb SQL za każdym razem, gdy połączysz się ); i

  • Twoje literały ciągu SQL są cytowane za pomocą znaków podwójnego cudzysłowu ".

Ten został złożony jako błąd # 72458 i został naprawiony w MySQL v5.7.6 (zobacz sekcję "the Saving Grace", poniżej).

To jest inny, (może mniej?) obscure EDGE CASE!!!

W hołdzie@ircmaxell ' s excellent answer (naprawdę, to ma być pochlebstwo, a nie plagiat!), Przyjmę jego format:

Atak

Zaczynając od demonstracji...

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');

To zwróci wszystkie rekordy z test stolik. Rozwarstwienie:

  1. Wybór trybu SQL

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

    Udokumentowane pod literały łańcuchowe :

    Istnieje kilka sposobów na umieszczenie znaków cudzysłowu w łańcuchu znaków:

    • "' "wewnątrz ciągu cytowanego z" ' "można zapisać jako" ''".

    • "" "wewnątrz ciągu cytowanego z" " "można zapisać jako" """.

    • Poprzedzaj cytat postać po znaku ucieczki ("\").

    • "' "wewnątrz ciągu cytowanego z" " " nie wymaga specjalnego traktowania i nie musi być PODWAJANE lub unikane. W ten sam sposób """ wewnątrz ciągu cytowanego z "'" nie wymaga specjalnego traktowania.

    Jeśli tryb SQL serwera zawiera NO_BACKSLASH_ESCAPES, wtedy trzecia z tych opcji - która jest zwyczajowym podejściem przyjętym przez mysql_real_escape_string() - nie jest dostępna: jedna z dwóch pierwszych opcji musi być używany zamiast. Zauważ, że efekt czwartego pocisku polega na tym, że trzeba koniecznie znać znak, który będzie używany do cytowania dosłownego, aby uniknąć muntowania swoich danych.

  2. Ładowność

    " OR 1=1 -- 
    

    Ładunek inicjuje to wstrzyknięcie dosłownie znakiem ". Brak konkretnego kodowania. Brak znaków specjalnych. Żadnych dziwnych bajtów.

  3. Mysql_real_escape_string()

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

    Na szczęście, mysql_real_escape_string() sprawdza tryb SQL i odpowiednio dostosowuje jego zachowanie. Zobacz też 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);
    }
    

    Tak więc inna podstawowa funkcja, escape_quotes_for_mysql(), jest wywoływana, jeśli używany jest tryb NO_BACKSLASH_ESCAPES SQL. Jak wspomniano powyżej, taka funkcja musi wiedzieć, który znak będzie używany do cytowania dosłownego, aby powtórzyć go bez powodowania, że drugi znak cytowania jest powtarzany dosłownie.

    Jednak funkcja ta arbitralnie zakłada , że łańcuch będzie cytowanie za pomocą pojedynczego cudzysłowu '. Zobacz też 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++= '\'';
        }
    

    Tak więc, pozostawia znaki podwójnego cudzysłowu " nietknięte (i podwaja wszystkie znaki pojedynczego cudzysłowu ') niezależnie od rzeczywistego znaku, który jest używany do cytowania dosłownego! W naszym przypadku $var pozostaje dokładnie taki sam jak argument, który został dostarczony mysql_real_escape_string()-to tak, jakby nie miało miejsca żadne wyjście } w ogóle.

  4. Na Zapytanie

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

    Coś z formalności, renderowane zapytanie to:

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

Jak to ujął mój uczony przyjaciel: gratulacje, właśnie z powodzeniem zaatakowałeś program używając mysql_real_escape_string()...

The Bad

mysql_set_charset() nie może pomóc, ponieważ nie ma to nic wspólnego z zestawami znaków; ani nie może mysqli::real_escape_string(), ponieważ to tylko inna owijka wokół tej samej funkcji.

Problem, jeśli nie już oczywisty, polega na tym, że wezwanie do mysql_real_escape_string() nie można wiedzieć , z którym znakiem literał będzie cytowany, ponieważ decyzja o tym zostanie pozostawiona deweloperowi w późniejszym czasie. Tak więc, w trybie NO_BACKSLASH_ESCAPES, nie ma dosłownie , nie ma możliwości, aby ta funkcja mogła bezpiecznie uciec od każdego wejścia do użycia z dowolnym cytowaniem(przynajmniej, nie bez podwojenia znaków, które nie wymagają podwojenia i tym samym munging danych).

Brzydka

Jest jeszcze gorzej. NO_BACKSLASH_ESCAPES może nie być to wszystko, co rzadkie na wolności ze względu na potrzeba użycia go w celu zapewnienia zgodności ze standardowym SQL (na przykład patrz punkt 5.3 specyfikacji SQL-92, a mianowicie produkcja gramatyki <quote symbol> ::= <quote><quote> i brak specjalnego znaczenia przypisywanego ukośnikowi odwrotnemu). Co więcej, jego użycie zostało wyraźnie zalecane jako obejście do (dawno naprawionego) błędu opisanego w poście ircmaxell. Kto wie, niektóre bazy danych mogą nawet skonfigurować je tak, aby były domyślnie włączone, aby zniechęcić do stosowania niewłaściwych metod ucieczki, takich jak addslashes().

Ponadto, tryb SQL nowego połączenia jest ustawiany przez serwer zgodnie z jego konfiguracją (którą użytkownik SUPER może zmienić w dowolnym momencie); dlatego, aby być pewnym zachowania serwera, musisz zawsze jawnie określić żądany tryb po połączeniu.

The Saving Grace

Tak długo, jak zawsze jawnie ustaw tryb SQL, aby nie zawierał NO_BACKSLASH_ESCAPES, lub cytowania literałów ciągu MySQL za pomocą pojedynczego cudzysłowu znak, ten bug nie może cofnąć swojej brzydkiej głowy: odpowiednio escape_quotes_for_mysql() nie zostanie użyty, lub jego założenie, które znaki cytowania wymagają powtórzenia, będzie poprawne.

Z tego powodu polecam każdemu, kto używa NO_BACKSLASH_ESCAPES również umożliwia ANSI_QUOTES mode, gdyż wymusi nawykowe użycie pojedynczych cytowanych liter. Zauważ, że nie uniemożliwia to SQL injection w przypadku użycia podwójnych cytowanych liter-zmniejsza to jedynie prawdopodobieństwo, że tak się stanie (ponieważ zwykłe, Nie złośliwe zapytania zawiedzie).

W PDO, zarówno jego równoważna funkcja PDO::quote() i jego przygotowanego emulatora instrukcji wywołania mysql_handle_quoter()-co robi dokładnie to: zapewnia, że literał ucieczki jest cytowany w pojedynczych cudzysłowach, więc możesz być pewien, że PDO jest zawsze odporny na ten błąd.

Od wersji MySQL V5. 7. 6 ten błąd został naprawiony. Zobacz change log :

Dodano lub zmieniono funkcjonalność

Bezpieczny Przykłady

Wzięte razem z błędem wyjaśnionym przez ircmaxell, poniższe przykłady są całkowicie bezpieczne (zakładając, że ktoś używa MySQL później niż 4.1.20, 5.0.22, 5.1.11; lub że ktoś nie używa kodowania połączenia 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');

...ponieważ wyraźnie wybraliśmy tryb SQL, który nie zawiera 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");

...ponieważ cytujemy nasz ciąg literalny z pojedynczymi cudzysłowami.

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

...ponieważ PDO przygotowane oświadczenia są odporne z tej luki (i ircmaxell też, pod warunkiem, że używasz PHP≥5.3.6 i zestaw znaków został poprawnie ustawiony w DSN; lub że emulacja przygotowanej instrukcji została wyłączona).

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

...ponieważ funkcja quote() PDO nie tylko ucieka literałowi, ale także cytuje go (w postaci pojedynczego cudzysłowu '); zauważ, że aby uniknąć błędu ircmaxell w tym przypadku, musisz używać PHP≥5.3.6 i poprawnie ustawić zestaw znaków w DSN.

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

...ponieważ instrukcje przygotowane przez MySQLi są bezpieczne.

Owijanie W Górę

Tak więc, jeśli:

  • użyj natywnych, przygotowanych instrukcji

Lub

  • użyj MySQL v5.7. 6 lub nowszego

Lub

  • W dodaniu do jednego z rozwiązań w podsumowaniu ircmaxell, Użyj co najmniej jednego z:

    • PDO;
    • pojedyncze cytowane literały łańcuchowe; lub
    • jawnie ustawiony tryb SQL, który nie zawiera NO_BACKSLASH_ESCAPES

...następnie powinieneś być całkowicie bezpieczny(luki poza zakresem ucieczki łańcucha znaków na bok).

 142
Author: eggyal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:45

Nie ma nic, co mogłoby przez to przejść, poza % wildcard. To może być niebezpieczne, jeśli używasz LIKE oświadczenie jako atakujący może umieścić po prostu % jako login, jeśli nie filtruj tego, i będzie musiał po prostu bruteforce hasło dowolnego z użytkowników. Ludzie często sugerują użycie gotowych oświadczeń, aby było w 100% bezpieczne, ponieważ dane nie mogą w ten sposób zakłócać samego zapytania. Ale dla takich prostych zapytań prawdopodobnie bardziej efektywne byłoby zrobienie czegoś takiego jak $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

 19
Author: Slava,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-21 08:15:22