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:

sqlInjection-login-vazio

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:

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 :)
Testing for SQL Injection (OWASP-DV-005)

Tags: , ,