Na semana passada ao navegar por um site, lembrei-me de fazer um pequeno teste à segurança do mesmo e para minha surpresa, ou não, o site era vulnerável a ataques do tipo sql injection.
Depois de ter enviado um e-mail aos responsáveis do site a avisar e de eles terem corrigido o problema, lembrei-me de falar um pouco sobre este tema.
O que é que são ataques do tipo SQL Injection?
Basicamente são ataques que exploram o facto as instruções SQL serem criadas dinamicamente com os valores dos inputs (formulários / querystring), sem que estes sejam devidamente tratados. Estes ataques tornam possível executar código sql, tal como U_PDATE, I_NSERT, D_ROP, etc., e até executar programas no servidor web!
Mas como é que funciona?
Vou tentar mostrar como é que o SQL Injection funciona através de um exemplo muito simples.
Para isso vou criar uma página de autenticação de utilizadores feita em PHP e MySQL.
Resumidamente vou ter:
- uma base de dados MySQL com uma tabela “utilizadores”;
- um ficheiro em php (login.php) onde vou ter o formulário HTML e o código PHP para validar os utilizadores na base de dados.
O aspecto da página login.php será qualquer coisa deste género:

Formulário de Login (login.php)
Passando à parte prática
Neste exemplo, a construção da instrução SQL é feita de forma dinâmica, utilizando os valores dos formulários sem qualquer tipo de validação.
Temos então que:
$sql="S_ELECT id F_ROM utilizadores WHERE login='".$_POST["utilizador"]."' and password='".$_POST["password"]."'";
Se no formulário introduzirmos os dados: “admin” e “1234“, a nossa instrução sql vai ficar:
S_ELECT id F_ROM utilizadores WHERE login='admin' and password='1234'
Aparentemente até aqui não existe qualquer problema. Só se adivinharmos o utilizador e a password é que vamos conseguir efectuar o login com sucesso.
O problema começa quando começamos a utilizar caracteres especiais do sql (que podem variar consoante a base de dados) nos inputs.
Por exemplo: vamos ver o que acontece se introduzirmos no utilizador o seguinte texto:
xpto' or 1=1#
O SQL resultante seria:
S_ELECT id F_ROM utilizadores WHERE login='xpto' or 1=1#' and password='1234'
Neste caso estou a utilizar dois caracteres especiais. a pelica “ ‘ ” e o cardinal “ # “.
O 1.º serve para delimitarmos campos do tipo “texto”.
O 2.º serve para fazer comentários.
Assim sendo, como carácter # é um carácter especial ( carácter p/ comentários), tudo o que vier depois do # vai ser ignorado pelo motor da BD. Ficamos então com:
S_ELECT id F_ROM utilizadores WHERE login='xpto' or 1=1#
Se analisarmos o sql vemos que além de termos ignorado todo o código sql que vier depois do #, estamos também a inserir a condição “ OR 1=1 “. Como esta condição é sempre verdade (1=1), o resultado da pesquisa vai devolver sempre pelo menos um id (a não ser que a tabela utilizadores esteja vazia).
Isto é, apesar de até poder não existir nenhum utilizador com o login=xpto, 1 é sempre igual a 1
Outro código que podem experimentar é:
xpto' or 1=1 limit 1#
Porquê o “limit 1″?
porque desta forma limitamos o n.º de resultados a 1.
Então mas porque é que isso é importante?
Pode ser ou não. Depende da forma como o programador está a validar o resultado da query à base de dados.
Se o programador tivesse feito qualquer coisa deste género:
// ligação à base de dados
mysql_connect("localhost", "root", "******")or die("cannot connect");
mysql_select_db("junkDB")or die("cannot select DB");
// construção da instrução sql
$sql="S_ELECT * F_ROM utilizadores WHERE login='".$_POST["utilizador"]."' and password='".$_POST["password"]."'";
// execução da instrução sql
$result=mysql_query($sql);
// variável que diz o n.º de resultados obtidos
$count=mysql_num_rows($result);
// se o n.º de resultados for igual a 1, então é porque os dados introduzidos
// no formulário são válidos. Isto é, existe um utilizador com o login e password
// iguais aos introduzidos no formulário
if($count==1) {
echo "Dados correctos!";
}
else{
echo "Dados incorrectos!";
}
com o código:
xpto' or 1=1#
Só iríamos conseguir “entrar” caso existisse apenas 1 registo na tabela utilizadores.
Bastava a tabela ter dois utilizadores para já não conseguirmos “entrar”.
Ao utilizarmos o “limit 1″ estamos a dizer que só queremos 1 registo. Logo a condição ($count==1) vai ser sempre verdadeira.
Código Necessário para simular este exemplo:
Criar a tabela “utilizadores”:
-- -- Table structure for table `utilizadores` -- C_REATE TABLE IF NOT EXISTS `utilizadores` ( `id` int(4) NOT NULL AUTO_INCREMENT, `login` varchar(65) NOT NULL DEFAULT '', `password` varchar(65) NOT NULL DEFAULT '', PRIMAR Y KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=8 ; -- -- Dumping data for table `utilizadores` -- I_NSERT INTO `utilizadores` (`id`, `login`, `password`) VALUES (1, 'gestor', '1234'), (2, 'admin', '1234');
O conteúdo do ficheiro login.php é:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Login - Security test</title>
<style type="text/css">
/*folha de estilos básica*/
*{font-family:verdana; font-size:12px; color:#333;}
form{width:400px; border:2px solid #ddd;}
form p{font-size:14px; font-weight:bold;}}
input{ border: solid 1px #ddd; background-color:#f5f5f5;}
label{font-weight:bold;}
.red{color:red;}
.green{color:green;}
#sql{ width:400px; background-color:#336699; color:white; padding:5px; margin:10px; text-align:left;}
</style>
</head>
<body>
<center>
<!-- formulário -->
<form name="form1" method="post" action="login.php?action=validateUser">
<p>Acesso Reservado</p>
<label for="username">Utilizador:</label>
<input name="utilizador" type="text" id="username" />
<br />
<label for="password">Password:</label>
<input name="password" type="password" id="password" />
<br />
<input type="submit" name="Submit" value="Login" />
</form>
<!-- // formulário -->
<?php
// "verificar" se o formulário foi submetido e validar o utilizador
if(isset($_POST["utilizador"]) && isset($_POST["password"]) ) {
// ligação à base de dados
mysql_connect("localhost", "root", "********")or die("cannot connect");
mysql_select_db("junkDB")or die("cannot select DB");
// construção da instrução sql
// notem que estou a utilizar os inputs directamente, sem qualquer tipo de validação.
$sql="S_ELECT * F_ROM utilizadores WHERE login='".$_POST["utilizador"]."' and password='".$_POST["password"]."'";
// execução da instrução sql
$result=mysql_query($sql);
// variavel que diz o n.º de resultados obtidos
$count=mysql_num_rows($result);
// se o n.º de resultados for igual a 1, então é porque os dados introduzidos
// no formulário são válidos. Isto é, existe na base de dados UM utilizador
// com o login e password iguais aos introduzidos no formulário
if($count==1) {
echo "<p class=\"green\">Dados correctos!</p>";
}
else{
echo "<p class=\"red\">Dados incorrectos!</p>";
}
echo "<div id=\"sql\"><b>SQL:</b><br>$sql</div>";
}
?>
</center>
</body>
</html>
Para quem quiser aprofundar os conhecimentos sobre este tema ficam aqui alguns links muito interessantes:
- SQL Injection Attacks by Example
- SQL Injection Cheat Sheet
- SQL Injection Walkthrough
- guide-to-php-security-ch3
- Testing for SQL Injection (OWASP-DV-005)
Notas:
- Este exemplo é muito simples e tem como objectivo apenas mostrar o que é o sql Injection.
- Relativamente ao site do meu teste, de referir que enviei um e-mail aos responsáveis pelo site a avisar dos problemas de segurança, tendo corrigido de imediato os problemas e agradecido o meu aviso. Gostei
#1 by Pedro Henrique on August 20, 2009 - 9:31 pm
Quote
Muito bom o post Tiago, eu tenho um site e presto serviços como desenvolvedor web e segurança, só que no meu site não posso ensinar como o ataque é feito se não meus serviços não serão necessarios neh auhahhauuha
queria lembrar dos ataques por querystring tbem né
dá uma olhada no meu site la eu fiz ele a menos de 1 semana e estou postando alguns artigos tbem …
abracao !
#2 by David on October 2, 2009 - 1:27 pm
Quote
Boas,
Não esquecer que temos funções ou procedimentos básicos para prevenir este tipo de situações.
Por exemplo: a função PHP mysql_real_escape_string()
Ou então filtrar/validar o conteúdo de qualquer variável user input.
Abraço
#3 by tiago on October 2, 2009 - 4:38 pm
Quote
Viva,
Este problema é transversal às várias plataformas existentes (.net, php, jsp, etc.).
E sem dúvida que a melhor solução para evitar estes problemas é a validação de todos os dados.
Sejam eles introduzidos pelo utilizador ou gerados pela aplicação (variáveis utilizadas em querystring, etc.).
Além disso, ainda é possivel utilizar alguns mecanismos nos servidores web (exemplo: mod_security para o Apache) que evitam determinados ataques.
Abraço.