Injeção SQL que contorna mysql_real_escape_string ()

Apr 21 2011

Existe uma possibilidade de injeção de SQL mesmo ao usar a mysql_real_escape_string()função?

Considere esta situação de amostra. SQL é construído em PHP assim:

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

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

Já ouvi várias pessoas me dizerem que um código como esse ainda é perigoso e possível de hackear mesmo com a mysql_real_escape_string()função usada. Mas não consigo pensar em nenhuma exploração possível?

Injeções clássicas como esta:

aaa' OR 1=1 --

não funciona.

Você conhece alguma injeção possível que passaria pelo código PHP acima?

Respostas

393 WesleyvanOpdorp Apr 21 2011 at 15:05

Considere a seguinte consulta:

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

mysql_real_escape_string()não irá protegê-lo contra isso. O fato de você usar aspas simples ( ' ') em torno de suas variáveis ​​dentro de sua consulta é o que o protege contra isso. O seguinte também é uma opção:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
652 ircmaxell Aug 25 2012 at 09:08

A resposta curta é sim, sim, há uma maneira de contornarmysql_real_escape_string() . #Para CASOS DE BORDA Muito OBSCUROS !!!

A longa resposta não é tão fácil. É baseado em um ataque demonstrado aqui .

O ataque

Então, vamos começar mostrando o ataque ...

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

Em certas circunstâncias, isso retornará mais de 1 linha. Vamos dissecar o que está acontecendo aqui:

  1. Selecionando um Conjunto de Caracteres

    mysql_query('SET NAMES gbk');
    

    Para que esse ataque funcione, precisamos da codificação que o servidor espera na conexão para codificar 'como em ASCII ou seja, 0x27 e para ter algum caractere cujo byte final seja ASCII, \ou seja 0x5c. Como se vê, há 5 tais codificações suportadas no MySQL 5.6 por padrão: big5, cp932, gb2312, gbke sjis. Vamos selecionar gbkaqui.

    Agora, é muito importante observar o uso de SET NAMESaqui. Isso define o conjunto de caracteres NO SERVIDOR . Se usássemos a chamada para a função C API mysql_set_charset(), estaríamos bem (nas versões do MySQL desde 2006). Mas mais sobre o porquê em um minuto ...

  2. The Payload

    A carga útil que vamos usar para esta injeção começa com a sequência de bytes 0xbf27. Em gbk, esse é um caractere multibyte inválido; em latin1, é a corda ¿'. Observe que in latin1 e gbk , 0x27por si só, é um 'caractere literal .

    Escolhemos essa carga porque, se addslashes()a chamássemos, inseriríamos um ASCII , \ou seja 0x5c, antes do 'caractere. Então terminaríamos com 0xbf5c27, que gbké uma sequência de dois caracteres: 0xbf5cseguido por 0x27. Ou em outras palavras, um caractere válido seguido por um sem escape '. Mas não estamos usando addslashes(). Então, para a próxima etapa ...

  3. mysql_real_escape_string ()

    A chamada da API C para mysql_real_escape_string()difere de addslashes()porque conhece o conjunto de caracteres de conexão. Portanto, ele pode executar o escape corretamente para o conjunto de caracteres que o servidor está esperando. Porém, até aqui, o cliente acha que ainda estamos usando latin1para a conexão, porque nunca dissemos o contrário. Dissemos ao servidor que estamos usando gbk, mas o cliente ainda pensa que é latin1.

    Portanto, a chamada para mysql_real_escape_string()insere a barra invertida e temos um 'caractere deslocado livre em nosso conteúdo "escapado"! Na verdade, se estivéssemos a olhar para $varno gbkconjunto de caracteres, veríamos:

    縗 'OU 1 = 1 / *

    O que é exatamente o que o ataque exige.

  4. A pergunta

    Esta parte é apenas uma formalidade, mas aqui está a consulta renderizada:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Parabéns, você acabou de atacar com sucesso um programa usando mysql_real_escape_string()...

O mal

Fica pior. PDOo padrão é emular instruções preparadas com MySQL. Isso significa que, do lado do cliente, ele basicamente faz um sprintf through mysql_real_escape_string()(na biblioteca C), o que significa que o seguinte resultará em uma injeção bem-sucedida:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Agora, é importante notar que você pode evitar isso desativando as instruções preparadas emuladas:

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

Isso geralmente resultará em uma instrução preparada verdadeira (ou seja, os dados sendo enviados em um pacote separado da consulta). No entanto, esteja ciente de que o PDO silenciosamente retornará para emular declarações que o MySQL não pode preparar nativamente: aquelas que podem estar listadas no manual, mas cuidado ao selecionar a versão de servidor apropriada)

O feio

Eu disse no início que poderíamos ter evitado tudo isso se tivéssemos usado em mysql_set_charset('gbk')vez de SET NAMES gbk. E isso é verdade, desde que você esteja usando uma versão do MySQL desde 2006.

Se você estiver usando uma versão do MySQL mais cedo, em seguida, um bug no mysql_real_escape_string()significava que caracteres de vários bytes inválidos, como os da nossa carga foram tratados como bytes únicas para efeitos escapar mesmo se o cliente tinha sido correctamente informado sobre a codificação de conexão e assim por este ataque faria ainda ter sucesso. O bug foi corrigido no MySQL 4.1.20 , 5.0.22 e 5.1.11 .

Mas a pior parte é que PDOnão expôs a API C mysql_set_charset()até 5.3.6, portanto, em versões anteriores, ela não pode impedir esse ataque para todos os comandos possíveis! Agora está exposto como um parâmetro DSN .

A graça salvadora

Como dissemos no início, para que esse ataque funcione, a conexão do banco de dados deve ser codificada usando um conjunto de caracteres vulnerável. nãoutf8mb4 é vulnerável e ainda assim pode suportar todos os caracteres Unicode: então você pode optar por usá-los - mas só está disponível a partir do MySQL 5.5.3. Uma alternativa é utf8, que também não é vulnerável e pode oferecer suporte a todo o plano multilíngue básico Unicode .

Alternativamente, você pode habilitar o NO_BACKSLASH_ESCAPESmodo SQL, que (entre outras coisas) altera a operação do mysql_real_escape_string(). Com este modo ativado, 0x27será substituído por em 0x2727vez de 0x5c27e, portanto, o processo de escape não pode criar caracteres válidos em qualquer uma das codificações vulneráveis ​​onde eles não existiam anteriormente (ou 0xbf27seja, ainda está 0xbf27etc.) - então o servidor ainda rejeitará a string como inválida . No entanto, consulte a resposta de @ eggyal para uma vulnerabilidade diferente que pode surgir com o uso deste modo SQL.

Exemplos Seguros

Os exemplos a seguir são seguros:

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

Porque o servidor está esperando 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");

Porque definimos corretamente o conjunto de caracteres para que o cliente e o servidor correspondam.

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

Porque desativamos a emulação de declarações preparadas.

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

Porque definimos o conjunto de caracteres corretamente.

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

Porque o MySQLi faz declarações preparadas verdadeiras o tempo todo.

Empacotando

Se você:

  • Use versões modernas do MySQL (final da 5.1, todas 5.5, 5.6, etc) E mysql_set_charset() / $mysqli->set_charset()/ PDO's charset parâmetro (em PHP ≥ 5.3.6)

OU

  • Não use um conjunto de caracteres vulnerável para codificação de conexão (você só usa utf8/ latin1/ ascii/ etc)

Você está 100% seguro.

Caso contrário, você está vulnerável , embora esteja usandomysql_real_escape_string() ...

190 eggyal Apr 25 2014 at 02:15

TL; DR

mysql_real_escape_string()não fornecerá proteção alguma (e pode, além disso, munge seus dados) se:

  • O NO_BACKSLASH_ESCAPESmodo SQL do MySQL está habilitado (o que pode ser, a menos que você selecione explicitamente outro modo SQL toda vez que se conectar ); e

  • seus literais de string SQL são citados usando "caracteres de aspas duplas .

Isso foi registrado como bug # 72458 e foi corrigido no MySQL v5.7.6 (consulte a seção intitulada " A Graça Salvando ", abaixo).

Este é outro (talvez menos?) CASO EDGE obscuro !!!

Em homenagem à excelente resposta de @ircmaxell (realmente, isso é para ser bajulação e não plágio!), Vou adotar o formato dele:

O ataque

Começando com uma demonstração ...

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

Isso retornará todos os registros da testtabela. Uma dissecção:

  1. Selecionando um Modo SQL

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

    Conforme documentado em Literais de String :

    Existem várias maneiras de incluir aspas em uma string:

    • Um “ '” dentro de uma string entre aspas “ '” pode ser escrito como “ ''”.

    • Um “ "” dentro de uma string entre aspas “ "” pode ser escrito como “ ""”.

    • Preceda o caractere de aspas por um caractere de escape (“ \”).

    • Um “ '” dentro de uma string entre aspas “ "” não precisa de tratamento especial e não precisa ser duplicado ou evitado. Da mesma forma, “ "” dentro de uma string entre aspas “ '” não precisa de tratamento especial.

    Se o modo SQL do servidor inclui NO_BACKSLASH_ESCAPES, então a terceira dessas opções - que é a abordagem usual adotada por mysql_real_escape_string()- não está disponível: uma das duas primeiras opções deve ser usada em seu lugar. Observe que o efeito do quarto marcador é que se deve necessariamente conhecer o caractere que será usado para citar o literal a fim de evitar munging de dados.

  2. The Payload

    " OR 1=1 -- 
    

    A carga útil inicia essa injeção literalmente com o "personagem. Nenhuma codificação específica. Sem caracteres especiais. Sem bytes estranhos.

  3. mysql_real_escape_string ()

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

    Felizmente, mysql_real_escape_string()verifica o modo SQL e ajusta seu comportamento de acordo. Veja 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);
    }
    

    Portanto, uma função subjacente diferente,, escape_quotes_for_mysql()é chamada se o NO_BACKSLASH_ESCAPESmodo SQL estiver em uso. Como mencionado acima, tal função precisa saber qual caractere será usado para citar o literal a fim de repeti-lo sem fazer com que o outro caractere de citação seja repetido literalmente.

    No entanto, essa função assume arbitrariamente que a string será colocada entre aspas usando o 'caractere de aspas simples . Veja 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++= '\'';
        }
    

    Portanto, ele deixa os "caracteres de aspas duplas intocados (e dobra todos os 'caracteres de aspas simples ), independentemente do caractere real que é usado para citar o literal ! No nosso caso $varpermanece exatamente o mesmo que o argumento de que foi fornecido a mysql_real_escape_string()-é como se há como escapar teve lugar em tudo .

  4. A pergunta

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

    Uma espécie de formalidade, a consulta processada é:

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

Como disse meu erudito amigo: parabéns, você acabou de atacar com sucesso um programa usando mysql_real_escape_string()...

O mal

mysql_set_charset()não pode ajudar, pois isso não tem nada a ver com conjuntos de caracteres; nem pode mysqli::real_escape_string(), já que é apenas um envoltório diferente em torno dessa mesma função.

O problema, se já não é óbvio, é que a chamada para mysql_real_escape_string() não pode saber com qual caractere o literal será citado, pois isso é deixado para o desenvolvedor decidir mais tarde. Portanto, no NO_BACKSLASH_ESCAPESmodo, não há literalmente nenhuma maneira de esta função escapar com segurança de cada entrada para uso com aspas arbitrárias (pelo menos, não sem duplicar caracteres que não requerem duplicação e, portanto, munging seus dados).

O feio

Fica pior. NO_BACKSLASH_ESCAPESpode não ser tão incomum devido à necessidade de seu uso para compatibilidade com o SQL padrão (por exemplo, consulte a seção 5.3 da especificação SQL-92 , a saber, a <quote symbol> ::= <quote><quote>produção de gramática e a falta de qualquer significado especial dado à barra invertida). Além disso, seu uso foi explicitamente recomendado como uma solução alternativa para o bug (há muito corrigido) que a postagem do ircmaxell descreve. Quem sabe, alguns DBAs podem até configurá-lo para ser ativado por padrão como meio de desencorajar o uso de métodos de escape incorretos como addslashes().

Além disso, o modo SQL de uma nova conexão é definido pelo servidor de acordo com sua configuração (que um SUPERusuário pode alterar a qualquer momento); portanto, para ter certeza do comportamento do servidor, você deve sempre especificar explicitamente o modo desejado após a conexão.

A graça salvadora

Contanto que você sempre defina explicitamente o modo SQL para não incluir NO_BACKSLASH_ESCAPESou citar literais de string MySQL usando o caractere de aspas simples, este bug não pode escape_quotes_for_mysql()mostrar sua cara feia: respectivamente não será usado, ou sua suposição sobre quais caracteres de aspas precisam ser repetidos esteja correto.

Por esse motivo, recomendo que qualquer pessoa que use NO_BACKSLASH_ESCAPEStambém o ANSI_QUOTESmodo enable , pois isso forçará o uso habitual de literais de string entre aspas simples. Observe que isso não impede a injeção de SQL no caso de literais entre aspas duplas serem usados ​​- apenas reduz a probabilidade de isso acontecer (porque consultas normais e não maliciosas falhariam).

No PDO, tanto sua função equivalente PDO::quote()quanto seu emulador de instrução preparado chamam mysql_handle_quoter()- o que faz exatamente isso: ele garante que o literal escapado seja citado entre aspas simples, para que você possa ter certeza de que o PDO está sempre imune a esse bug.

A partir do MySQL v5.7.6, esse bug foi corrigido. Veja o log de alterações :

Funcionalidade adicionada ou alterada

  • Alteração incompatível: Uma nova função da API C,,mysql_real_escape_string_quote()foi implementada como uma substituiçãomysql_real_escape_string()porque a última função pode falhar ao codificar corretamente os caracteres quando oNO_BACKSLASH_ESCAPESmodo SQL está ativado. Nesse caso,mysql_real_escape_string()não pode escapar os caracteres de aspas, exceto dobrando-os e, para fazer isso corretamente, deve saber mais informações sobre o contexto de aspas do que as disponíveis. mysql_real_escape_string_quote()leva um argumento extra para especificar o contexto de citação. Para detalhes de uso, veja mysql_real_escape_string_quote () .

     Observação

    Os aplicativos devem ser modificados para usar mysql_real_escape_string_quote(), em vez de mysql_real_escape_string(), que agora falha e produz um CR_INSECURE_API_ERRerro se NO_BACKSLASH_ESCAPESestiver ativado.

    Referências: Veja também Bug # 19211994.

Exemplos Seguros

Juntamente com o bug explicado por ircmaxell, os exemplos a seguir são totalmente seguros (assumindo que alguém está usando o MySQL posterior a 4.1.20, 5.0.22, 5.1.11; ou que não está usando uma codificação de conexão 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');

... porque selecionamos explicitamente um modo SQL que não inclui 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");

... porque estamos citando nosso literal de string com aspas simples.

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

... porque as declarações preparadas por PDO são imunes a esta vulnerabilidade (e ircmaxell também, desde que você esteja usando PHP≥5.3.6 e o ​​conjunto de caracteres tenha sido definido corretamente no DSN; ou que a emulação de declaração preparada tenha sido desabilitada) .

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

... porque a quote()função do PDO não apenas escapa do literal, mas também o coloca entre aspas (em aspas simples '); note que para evitar o bug do ircmaxell neste caso, você deve estar usando PHP≥5.3.6 e ter configurado corretamente o conjunto de caracteres no DSN.

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

... porque as declarações preparadas pelo MySQLi são seguras.

Empacotando

Assim, se você:

  • usar declarações preparadas nativas

OU

  • use MySQL v5.7.6 ou posterior

OU

  • em Além de empregar uma das soluções em resumo ircmaxell de, o uso de pelo menos um dos seguintes:

    • PDO;
    • literais de string entre aspas simples; ou
    • um modo SQL explicitamente definido que não inclui NO_BACKSLASH_ESCAPES

... então você deve estar completamente seguro (vulnerabilidades fora do escopo de escape de string à parte).

19 Slava Apr 21 2011 at 15:01

Bem, não há nada realmente que possa passar por isso, além de %curinga. Pode ser perigoso se você estiver usando uma LIKEinstrução, pois o invasor pode colocar apenas %como login se você não filtrar isso, e teria que apenas aplicar força bruta na senha de qualquer um de seus usuários. As pessoas costumam sugerir o uso de declarações preparadas para torná-la 100% segura, pois os dados não podem interferir na consulta em si dessa forma. Mas, para essas consultas simples, provavelmente seria mais eficiente fazer algo como$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);