Injeção SQL que contorna mysql_real_escape_string ()
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
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";
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:
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 seja0x5c
. Como se vê, há 5 tais codificações suportadas no MySQL 5.6 por padrão:big5
,cp932
,gb2312
,gbk
esjis
. Vamos selecionargbk
aqui.Agora, é muito importante observar o uso de
SET NAMES
aqui. Isso define o conjunto de caracteres NO SERVIDOR . Se usássemos a chamada para a função C APImysql_set_charset()
, estaríamos bem (nas versões do MySQL desde 2006). Mas mais sobre o porquê em um minuto ...The Payload
A carga útil que vamos usar para esta injeção começa com a sequência de bytes
0xbf27
. Emgbk
, esse é um caractere multibyte inválido; emlatin1
, é a corda¿'
. Observe que inlatin1
egbk
,0x27
por si só, é um'
caractere literal .Escolhemos essa carga porque, se
addslashes()
a chamássemos, inseriríamos um ASCII ,\
ou seja0x5c
, antes do'
caractere. Então terminaríamos com0xbf5c27
, quegbk
é uma sequência de dois caracteres:0xbf5c
seguido por0x27
. Ou em outras palavras, um caractere válido seguido por um sem escape'
. Mas não estamos usandoaddslashes()
. Então, para a próxima etapa ...mysql_real_escape_string ()
A chamada da API C para
mysql_real_escape_string()
difere deaddslashes()
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 usandolatin1
para a conexão, porque nunca dissemos o contrário. Dissemos ao servidor que estamos usandogbk
, 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$var
nogbk
conjunto de caracteres, veríamos:縗 'OU 1 = 1 / *
O que é exatamente o que o ataque exige.
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. PDO
o 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 PDO
nã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, 0x27
será substituído por em 0x2727
vez de 0x5c27
e, 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 0xbf27
seja, ainda está 0xbf27
etc.) - 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()
...
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 test
tabela. Uma dissecção:
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.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.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 oNO_BACKSLASH_ESCAPES
modo 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$var
permanece exatamente o mesmo que o argumento de que foi fornecido amysql_real_escape_string()
-é como se há como escapar teve lugar em tudo .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_ESCAPES
modo, 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_ESCAPES
pode 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 SUPER
usuá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_ESCAPES
ou 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_ESCAPES
també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).
Bem, não há nada realmente que possa passar por isso, além de %
curinga. Pode ser perigoso se você estiver usando uma LIKE
instruçã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);