Por Vinicius Moraes
SQL Injection (ou SQLi) é um tipo de Injeção de código (code injection) em que é possível manipular consultas SQL realizadas por uma aplicação em um banco de dados. Um problema que já era discutido ao menos desde 1998 e que ainda hoje afeta muitas aplicações.
Falhas de Injeção (SQL, NoSQL, OS, LDAP, etc) estão no primeiro lugar do OWASP Top Ten 2017, que atualmente é a versão mais recente de um documento que lista os maiores riscos de segurança para aplicações web.
E de fato, a partir de um SQL Injection é possível realizar uma série de ataques perigosos: como obter o controle de leitura e escrita sobre um banco de dados; e, em alguns casos, é possível conseguir uma execução de comandos remotos na máquina — do inglês Remote Command Execution (RCE), como veremos adiante.
É importante ressaltar que esse problema não se restringe nem a uma linguagem, nem a um banco de dados específicos. Dito isso, todos os exemplos que irão ser demonstrados utilizam a linguagem PHP e o banco de dados MySQL por opção do autor.
Entendendo o SQLi
Vamos então ao nosso primeiro exemplo. Considere o trecho de código vulnerável abaixo:
Inicialmente, podemos notar que esse código é de uma funcionalidade de autenticação. Ele está recebendo uma credencial (username e password), e a partir daí, usando-a para tentar recuperar dados do usuário no banco de dados. Como o valor fornecido pelo usuário está sendo utilizado diretamente na criação da consulta SQL, sem a realização de qualquer tipo de tratamento, podemos ver que o exemplo está vulnerável ao SQL Injection.
Para explorar a falha do exemplo 1, um atacante pode submeter, como credenciais da aplicação, os valores admin’ — e password123. O símbolo ‘ (aspa simples) causará o fechamento da string que originalmente deveria conter o nome do usuário; e então os sinais de comentário — (dois hífens seguidos por um espaço) farão com que a consulta SQL resultante, que é ilustrada a seguir, não realize a verificação de senha, assim permitindo a autenticação apenas com o nome do usuário:
Além disso, o ponto afetado também poderia ser um campo numérico. Vejamos este outro caso:
Por sua vez, esse código busca um documento pertencente ao usuário que corresponda ao id informado por ele.
Visto que a consulta também é construída a partir da entrada do usuário, sem nenhum tipo de tratamento, um atacante pode submeter o valor 1 OR 1=1 no campo id, resultando em um WHERE de condição sempre verdadeira e consequentemente fazendo com que todos os documentos sejam retornados. Isso não só permite ao atacante o acesso aos documentos de outros usuários; como também, a depender do volume de dados armazenados, pode causar uma negação de serviço (do inglês Denial Of Service, DoS).
É interessante destacar que o SQLi não se limita apenas ao SELECT, ele pode ocorrer em outras instruções como INSERT, UPDATE, DELETE. Além disso, analisando a funcionalidade fornecida na aplicação, muitas vezes é fácil deduzir qual instrução foi utilizada.
O exemplo 3 é de uma funcionalidade que cria documentos. Aqui, caso um atacante inserir o valor opa.doc”,user()); — no campo do nome, será criado um documento com o nome opa.doc, que conterá em seu conteúdo o resultado da função user().
Até agora, observamos formas simples de exploração de um SQL Injection. A seguir, veremos estratégias mais avançadas que podem ser empregadas de acordo com as características de cada sistema.
Union-based
Na linguagem SQL existe o operador UNION, que une o resultado de múltiplas instruções SELECT. Assim, quando uma injeção acontece em um SELECT, é possível utilizar esse operador em uma técnica conhecida como Union-based, para criar uma nova consulta.
No entanto, deve-se atentar que, para usar o UNION, a segunda consulta tem que retornar a mesma quantidade de colunas da consulta original; além disso, elas devem ter tipos de dados compatíveis. Isso pode ser resolvido por tentativa e erro, ao incrementar o número de elementos da segunda consulta com valores null. Como é demonstrado abaixo, em uma exploração contra o exemplo 2:
Adicionalmente, o UNION pode ser utilizado em conjunto com outras técnicas, como será ilustrado na Imagem 12. E ele também pode criar a necessidade de se conhecer os nomes das tabelas e colunas do banco de dados, o que pode ser resolvido através da leitura de tabelas padrões que contém essas informações (information_schema.tables em MSSQL e MySQL, all_tables em Oracle).
Stacked Queries
Stacked queries é o nome de uma funcionalidade que permite a realização de múltiplas operações SQL de uma vez só. Então, no cenário de exploração de uma falha SQLi, é possível utilizar o operador ; (ponto e vírgula) para finalizar a consulta original e então criar uma nova.
No entanto, esse método nem sempre retorna o resultado da consulta injetada, visto que a aplicação pode ter sido desenvolvida para devolver apenas a saída de uma consulta. Logo, pode ser interessante utilizá-lo em conjunto com outras técnicas, como as Out-Of-Band, que serão abordadas mais adiante. Além disso, stacked queries só são suportadas por alguns bancos de dados (MSSQL suporta, enquanto MySQL e Oracle não suportam).
Error-based
Quando uma aplicação expõe mensagens de erro referentes ao banco de dados, a técnica Error-based pode ser utilizada, com o objetivo de se aproveitar dessas mensagens para realizar uma exploração.
Considere o código vulnerável abaixo:
Ao inserir caracteres que quebram a consulta, é possível notar o surgimento de mensagens de erro do banco de dados na resposta da aplicação.
Para realizar esse tipo de exploração, o banco de dados é um fator que precisa ser considerado. No caso do MySQL, podemos forçar uma mensagem de erro no parser XML dele, através da função extractvalue que espera um XML no primeiro parâmetro; e no segundo, uma expressão que será usada para buscar um valor no XML, isto é, um XPath.
Como o XPath tem uma sintaxe bem definida, é possível iniciá-lo com um caractere inesperado, como um ponto final ‘.’, e o concatenar com uma consulta válida, que terá seu resultado retornado na mensagem de erro. Veja o caso a seguir:
Blind SQL Injection
Quando uma aplicação web vulnerável não envia nas suas respostas (o resultado das consultas SQL ou mensagens de erro do banco de dados) o SQL Injection é categorizado como Blind, e por isso, pode ser chamado de Blind SQL Injection.
Como ilustrado a seguir, é possível reproduzir esse cenário ao modificar um pouco o exemplo 4:
Nos próximos tópicos, veremos alguns subtipos que existem no Blind.
Boolean-based
No exemplo 5, a aplicação busca documentos pelo nome e se comporta de forma distinta de acordo com a existência deles. Nesse contexto, é interessante o uso da técnica Boolean-based, que se aproveita dessa diferença de respostas para avaliar expressões, e com isso, extrair informações.
Então, ao se inserir um nome de documento que já existe na aplicação (doc1), seguido por um operador AND com uma expressão a ser avaliada, é possível descobrir caractere por caractere do resultado de uma expressão, como demonstrado abaixo:
Time-based
Prosseguindo para o nosso sexto exemplo vulnerável, desta vez a aplicação retorna sempre a mesma resposta, independente do valor submetido pelo usuário. Para isso, as funções mysqli_fetch_array e die do exemplo anterior foram removidas, como é demonstrado abaixo:
Com isso, o retorno da aplicação independe da consulta SQL, de modo a ser necessário utilizar uma outra fonte de dados para a exploração — o tempo de resposta — em uma técnica conhecida como Time-based.
A ideia do Time-based é semelhante a da técnica Boolean-based; porém agora é necessária a utilização de uma função que irá executar um sleep (gerando uma diferença no tempo de resposta), a depender do resultado da expressão avaliada.
No MySQL, existe a função IF, que recebe uma expressão a ser avaliada no primeiro argumento; e de acordo com o resultado dela, escolhe o segundo ou o terceiro argumento para ser retornado. Assim, é possível realizar a exploração Time-based, como demonstrado a seguir:
Na imagem acima, é possível notar que quando a expressão do IF está incorreta, a função sleep aumenta 2 segundos no tempo de resposta; quando não, a resposta retorna no seu tempo normal, então essa diferença de comportamento possibilita a identificação do primeiro caractere retornado pela função user(), ‘r’.
Out-of-Band
Por fim, chegamos ao último exemplo de exploração, que acontece quando o ponto vulnerável não permite nem mesmo a contabilização do tempo da consulta SQL. O que pode ser reproduzido ao modificar a função mysqli_query que foi utilizada no exemplo 6, como ilustrado a seguir:
Nesse exemplo, a funcionalidade é assíncrona, isto é, a aplicação não aguarda o término da consulta no banco para responder ao usuário. Então, é necessário tentar um grupo de técnicas conhecidas como Out-of-Band, que para obter os dados do ataque utilizam um canal de comunicação diferente do que é utilizado para realizar o ataque.
Uma boa opção é criar uma consulta que ao ser executada envia o seu resultado para um servidor externo na internet. E por mais que o MySQL não tenha diretamente implementado uma função para realizar esse tipo de comunicação, ele fornece funcionalidades de manipulação de arquivos, que no Windows utilizam a função CreateFile.
CreateFile é um API do Windows que, através da implementação realizada pelo MySQL, permite não só o acesso aos arquivos da própria máquina (nesse caso o servidor rodando o MySQL), como também possibilita o acesso a arquivos na rede. Isso significa que quando o MySQL está rodando no Windows, é possível criar uma consulta que envia dados pela internet. Como é demonstrado abaixo:
Na imagem 10, através da função load_file, o MySQL tenta ler um arquivo do domínio hrcxgmpghhpj6spml8g1nklullrff4.burpcollaborator.net pelo protocolo SMB; porém, antes ele precisa realizar uma consulta DNS desse domínio. E ao fazer isso, o valor de @@version é enviado para um servidor DNS, como se fosse o nome de um subdomínio, o que pode ser observado na imagem a seguir:
Mesmo que a exploração anterior não possa ser realizada em distribuições Linux, ainda existe a possibilidade de manipular arquivos locais, que não se restringe a um sistema operacional. Nesse cenário, se a máquina estiver rodando um servidor web e for possível escrever em um diretório dele, o resultado de uma consulta pode ser salvo em um arquivo e posteriormente acessado, como demonstrado abaixo:
Uma opção ainda mais interessante nesse caso, é utilizar a permissão de escrita em arquivos para obter um RCE. Para isso, basta enviar um código feito em uma linguagem suportada pelo servidor web (no nosso exemplo, o PHP), que irá executar, no sistema operacional do alvo, um comando enviado pelo usuário, de modo a retornar o resultado: enviar o payload de uma web shell.
Nesse momento, é possível pensar que técnicas Out-of-Band são sempre as melhores opções, ao menos quando se fala de Blind SQL Injection. No entanto, isso não é verdade, pois são muitos os fatores que podem torná-las inviáveis, como por exemplo, a falta de permissão para escrever em diretórios. O que no MySQL pode ser causada por um valor restritivo (que já é o padrão de instalação) na variável secure_file_priv; pela falta da permissão FILE no usuário do banco; ou ainda, pela falta da permissão de escrita para o usuário que executa o banco de dados no sistema operacional.
Logo, quando o assunto é exploração de SQLi “balas de prata” realmente não existem; no lugar delas, existem diversas técnicas cada qual ideal para lugar e momento específicos.
Sqlmap
Sqlmap é o nome de uma ferramenta muito popular, escrita em Python e de código aberto, que serve para identificação e exploração de SQL Injection. Ele é muito interessante para automatizar ataques; pois permite, por exemplo, explorar de forma simples um ponto que precisa da técnica Time-based, abstraindo todo o trabalho necessário para a leitura de cada caractere.
Então, vamos ver um caso de uso do sqlmap contra o exemplo 6, que foi explorado anteriormente com a técnica Time-based:
As opções — dump e — stop 3 fazem, respectivamente, com que os dados das tabelas do banco sejam recuperados e que eles se limitem a 3 entradas.
Já a opção -u (abreviação de — url) define o alvo, para casos mais complexos ela pode ser substituída por -r, seguida pelo caminho de um arquivo que contém a requisição HTTP que será realizada.
Além disso, por padrão, o sqlmap já testa todos os parâmetros da URL de um GET, e todos do body de um POST, mas é possível especificar outros pontos.
Correção
Quando o objetivo é corrigir o SQL Injection, a melhor opção é conhecida como consultas parametrizadas, do inglês parameterized queries, também chamada de prepared statements.
Consultas parametrizadas são uma forma que a aplicação tem de previamente estruturar as operações que serão realizadas no banco de dados. Elas não são um recurso exclusivo de uma tecnologia, e ao serem utilizadas necessitam que seja indicado o local onde a entrada do usuário será inserida, isto é, um placeholder. Dessa forma, é possível corrigir completamente falhas de SQLi, como no caso do exemplo 1, agora corrigido:
Adicionalmente, consultas parametrizadas também trazem uma melhoria de performance. Isso ocorre pois existem diversos passos no tratamento de uma consulta SQL. Basicamente, primeiro é realizada uma checagem que verifica se ela faz sentido (análise sintática e semântica); em seguida ela é compilada, gerando uma linguagem que máquinas entendem; para então ela ser executada.
Todas essas etapas se repetem sempre que uma consulta padrão acontece. Já no caso das consultas parametrizadas, o banco de dados pode guardar o código que foi gerado em cada operação, e assim pular direto para a etapa de execução. Como a compilação não é realizada novamente, todos os placeholders são lidos puramente como dados através de um protocolo binário, diferente do padrão em que a consulta completa é enviada ao banco de dados. Assim, os dados do usuário não conseguem alterar a lógica da consulta e causar injeções de SQL. Porém, isso também limita as partes da consulta que podem receber as entradas do usuário. Por exemplo, no caso de uma aplicação que define a tabela utilizada no ORDER BY, utilizando diretamente o dado enviado pelo usuário, não seria possível fazer uso de consultas parametrizadas.
Para resolver isso, é possível utilizar uma allowlist que irá restringir os dados recebidos, aceitando apenas um número conhecido e limitado de valores, e assim impedindo que comandos SQL maliciosos sejam injetados. O código abaixo ilustra essa solução, em uma funcionalidade que lista usuários:
A imagem, a seguir, demonstra o exemplo 9 em funcionamento:
Por fim, é importante lembrar que todos os dados externos recebidos por terceiros são perigosos, e por tanto, nunca devem ser utilizados na construção de uma consulta SQL sem os devidos tratamentos que foram abordados.
Até a próxima!