Injeção de SQL
A injeção de SQL é uma técnica onde o agressor explora falhas
no código da aplicação responsável em criar e povoar instruções SQL.
O agressor pode assim obter acesso privilegiado a partes da aplicação,
extrair todos os dados do banco de dados, alterar os dados,
e até mesmo executar comandos perigosos em nível do sistema onde o banco
de dados roda. A falha ocorre quando desenvolvedores concatenam ou
interpolam dados arbitrários em instruções SQL.
Exemplo #1
Dividindo o result set em páginas ... e criando super-usuários
(PostgreSQL)
No exemplo a seguir, dados de usuário são diretamente interpolados na
instrução SQL, permitindo ao agressor obter uma conta de superusuário no banco de dados.
<?php
$offset = $_GET['offset']; // Usando os dados sem validação!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
?>
Usuários normais clicam nos links 'próxima' e 'anterior' onde
$offset
é codificado na
URL. O script espera que o valor de
$offset seja um número decimal. No entanto, e se alguém tentar
quebrar a instrução SQL, utilizando a seguinte
URL:
Se isso acontecesse, então o script daria de presente acesso de superusuário ao
atacante. Perceba que
0;
é para fornecer uma deslocamento válido
para a consulta original e terminá-la.
Nota:
É uma técnica comum forçar o avaliador de SQL ignorar o resto da consulta
escrita pelo desenvolvedor com --
, que é o sinal de
comentário no SQL.
Uma maneira de ganhar senhas é desviar suas páginas de resultado de busca.
A única coisa que o atacante precisa fazer é ver se alguma variável enviada
é usada em um comando SQL que não é tratado corretamente. Esses filtros podem ser
configurados de forma a personalizar cláusulas WHERE, ORDER BY,
LIMIT
e OFFSET
em cláusulas SELECT
Se seu banco de dados suporta o construtor UNION
,
o atacante pode tentar adicionar uma consulta inteira à consulta original para
listar senhas de uma tabela arbitrária. É altamente recomendável gravar apenas
hashs criptográficos das senhas, ao invés de gravar a senha.
Exemplo #2
Listando artigos ... e algumas senhas (qualquer banco de dados)
<?php
$query = "SELECT id, name, inserted, size FROM products
WHERE size = '$size'";
$result = odbc_exec($conn, $query);
?>
A parte estática da consulta pode ser combinada com outro comando
SELECT
que revela todas as senhas:
Instruções UPDATE
e INSERT
também podem
ser abusados em ataques.
Exemplo #3
De recuperando uma senha ... para ganhando mais privilégios (qualquer banco de dados)
<?php
$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
Se um usuário malicioso envia o valor
' or uid like'%admin%
para
$uid para
mudar a senha do administrador, ou simplesmente configura
$pwd para
hehehe', trusted=100, admin='yes
(com um espaço
sobrando) para ganhar mais privilégios. Então, a consulta ficará retorcida:
Pode parecer que o agressor precisa saber alguma coisa
da arquitetura do banco para conduzir um ataque
efetivo, mas obter esse tipo de informação é geralmente bem simples. Por exemplo,
o código pode ser parte de um sistema open source e publicamente disponível.
Esse tipo de informação pode também pode ser obtido
em sistemas de código fechado -- mesmo no caso dele estar ofuscado ou compilado --
e mesmo através do seu próprio código, através de mensagens de erro.
Outros métodos incluem o uso de nomes típicos de tabelas e colunas. Por
exemplo, um formulário de login normalmente utiliza tabelas chamadas 'user' com
colunas chamadas 'id', 'username', and 'password'.
Exemplo #4 Atacando o sistema de um banco de dados (MSSQL Server)
Um exemplo assustador de como comandos do sistema operacional podem ser acessados
em alguns bancos de dados.
<?php
$query = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
?>
Se o atacante enviar o valor
a%' exec master..xp_cmdshell 'net user test testpass /ADD' --
para
$prod, então
$query terá o valor:
O MSSQL Server executa comandos SQL em um lote incluindo um comando
para adicionar um novo usuário para o banco de dados de contas locais. Se essa aplicação
estiver sendo executada como
sa
e o serviço MSSQLSERVER estivesse
sendo executado com privilégios suficientes, o atacante teria agora uma
conta com a qual poderia acessar essa máquina.
Nota:
Alguns dos exemplos acima estão ligados a bancos específicos. Isso não
significa que um ataque similar é impossível contra outros produtos.
Seu servidor de banco de dados pode ter uma vulnerabilidade similar de outra maneira.
Imagem em cortesia de
» xkcd
Técnicas para evitar ataques
A maneira recomendada de evitar ataques de injeção de SQL é informar todos os
dados em instruções preparadas. Usar instruções parametrizadas não é o suficiente
para evitar SQL injection, mas é a maneira mais rápida e segura de fornecer dados
a instruções SQL. Todo os dados dinâmicos em WHERE
,
SET
, e VALUES
precisam ser
substituídos por âncoras. Os dados em si serão informados durante a
execução, e serão enviados separadamente do comando SQL.
Informar dados via parâmetros deve ser utilizado apenas para dados. Outras partes dinâmicas
de uma instrução SQL precisa ser filtrada por uma lista prévia e conhecida de valores válidos.
Exemplo #5 Evitando SQL injection ao utilizar instruções preparadas PDO
<?php
// A parte SQL dinâmica precisa é validada a partir de dados prévios
$sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
$productId = $_GET['productId'];
// O SQL é preparado utilizando âncoras
$stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// O valor é informado, incluindo caracteres curinga
$stmt->execute(["%{$productId}%"]);
?>
Instruções preparadas são fornecidos
no PDO,
no MySQLi,
e por outras bibliotecas de bancos de dados.
Ataques de injeção de SQL são principalmente baseados na exploração de código que não é
escrito pensando em segurança. Nunca confie em nenhum dado enviado pelo usuário,
e menos ainda em dados enviados pelo navegador, mesmo que o dado venha de um option box
ou um campo hidden, nem mesmo cookies. O primeiro exemplo mostra como
uma instrução SQL muito simples pode causar um dano desastroso.
Uma estratégia de defesa envolve várias boas práticas de codificação:
-
Nunca se conecte no banco de dados utilizando um usuário administrador ou dono
dos objetos do banco. Sempre utilize usuários com privilégios mínimos.
-
Sempre verifique se o dado enviado tem o tipo esperado. O PHP possui
várias funções de validação de dados, de coisas simples como
as encontradas em funções de variável e
em funções de string
(por exemplo, is_numeric(), ctype_digit())
a coisas mais avançadas como
Expressões regulares Perl
support.
-
Se a aplicação espera dados numéricos, considere verificando os dados
com ctype_digit(), ou modificar os dados utilizando
settype(), ou ainda reformatando o dado
com sprintf().
-
Se o banco de dados não suportar enviar dados por parâmetros, então
é necessário escapar todos os dados de usuário não numéricos, passando
o dado para funções específicas de escape do banco (por exemplo
mysql_real_escape_string(),
sqlite_escape_string(), etc).
Funções genéricas como addslashes() são úteis apenas
em contextos específicos (por exemplo, no MySQL é possível modificar o
comportamento das aspas com NO_BACKSLASH_ESCAPES), então
o escape específico é necessário.
-
Nunca imprimi nenhum dado ou erros específico do banco de dados, especialmente
dados referentes a schema. Veja também exibição de erros e funções de manipulação e log de erros.
Além disso, você ganha em relatar consultas ou dentro do script
ou no próprio banco de dados, se esse suportar. Obviamente, o relatório é incapaz
de prevenir qualquer tentativa danosa, mas pode ser útil para ajudar a
rastrear qual aplicação foi atacada. O relatório não é útil em si, mas
através da informação que ele contém. Mais detalhes geralmente é melhor que menos.