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, gbke sjis. Vamos selecionar gbkaqui.
Agora, é muito importante observar o uso SET NAMESdaqui. 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 , 0x27por 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: 0xbf5cseguida 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 latin1a 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 $varno gbkconjunto 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. PDOO 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 PDOnã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 é utf8que 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_ESCAPESmodo SQL, que (entre outras coisas) altera a operação do mysql_real_escape_string(). Com esse modo ativado, 0x27será substituído por em 0x2727vez de 0x5c27e, 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 0xbf27seja, ainda é 0xbf27etc.) - 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() ...