A resposta curta é sim, sim, existe uma maneira de contornar
mysql_real_escape_string()
.#Para CASOS DE BORDA MUITO OBSCUROS!!! A resposta longa 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 determinadas 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 este ataque funcione, precisamos da codificação que o servidor espera na conexão tanto para codificar'
como em ASCII, ou seja,0x27
e ter algum caractere cujo byte final seja um\
ASCII ou seja,0x5c
. Como se vê, existem 5 dessas codificações suportadas no MySQL 5.6 por padrão:big5
,cp932
,gb2312
,gbk
esjis
. Vamos selecionargbk
aqui.
Agora, é muito importante observar o uso deSET NAMES
aqui. Isso define o conjunto de caracteres NO SERVIDOR . Se usássemos a chamada para a função da API Cmysql_set_charset()
, estaríamos bem (em versões do MySQL desde 2006). Mas mais sobre por que em um minuto ...
-
A carga
A carga útil que vamos usar para esta injeção começa com a sequência de bytes0xbf27
. Emgbk
, é um caractere multibyte inválido; emlatin1
, é a string¿'
. Observe que emlatin1
egbk
,0x27
por si só é um'
literal personagem.
Escolhemos esta carga porque, se chamássemosaddslashes()
nele, inseriríamos um\
ASCII ou seja,0x5c
, antes do'
personagem. Então, terminaríamos com0xbf5c27
, que emgbk
é uma sequência de dois caracteres:0xbf5c
seguido por0x27
. Ou, em outras palavras, um válido caractere seguido por um'
sem escape . Mas não estamos usandoaddslashes()
. Então vamos para o próximo passo...
-
mysql_real_escape_string()
A chamada da API C paramysql_real_escape_string()
difere deaddslashes()
em que ele conhece o conjunto de caracteres de conexão. Assim, ele pode executar o escape corretamente para o conjunto de caracteres que o servidor está esperando. No entanto, até este ponto, o cliente pensa que ainda estamos usandolatin1
para a conexão, porque nunca dissemos o contrário. Nós informamos ao servidor estamos usandogbk
, mas o cliente ainda acha que élatin1
.
Portanto, a chamada paramysql_real_escape_string()
insere a barra invertida, e temos um'
pendurado livre personagem em nosso conteúdo "escapado"! Na verdade, se olharmos para$var
nogbk
conjunto de caracteres, veríamos:
縗' OR 1=1 /*
Qual é exatamente o que o ataque exige.
-
A consulta
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 ruim
Fica pior.
PDO
o padrão é emular instruções preparadas com MySQL. Isso significa que no lado do cliente, ele basicamente faz um sprintf através de 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, vale a pena notar que você pode evitar isso desativando instruções preparadas emuladas:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Isso geralmente resultar em uma declaração preparada verdadeira (ou seja, os dados sendo enviados em um pacote separado da consulta). No entanto, esteja ciente de que o PDO irá silenciosamente fallback para emular declarações que o MySQL não pode preparar nativamente:aquelas que ele pode são listado no manual, mas tenha cuidado para 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
mysql_set_charset('gbk')
em 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 anterior do MySQL, um bug em
mysql_real_escape_string()
significava que caracteres multibyte inválidos, como aqueles em nossa carga útil, eram tratados como bytes únicos para fins de escape mesmo que o cliente tivesse sido informado corretamente da codificação da conexão e assim este ataque ainda teria 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 para mysql_set_charset()
até 5.3.6, então em versões anteriores ele não pode evite este ataque para cada comando possível! 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 com o banco de dados deve ser codificada usando um conjunto de caracteres vulnerável.
utf8mb4
é não vulnerável e ainda pode suportar todos Caractere Unicode:então você pode optar por usá-lo—mas ele só está disponível desde o MySQL 5.5.3. Uma alternativa é utf8
, que também não é vulnerável e pode suportar todo o Unicode Basic Multilingual Plane
. Como alternativa, você pode ativar o
NO_BACKSLASH_ESCAPES
Modo SQL, que (entre outras coisas) altera a operação de mysql_real_escape_string()
. Com este modo ativado, 0x27
será substituído por 0x2727
em vez de 0x5c27
e, portanto, o processo de escape não pode crie caracteres válidos em qualquer uma das codificações vulneráveis onde eles não existiam anteriormente (ou seja, 0xbf27
ainda é 0xbf27
etc.)—então o servidor ainda rejeitará a string como inválida. No entanto, veja resposta de @eggyal
para uma vulnerabilidade diferente que pode surgir do uso desse 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 as instruções preparadas emuladas.
$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 MySQLi faz declarações preparadas verdadeiras o tempo todo.
Encerrando
Se vocês:
- Use versões modernas do MySQL (final da 5.1, todas 5.5, 5.6 etc.) E
mysql_set_charset()
/$mysqli->set_charset()
/ Parâmetro do conjunto de caracteres DSN do PDO (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ê estará vulnerável mesmo que esteja usando
mysql_real_escape_string()
...