Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Injeção de SQL que contorna mysql_real_escape_string()


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:

  1. 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 e sjis . Vamos selecionar gbk 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 da API C mysql_set_charset() , estaríamos bem (em versões do MySQL desde 2006). Mas mais sobre por que em um minuto ...

  2. A carga

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

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

  3. mysql_real_escape_string()

    A chamada da API C para mysql_real_escape_string() difere de addslashes() 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 usando latin1 para a conexão, porque nunca dissemos o contrário. Nós informamos ao servidor estamos usando gbk , mas o cliente ainda acha que é latin1 .

    Portanto, a chamada para mysql_real_escape_string() insere a barra invertida, e temos um ' pendurado livre personagem em nosso conteúdo "escapado"! Na verdade, se olharmos para $var no gbk conjunto de caracteres, veríamos:
    縗' OR 1=1 /*

    Qual é exatamente o que o ataque exige.

  4. 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() ...