SQL Injection
Introdução
O que é SQL?
SQL (Structured Query Language) é uma linguagem computacional usada para gerenciar bancos de dados relacionais. Um banco de dados relacional agrupa os dados em tabelas (tables), onde uma tabela é composta por várias linhas e colunas. Cada linha representa um elemento da tabela de dados e cada coluna representa alguma propriedade deste elemento. Por exemplo, considere uma tabela que descreva os produtos de uma certa loja:
001
Cama
100
002
Mesa
200
Qualquer programa de computador, tal como o servidor por trás de algum website, pode se comunicar com um sistema de banco de dados relacional por meio de queries ou consultas SQL. Existem vários tipo de queries, mas a mais importante para o contexto deste documento será a query SELECT. Ela serve para selecionar elementos dentro do banco de dados e enviá-los a nossa aplicação. Ela tem a seguinte forma:
No campo coluna1, coluna2
, é especificado todas as colunas que desejamos receber do banco de dados. No campo tabela
, é especificado a tabela na qual será retirado os dados. O campo coluna1 = 'alguma coisa'
define uma condição que filtra as linhas a serem recebidas, ou seja, neste exemplo, somente as linhas da tabela que tenham o valor da coluna1 sendo igual a 'alguma coisa'
serão retornados pelo banco de dados para nossa aplicação.
O que é SQL Injection (SQLi)?
SQL Injection (SQLi) é um tipo de vulnerabilidade muito comum em aplicações web que usam bancos de dados SQL. Ela permite que invasores manipulem os queries que a aplicação faz para o banco de dados. Isto permite o invasor acessar dados que não devia, tal como dados que pertencem a outros usuários. Em certos casos, é até possível modificar ou excluir esses dados.
Para entender melhor o conceito, considere o exemplo a seguir. Suponha que haja alguma aplicação com a seguinte linha de código no back-end:
Esse código executa uma consulta SQL para buscar os dados de um usuário com um nome e senha correspondentes. No entanto, se a aplicação não tratar corretamente os valores inseridos pelo usuário, um invasor pode realizar um ataque. Se ele definir a variável nome
como "nome aleatorio"
e senha
como " OR 1=1
, a query gerada será:
O problema dessa consulta é que a condição 1=1 sempre retorna verdadeiro, o que significa que o banco de dados irá retornar as informações de todos os usuários, ignorando a necessidade de uma senha válida.
In-band
SQL injections do tipo In-band, ou também chamadas de Classic SQL Injection, são aquelas que o atacante usa o mesmo canal de comunicação tanto para lançar o ataque quanto para receber os dados. Um exemplo é se o atacante usar um browser para enviar um ataque para uma aplicação e então visualizar os resultados neste mesmo browser.
Blind
Em uma SQL injection do tipo Blind, o atacante não consegue visualizar diretamente o resultado do ataque. Em vez disso, ele reconstrói o banco de dados enviando payloads e observando o comportamento da aplicação web.
Superfícies/Vetores de ataque
Existem vários pontos em uma aplicação web onde um atacante pode injetar código malicioso que manipula queries SQL. Alguns destes são:
Parâmetros de URL (Query Strings)
Muitas aplicações usam parâmetros na URL para buscar informações no banco de dados. Se não forem devidamente protegidos, podem ser explorados. Vejamos um exemplo:
Exemplo de URL:
Se a query SQL for:
Um invasor pode manipular a URL para:
Resultando em:
O que pode expor todos os produtos da base de dados.
Formulários de Login
Campos de entrada de usuário e senha são alvos bem comuns em SQL injections. Vejamos um exemplo:
Se um invasor inserir ' OR '1'='1
no campo de senha, a query se transforma em:
Isso pode permitir login sem credenciais válidas. Isso também é um tipo de vulnerabilidade bem comum e recebe o nome de Login Bypass.
HTTP Headers
Algumas aplicações fazem consultas ao banco de dados com base em informações dos HTTP headers, como User-Agent, Referer ou Cookie. Vejamos um exemplo:
Se a aplicação web armazena logs de acesso com o header User-Agent:
Um atacante pode modificar o User-Agent para algo como:
A query se transforma para:
Campos de Pesquisa
Muitas aplicações permitem buscas personalizadas em bancos de dados. Se a entrada não for tratada corretamente, um invasor pode injetar SQL malicioso. Vejamos um exemplos:
Se o usuário digitar %' UNION SELECT * FROM users --
, a consulta se torna:
APIs
APIs que recebem entradas do usuário podem estar vulneráveis a SQL Injection.
Técnicas de detecção
Detectando pontos vulneráveis
É possível detectar vulnerabilidades do tipo SQL injection ao fazer uma série de testes em cada ponto de entrada da aplicação web (superfície/vetor de ataque). Estes testes incluem:
Teste com Aspas Simples (')
Envie um apóstrofo ('
) e observe a resposta da aplicação. Se a aplicação exibir uma mensagem de erro como:
Isso indica que a aplicação não bloqueia caracteres como o '
e que o banco de dados está revelando mensagens de erro detalhadas. Essa falha pode permitir diversos payloads de SQL Injection, facilitando a exploração da vulnerabilidade.
Faça também testes com outros caracteres ou strings que aparecem geralmente em payloads de SQL injection, tais como --
, /**/
, ||
, OR
, IS
, etc.
Teste com Sintaxe SQL Válida e Alterada
Envie comandos SQL que avaliem para o mesmo valor da entrada original e depois alterar para um valor diferente, analisando mudanças na resposta. Por exemplo, suponha um campo de busca onde digitamos batata
e recebemos um certo resultado. Se enviarmos em vez disso:
E recebermos um resultado diferente, há um forte indício de vulnerabilidade.
Teste com Condições Booleanas
Envie expressões booleanas verdadeiras (1=1
) e falsas (1=2
) para analisar as diferenças nas respostas. Vejamos um exemplo:
Se OR 1=1
retorna uma resposta válida (como login bem-sucedido) e OR 1=2
não, a query está vulnerável.
Teste com Time-Based SQL Injection
Se a aplicação não mostrar nenhum tipo de mudança visível, teste os atrasos nas respostas. Envie um dos payloads a seguir:
Se a resposta demorar 10 segundos a mais, isso indica que o banco de dados executou a injeção com sucesso.
Detectando vulnerabilidades em diferentes partes do query
A maioria das vulnerabilidades ocorre dentro da cláusula WHERE de um query SELECT. No entanto, vulnerabilidades do tipo SQL injection podem ocorrer em outras partes do query. Algumas partes comuns são:
Comandos UPDATE
O comando SQL UPDATE serve para atualizar os valores dos elementos de alguma tabela no banco de dados. Ele tem a seguinte forma:
Agora, suponha que uma aplicação faça um query para o banco de dados para alterar o nome de um usuário:
Se a entrada do usuário não for sanitizada, um atacante pode injetar um payload como:
Transformando a query em:
Isso permitiria ao atacante alterar não apenas o nome do usuário, mas também a sua senha para uma que ele controle.
Comandos INSERT
O comando SQL INSERT serve para inserir novos dados dentro de alguma tabela no banco de dados. Ele tem a seguinte forma:
Agora, suponha que uma aplicação faça um query para o banco de dados para criar um novo usuário:
Se um atacante inserir o seguinte payload no campo username
:
A query resultante será:
Por meio do comando DROP, o atacante será capaz de deletar a tabela users
inteira.
Comando SELECT, Injeção dentro de Nome de Tabelas ou Colunas
Agora, voltamos para a query SELECT, porém nos concentramos em outra parte dela além da cláusula WHERE, o nome das tabelas e das colunas. Suponha que uma aplicação permita os usuários pesquisar no banco de dados por meio do nome das tabelas:
Se um atacante injetar o seguinte valor no parâmetro tabela
:
A query resultante será:
Isso pode levar à exclusão da tabela inteira.
Cláusula ORDER BY
Suponha que uma aplicação permita o usuário determinar a ordenação do resultado da query SELECT com o operador ORDER BY:
Se o atacante inserir o seguinte payload no parâmetro ordem
:
A query se tornará:
O que pode revelar as senhas de todos os usuários.
Examinando o Banco de Dados
Antes de começar a explorar as vulnerabilidades de SQL injection no banco de dados, é necessário examinar o banco de dados para determinar seu tipo, versão e estrutura.
Tipos de Bancos de Dados SQL
Apesar de SQL ser um padrão de linguagem bem definido entre os bancos de dados relacionais, cada sistema o implementa a sua própria maneira, criando variações entre diferentes programas de bancos de dados. Dentre os programas de banco de dados mais conhecidos estão o MySQL, PostgreSQL, SQLite e Oracle. Eles todos implementam os comandos principais do SQL (tal como SELECT, UPDATE, DELETE, INSERT, WHERE) de forma similar. Entretanto, há algumas diferenças, como as listadas na tabela a seguir:
Concatenation
CONCAT()
||
, CONCAT()
||
, CONCAT()
OR operator
OR
, ||
OR
OR
Not Equal
!=
, <>
!=
, <>
!=
, <>
Comments
-- abc
, /*abc*/
, #abc
, /*! Special MySQL comment */
--abc
, /*abc*/
--abc
, /*abc*/
Delays
SLEEP()
SLEEP()
Not defined
Compreender as diferenças entre os diversos sistemas de gerenciamento de bancos de dados é fundamental para quem deseja explorar SQL Injections. Esse conhecimento permite que um atacante crie payloads mais eficazes, adaptados ao ambiente específico.
Além disso, identificar o tipo e a versão do banco de dados alvo é um passo essencial na exploração da vulnerabilidade. Abaixo estão alguns comandos SQL que podem ser usados para obter essa informação:
Para SQLite:
select sqlite_version();
Para MySQL:
SELECT VERSION();
ouSHOW VARIABLES LIKE "%version%";
Para PostgreSQL:
SELECT version();
Assim sendo, o atacante poderá combinar algum desses comandos com o operador UNION e criar um payload que o revele informações úteis:
Descobrindo a Estrutura do Banco de Dados
Depois de descobrir a versão do banco de dados, é necessário identificar sua estrutura, ou seja, listar todas as suas tabelas e as colunas delas. A maioria dos programas de bancos de dados (com exceção do Oracle) possuem um objeto chamado information schema que fornece informações sobre o banco. Através dele, é possível listar todas as tabelas:
Esse query irá retornar algo como:
A partir daí, pode-se também listar as colunas de uma das tabelas com:
Isto retorna algo como:
Assim, revela-se o nomes das colunas, bem como o tipo de dados (datatype) delas.
Descobrindo a Estrutura de Banco Oracle
Os bancos de dados Oracle funcionou um pouco diferente. Eles não possuem a information schema. Em vez disso, usa-se o query abaixo para listar as tabelas:
E este query para listar as colunas:
Técnicas de exploração
Depois que o atacante conseguiu detectar os pontos que são vulneráveis a um SQL injection e examinar o banco de dados, ele deve começar a explorar estas vulnerabilidades. Aqui analisaremos algumas técnicas usadas para este propósito:
Login Bypass
Quando o objetivo de um atacante for fazer login como outro usuário sem saber a senha dele, ele pode usar da técnica de Login Bypass. Vejamos um exemplo. Imagine que uma aplicação web realize o login de algum usuário por meio de uma query SQL:
Se a query retornar as informações sobre um usuário, quer dizer que aquele usuário existe e a senha fornecida está correta. O login terá sido feito com sucesso. Caso contrário, a aplicação enviará uma mensagem de erro. É possível para um atacante enviar no campo username
o payload admin'--
, transformando a query em:
O comando SQL --
indica que todo o texto a partir dele até o final da linha será um comentário e, portanto, não deverá ser interpretado como comandos SQL. Sendo assim, todo o trecho AND senha = ''
será desconsiderado e o atacante poderá logar como admin sem mesmo saber a senha correta.
Union-based
Para entender a injection Union-based, é necessário antes conhecer o operador SQL UNION. Ele serve para combinar o resultado de dois ou mais sentenças SELECT. Ele segue o formato:
O Union-based injection usa o operador UNION para acessar diferentes dados de algum banco de dados. No entanto, para que o operador UNION funcione corretamente, deve-se preencher dois requisitos importantes: cada sentença SELECT deve retornar o mesmo número de colunas e os tipos de dados (data types) das colunas correspondentes devem ser compatíveis.
Determinando o número de colunas
Um invasor pode determinar quantas colunas uma query retorna utilizando ORDER BY. Esse operador permite ordenar os resultados tanto pelo nome da coluna quanto pelo seu índice.
Se um índice maior que o número real de colunas for usado, o banco de dados retornará um erro. Assim, ele pode testar, usando os payloads abaixo, até encontrar uma mensagem de erro.
Ajustando o número de colunas
Caso a query injetada tenha menos colunas que a original, pode-se preencher os espaços vazios com NULL:
Por outro lado, se houver mais colunas, pode-se concatenar valores de múltiplas colunas em uma única. Em bancos que usam || como o operador de concatenação, teriamos:
Descobrindo os data types
Depois de saber a quantidade de colunas, um atacante pode testar os tipos de cada uma inserindo diferentes valores e observando se ocorre alguma mensagem de erro:
Boolean-based
Uma Boolean-Based SQL Injection ocorre quando a aplicação não exibe diretamente os dados do banco de dados, mas seu comportamento muda com base nos resultados da query. Isso permite que um invasor extraia informações peça por peça, analisando as respostas da aplicação.
Vejamos um exemplo. Imagine que um site armazene no navegador um cookie chamado user_id
, usado para identificar o usuário. Sempre que uma requisição é feita, a aplicação verifica se o user_id
existe no banco de dados:
Se o usuário for encontrado, o site exibe "Bem-vindo", caso contrário, mostra "Desculpe, não te conheço". Agora, um invasor pode modificar seu cookie para testar diferentes condições:
A segunda consulta retorna um resultado diferente porque '1'='2'
é falso. Isso permite ao invasor testar diversas condições e inferir informações do banco de dados.
Suponha que a tabela users
contenha mais duas colunas, username
e senha
. Para descobrir a senha do usuário Admin, o invasor pode usar a função SUBSTRING() para testar letra por letra:
Se a aplicação ainda responder "Bem-vindo", significa que a primeira letra da senha é maior que 'm'
. Se responder "Desculpe, não te conheço", significa que a primeira letra é menor ou igual a 'm'
. O invasor pode repetir esse teste com 't'
, 'p'
, etc., até descobrir a primeira letra. Depois, ele repete o processo para os demais caracteres:
Error-based
Nesta técnica, o atacante obtém informações por meio de mensagens de erro geradas pelo banco de dados e repassadas indevidamente pela aplicação. Se o sistema estiver configurado para exibir mensagens de erro detalhadas (verbose mode), um atacante pode obter a consulta SQL utilizada pela aplicação simplesmente injetando um apóstrofo '
. Isso pode resultar em um erro como:
Além disso, é possível revelar o conteúdo do banco de dados dentro de uma mensagem de erro utilizando uma payload como:
Essa injeção pode gerar uma mensagem do tipo:
Nesse caso, o atacante tenta converter o conteúdo da coluna coluna1
da tabela tabela1
para um número inteiro usando a função CAST(). No entanto, como o dado armazenado em coluna1
é do tipo texto (string), a conversão falha e a mensagem de erro revela o valor armazenado.
Time-based
Em algumas aplicações, as queries SQL são executadas sem alterar visivelmente o comportamento da página. Nesses casos, a técnica de Boolean-Based SQL Injection pode não funcionar. Para contornar essa limitação, utiliza-se a Time-Based SQL Injection, que explora atrasos na resposta da aplicação para inferir informações do banco de dados.
A ideia é inserir uma condição que, se verdadeira, força um atraso na resposta usando comandos como WAITFOR DELAY:
Se a resposta do servidor demorar, o invasor sabe que a condição injetada era verdadeira. Caso contrário, a condição era falsa. Assim como na Boolean-Based SQL Injection, um atacante pode testar caractere por caractere de uma senha ou outro dado sensível:
Prevenção e mitigação
A prevenção de SQL injections é um tópico essencial para qualquer desenvolve aplicações que fazem uso de banco de dados relacionais. Nesta seção, iremos ver algumas técnicas usadas para impedir ataques deste tipo.
Prepared Statements
Os Prepared Statements (ou declarações preparadas) são uma técnica para executar queries SQL de forma segura, prevenindo ataques de SQL Injection. Eles funcionam separando a estrutura da query dos valores fornecidos pelo usuário, garantindo que qualquer entrada seja tratada como dado e não como parte do comando SQL.
Para isso, a aplicação envia uma query ao banco de dados com espaços especiais destinados a entrada do usuário. Então, o banco de dados irá analisá-la e compilá-la. Em seguida, os valores do usuário são enviados separadamente e são tratados somente como dados. Por fim, a query é executada com segurança. Abaixo, estão alguns exemplos:
Em python com MySQL:
Em php com PDO:
ORM (Object-Relational Mapping)
ORM (Object-Relational Mapping, ou Mapeamento Objeto-Relacional) é uma técnica que permite interagir com bancos de dados usando código orientado a objetos, em vez de escrever queries SQL manualmente. Com um ORM, as tabelas do banco de dados são representadas como classes e os registros como objetos, facilitando a manipulação dos dados de forma mais intuitiva e segura.
Existem muitos frameworks ORM como SQLAlchemy (python), Sequelize (javascript), Hibernate e Entity Framework (C#). Vejamos um exemplo com o framework SQLAlchemy em python:
Aqui, User é uma classe que representa a tabela users
, e o método .query()
permite buscar registros como se fossem objetos Python.
Valide e sanitize a entrada do usuário
Para evitar ataques de SQL Injection, é essencial validar e sanitizar as entradas do usuário antes de utilizá-las no banco de dados. Para validar, verifique se o dado tem formato esperado. Por exemplo, verificar se um campo destinado a preço é realmente um número:
Em python:
Para sanitizar, remova ou substitua caracteres perigosos como '
, -
, ;
e etc.
Em python:
Desative mensagens de erro verbosas
Erros detalhados podem revelar a estrutura do banco de dados para um atacante. Para evitar isso, desative a exibição de erros na aplicação web:
Exemplo PHP:
Desative as mensagens detalhadas no banco de dados:
Exemplo MySQL:
E use logs internos para registrar os erros.
Algumas ferramentas
Existem várias ferramentas que servem para facilitar e automatizar as tarefas relacionadas a exploração de SQL injections. Nesta seção, veremos algumas delas:
SQL Map
O SQLMap é uma ferramenta de código aberto para detecção e exploração automática de SQL Injection. Ele permite que um pentester descubra vulnerabilidades em aplicações web e até mesmo extraia dados de bancos de dados comprometidos. Esta ferramenta pode ser usada por meio do terminal. Vejamos como ela funciona.
Imagine que queremos testar uma vulnerabilidade SQL Injection na URL de uma aplicação. Podemos fazer isso com o comando:
-u
Define a URL de destino do ataque
--dbs
Detecta bancos de dados
Depois disso, o SQL Map irá revelar os parâmetros URL vulneráveis e os bancos de dados disponíveis na aplicação. Então, será possível executar:
-D
Define o banco de dados
-–tables
Lista todas as tabelas
Assim, será retornado todas as tabelas do banco de dados. Em seguida, pode-se executar:
-T
Define a tabela
--columns
Lista todas as colunas
Por fim, pode-se extrair todos os elementos de uma tabela com o comando:
-C
Define as colunas
--dump
Extrai as informações
Links úteis
Documentação MySQL: https://dev.mysql.com/doc/refman/8.4/en/ Documentação PostgreSQL: https://www.postgresql.org/docs/17/index.html1 Documentação SQLite: https://www.sqlite.org/lang.html Para SQLi geral: https://book.hacktricks.xyz/pentesting-web/sql-injection https://www.invicti.com/blog/web-security/sql-injection-cheat-sheet/ Para payloads do SQLite: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/SQLite%20Injection.md Para login bypass: https://book.hacktricks.xyz/pentesting-web/login-bypass/sql-login-bypass
Atualizado