A resposta curta é sim, sim, há uma maneira de se locomovermysql_real_escape_string()
.
PARA CASOS DE OURO MUITO OBSCURO !!!
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 certas circunstâncias, isso retornará mais de uma linha. Vamos dissecar o que está acontecendo aqui:
Selecionando um conjunto de caracteres
mysql_query('SET NAMES gbk');
Para este ataque ao trabalho, precisamos da codificação que o servidor está esperando na conexão tanto para codificar '
como no ie ASCII 0x27
e ter algum personagem cujo byte final é um ASCII \
ie 0x5c
. Como se vê, há 5 tais codificações suportadas no MySQL 5.6 por padrão: big5
, cp932
, gb2312
, gbk
e sjis
. Vamos selecionar gbk
aqui.
Agora, é muito importante observar o uso SET NAMES
daqui. Isso define o conjunto de caracteres NO SERVIDOR . Se usássemos a chamada para a função da API C mysql_set_charset()
, estaríamos bem (nas versões do MySQL desde 2006). Mas mais sobre por que em um minuto ...
A carga útil
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; dentro latin1
, é a corda ¿'
. Observe que em latin1
e gbk
, 0x27
por si só, é um '
caractere literal .
Escolhemos essa carga útil, porque, se addslashes()
a utilizássemos, inseriríamos um ASCII , \
ou seja 0x5c
, antes do '
caractere. Então, terminamos com 0xbf5c27
, que gbk
é uma sequência de dois caracteres: 0xbf5c
seguida por 0x27
. Ou, em outras palavras, um caractere válido seguido por um sem escape '
. Mas não estamos usando addslashes()
. Então, para o próximo passo ...
mysql_real_escape_string ()
A chamada da API C mysql_real_escape_string()
é diferente por addslashes()
conhecer o conjunto de caracteres da conexão. Portanto, ele pode executar o escape corretamente para o conjunto de caracteres que o servidor está esperando. No entanto, até o momento, o cliente pensa que ainda estamos usando latin1
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 livre suspenso em nosso conteúdo "escapado"! Na verdade, se estivéssemos a olhar para $var
no gbk
conjunto de caracteres, veríamos:
OR 'OR 1 = 1 / *
Qual é 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 o MySQL. Isso significa que, no lado do cliente, ele basicamente faz um sprintf 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 verdadeira declaração preparada (isto é, os dados sendo enviados em um pacote separado da consulta). No entanto, esteja ciente de que o PDO silenciosamente recorrerá a emulações de instruções que o MySQL não pode preparar de forma nativa: aquelas que podem ser listadas no manual, mas tome cuidado para selecionar a versão apropriada do servidor).
O feio
Eu disse desde o 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 tem 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 expusemos a API C mysql_set_charset()
até a 5.3.6; portanto, nas versões anteriores, não é possível 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 com o banco de dados deve ser codificada usando um conjunto de caracteres vulneráveis. nãoutf8mb4
é vulnerável e ainda pode suportar todos os caracteres Unicode: portanto, 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 Plano Multilíngue Básico Unicode .
Como alternativa, você pode ativar o NO_BACKSLASH_ESCAPES
modo SQL, que (entre outras coisas) altera a operação do mysql_real_escape_string()
. Com esse modo ativado, 0x27
será substituído por em 0x2727
vez de 0x5c27
e, portanto, o processo de escape não poderá criar caracteres válidos em nenhuma das codificações vulneráveis onde elas não existiam anteriormente (ou 0xbf27
seja, ainda é 0xbf27
etc.) - para que o servidor ainda rejeite a string como inválida . No entanto, consulte a resposta do @ eggyal para uma vulnerabilidade diferente que pode surgir ao usar este modo SQL.
Exemplos seguros
Os seguintes exemplos 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 o MySQLi faz verdadeiras declarações preparadas o tempo todo.
Empacotando
Se vocês:
- Use Versões Modernas do MySQL (final de 5.1, todas as 5.5, 5.6, etc.) Parâmetro de charset DSN do AND
mysql_set_charset()
/ $mysqli->set_charset()
/ PDO (no PHP ≥ 5.3.6)
OU
- Não use um conjunto de caracteres vulneráveis para codificação de conexão (você usa apenas
utf8
/ latin1
/ ascii
/ etc)
Você é 100% seguro.
Caso contrário, você estará vulnerável mesmo usandomysql_real_escape_string()
...