Encontre e Corrija uma SQL Injection
Rode o DVWA localmente no Docker, encontre e explore uma SQL injection real nos níveis de segurança baixo e médio para entender exatamente como funciona, e depois reescreva a lógica equivalente usando queries parametrizadas para provar que a correção realmente fecha o buraco.
Problema
SQL injection acontece quando a entrada do usuário é concatenada diretamente numa string de query SQL em vez de ser passada como um parâmetro separado e tipado. O banco de dados não consegue diferenciar "dado que o desenvolvedor escreveu" de "dado que o atacante forneceu" — então a entrada de um atacante pode mudar a *estrutura* da própria query: transformar uma busca de uma linha num dump da tabela inteira, contornar completamente uma checagem de login, ou extrair dados de tabelas que a query nunca deveria tocar. Neste lab você vai rodar o **DVWA (Damn Vulnerable Web Application)** num container Docker local — uma aplicação deliberadamente construída com vulnerabilidades reais para treinamento. Você vai encontrar e explorar sua SQL injection clássica no nível de segurança "low", entender por que o escaping ingênuo do nível "medium" ainda está quebrado, e depois voltar para o nível de código: reescrever a lógica de query equivalente (como um pseudo-endpoint, já que o próprio DVWA é PHP) usando **queries parametrizadas / prepared statements**, e provar que sua versão reescrita resiste exatamente ao payload que quebrou a original. > Só rode estas técnicas contra o container DVWA local que você mesmo sobe > neste lab (`localhost`). Nunca rode payloads de SQL injection contra qualquer > site, API ou banco de dados que você não seja dono ou não tenha permissão > explícita por escrito para testar — fazer isso contra sistemas de terceiros é > ilegal na maioria das jurisdições.
Objetivos
Ao final deste lab você será capaz de:
- Subir o DVWA localmente no Docker e configurá-lo para uma sessão de prática
controlada e offline. - Identificar um ponto de SQL injection observando como o comportamento da
aplicação muda com entradas manipuladas (mensagens de erro, linhas extras,
lógica booleana). - Explorar uma SQL injection clássica baseada em UNION para extrair dados fora
do escopo pretendido da query. - Explicar por que o escaping/blocklist ingênuo (DVWA "medium") ainda falha, e
qual classe de payload o derrota. - Reescrever a query vulnerável usando queries parametrizadas / prepared
statements e explicar por que elas fecham a vulnerabilidade estruturalmente,
não só cosmeticamente. - Escrever um teste que prove que o payload de injeção não altera mais o
comportamento da query contra a versão corrigida.
Pré-requisitos
Para completar este lab você vai precisar de:
- Docker instalado e capaz de rodar
docker run -p 80:80 vulnerables/web-dvwa. - Um navegador web para interagir com a interface do DVWA.
- Familiaridade básica com SQL (
SELECT,UNION,WHERE). - Um projeto backend ou arquivo de rascunho em qualquer linguagem com um
cliente de banco SQL, para escrever o pseudo-endpoint "antes/depois" no
Passo 5 (não precisa ser PHP — o ponto é a lógica da query, não a linguagem).
O modelo mental: código vs. dado
Uma query SQL deveria ser código que o desenvolvedor escreveu, com a
entrada do usuário encaixada como dado. Concatenação de string destrói essa
fronteira — o banco de dados recebe uma única string e não tem como saber quais
partes eram para ser uma instrução fixa e quais vieram de um atacante:
Template da query: SELECT * FROM users WHERE id = '<entrada do usuário>'
Pretendido: id = '3' → busca normal
Atacante: id = '3' OR '1'='1' → a cláusula WHERE agora é sempre verdadeira
Atacante: id = '3' UNION SELECT user,password FROM users -- '
→ a query agora retorna um segundo conjunto de resultados, não relacionado
Queries parametrizadas (prepared statements) corrigem isso no nível do
protocolo: a estrutura da query é enviada ao banco primeiro e separadamente,
e o valor do usuário é vinculado depois como dado puro. O banco é avisado, de
antemão, "este placeholder é um único valor string", então nenhuma quantidade
de aspas, UNION ou marcadores de comentário dentro desse valor consegue mudar
a forma da query. É por isso que escaping (tentar neutralizar caracteres
perigosos no código da aplicação) é mais fraco que parametrização (nunca deixar
a entrada do usuário tocar a string da query).
Por que o DVWA
O DVWA é intencionalmente vulnerável e vem com um seletor de nível de segurança
(low, medium, high, impossible) para que você veja o mesmo endpoint ficar
progressivamente mais difícil de explorar — e, crucialmente, veja que "medium"
ainda não é seguro. Essa progressão é a melhor forma de internalizar por que o
tipo de correção importa mais que a quantidade de filtragem.
Como trabalhar neste lab
Os Passos 1–2 sobem e logam no DVWA. Os Passos 3–4 exploram os níveis low e
medium. O Passo 5 é a correção — não pule ele; todo lab de exploração deste
catálogo termina com remediação, não só um ataque funcionando.
Passos
-
Suba o DVWA localmente e faça login
Rode a imagem oficial do DVWA localmente. Ela escuta na porta 80 e precisa
de uma configuração única de banco de dados na primeira inicialização.docker run --rm -d --name dvwa -p 80:80 vulnerables/web-dvwaAbra
http://localhost/setup.phpno seu navegador e clique em
"Create / Reset Database". Depois vá parahttp://localhost/login.php
e faça login com as credenciais padrão:usuário: admin senha: passwordCheckpoint
Depois de logar, clique em DVWA Security no menu à esquerda e confirme
que o seletor de nível de segurança está visível com as opçõeslow,
medium,high,impossible. Deixe em low por enquanto — você vai
mudar no Passo 4.Entregável deste passo: DVWA rodando em
http://localhost, logado
comoadmin, com o nível de segurança emlow. -
Encontre o ponto de injeção (segurança low)
Navegue até o módulo SQL Injection no menu à esquerda. É um formulário
simples: você entra com um "User ID" e ele imprime o primeiro/último nome
desse usuário.Comece com a entrada normal, esperada, para ver o comportamento de
referência:User ID: 1 → Resultado: First name: admin, Surname: adminAgora sonde com uma aspas simples — o primeiro movimento clássico para
encontrar injeção, porque frequentemente quebra o literal de string da
query e revela um erro de banco de dados ou uma página em branco:User ID: 1'Você deve ver um erro SQL (algo como
You have an error in your SQL syntax...) — esse é o ponto de injeção se anunciando. A aplicação está
fazendo algo como:SELECT first_name, last_name FROM users WHERE user_id = '$id';e sua
'fechou a string cedo demais, deixando uma aspas solta que o
banco não consegue interpretar.Confirme com um payload booleano
User ID: 1' OR '1'='1Se isso retornar todos os usuários em vez de só um, você confirmou que
a cláusulaWHEREestá sendo sobrescrita, não só quebrada.Checkpoint
Anote a requisição/resposta exata para os dois payloads (
1'e
1' OR '1'='1). Você deve conseguir apontar: a mensagem de erro que
revelou a injeção, e o resultado da query que provou que você controlou a
lógica doWHERE.Entregável deste passo: dois pares payload/resposta observados,
provando que o campo User ID é concatenado sem proteção numa query SQL. -
Explore com UNION para extrair outros dados
Uma injeção baseada em
UNIONacrescenta um segundoSELECT, escolhido
pelo atacante, ao conjunto de resultados da query original. Funciona quando
você consegue casar a contagem de colunas e conseguir que a saída seja
renderizada em algum lugar visível — exatamente o caso do DVWA, já que ele
imprime duas colunas (first_name, last_name).Passo 1: descubra a contagem de colunas
User ID: 1' ORDER BY 1-- - User ID: 1' ORDER BY 2-- - User ID: 1' ORDER BY 3-- -Aumente o número até a query dar erro (
Unknown column) — o último que
funcionou te diz a contagem de colunas. No DVWA padrão isso confirma
2 colunas.Passo 2: extraia dados com UNION SELECT
Agora substitua por um
UNION SELECTcom 2 colunas, puxando de uma tabela
completamente diferente:User ID: 1' UNION SELECT user, password FROM users-- -Isso deve imprimir toda linha de
users.usereusers.passwordnos
mesmos campos de exibição "First name / Surname" — dados que a busca por
User ID nunca deveria expor. No banco de exemplo do DVWA, as senhas são
hashes MD5; anote isso também (hashing fraco é um problema separado da
injeção, mas agrava o dano aqui).Por que isso funciona estruturalmente
A query original virou:
SELECT first_name, last_name FROM users WHERE user_id = '1' UNION SELECT user, password FROM users-- -';O
--comenta a aspas final da query original para que ela não cause um
erro de sintaxe. DoisSELECTs com contagens de coluna compatíveis, unidos
por UNION, são indistinguíveis de um só para a aplicação — ela só imprime
quaisquer linhas que voltarem.Checkpoint
Confirme que você extraiu pelo menos as colunas
userepasswordde
toda linha na tabelausers, usando só o formulário web — sem acesso
direto ao banco. Anote a string de payload final que você usou.Entregável deste passo: o payload
UNION SELECTfuncionando e os
dadosuser/passwordextraídos por ele, demonstrando exposição completa
de uma tabela que o endpoint nunca deveria tocar. -
Veja por que a segurança 'medium' ainda falha
Volte para DVWA Security e mude o nível para medium. Abra o módulo
SQL Injection de novo — repare que agora é um dropdown<select>de IDs
numéricos em vez de um campo de texto livre, e o código por trás troca a
concatenação crua pormysqli_real_escape_string()na entrada.Escaping neutraliza caracteres de aspas, mas não muda a estrutura da
query, e não faz nada se você conseguir passar entrada numérica/sem aspas.
Como a fonte é um dropdown, a UI direta do navegador resiste a digitação
casual — mas a checagem do lado do servidor ainda é só escaping, e a
própria requisição é um parâmetro HTTP normal que você pode forjar
diretamente.Contorne com uma requisição HTTP crua
Use curl (ou as dev tools do navegador) para enviar um valor que o
dropdown nunca ofereceria, mandando seu cookie de sessão para a requisição
ser autenticada:# Pegue seu cookie PHPSESSID nas dev tools do navegador depois de logar curl -s "http://localhost/vulnerabilities/sqli/?id=1+UNION+SELECT+user,password+FROM+users-- -&Submit=Submit" \ -H "Cookie: PHPSESSID=<seu-session-id>; security=medium" \ | grep -A2 "first_name"Como a query ainda é construída como
... WHERE user_id = $id(contexto
numérico, sem aspas para escapar) ou como uma string que só remove aspas em
vez de parametrizar, um payload UNION numérico/sem aspas atravessa o
escaping direto.Checkpoint — nomeie o buraco real
Responda com suas próprias palavras: por que adicionar
mysqli_real_escape_string()não corrigiu a vulnerabilidade? A resposta
deve mencionar que o escaping trata sintomas (caracteres perigosos) em vez
da causa (a entrada do usuário sendo interpretada como parte da gramática
da query de todo) — um valor sem aspas para escapar (como umUNION SELECTpuro) atravessa direto.Entregável deste passo: uma extração UNION bem-sucedida contra a
segurança "medium" via requisição forjada, mais um parágrafo explicando
por que só o escaping não é uma correção estrutural. -
Corrija: reescreva com queries parametrizadas (submissão)
O próprio DVWA é PHP e está fora de escopo para corrigir aqui — o ponto
deste passo é provar que você entende a correção correta reescrevendo a
lógica equivalente na sua própria stack, e escrever um teste que mostre que
o payload exato do Passo 3 não funciona mais.O padrão vulnerável (pseudocódigo, espelha o DVWA)
def buscar_usuario_por_id(id): query = "SELECT first_name, last_name FROM users WHERE user_id = '" + id + "'" return db.execute(query) # id é concatenado direto na string SQLA correção: query parametrizada / prepared statement
def buscar_usuario_por_id(id): query = "SELECT first_name, last_name FROM users WHERE user_id = ?" return db.execute(query, params=[id]) # id é vinculado como dado, nunca interpretado como SQLTraduza isso para sua stack real. Alguns exemplos concretos:
# Python + um driver DB-API (psycopg2, sqlite3, mysql-connector, ...) cursor.execute( "SELECT first_name, last_name FROM users WHERE user_id = %s", (user_id,), )// Node.js + node-postgres await pool.query( "SELECT first_name, last_name FROM users WHERE user_id = $1", [userId] );# Ruby / ActiveRecord User.where("user_id = ?", user_id)Qualquer linguagem que você use, a forma é a mesma: a string da query com
placeholders é fixa e conhecida de antemão; a entrada do usuário é entregue
ao driver separadamente e nunca toca a string SQL em si.Prove a correção com um teste
Escreva um teste (no framework de teste da sua stack, ou um script
pequeno) que roda o payload exato do Passo 3 contra sua versão
parametrizada e afirma que ele se comporta como dado inerte, não como
SQL:test "payload UNION é tratado como um ID literal, não executado": payload = "1' UNION SELECT user, password FROM users-- -" resultado = buscar_usuario_por_id(payload) # Uma query parametrizada trata a string inteira como o valor de user_id. # Nenhuma linha tem essa string literal como id, então esperamos zero linhas — # NÃO uma coluna de senha vazada e NÃO um erro SQL. espera(resultado) == [] test "busca normal ainda funciona": resultado = buscar_usuario_por_id("1") espera(resultado) == [{ first_name: "admin", last_name: "admin" }]Por que isso é uma correção estrutural, não um filtro mais forte
Diferente do escaping, a parametrização não tenta detectar e neutralizar
caracteres perigosos — ela remove a possibilidade de a gramática da query
mudar de todo, porque o driver envia o template da query e o dado pela rede
como duas coisas separadas. Não existe uma string paraUNION,--ou'
"escaparem" dela, porque o valor nunca é reinterpretado como sintaxe SQL.
Critério de submissão
Submeta quando tudo o que segue for verdade:
- Evidência documentada do exploit contra o DVWA "low" (Passo 2) e a
extração completa de dados viaUNION SELECT(Passo 3). - Evidência documentada de que o DVWA "medium" é contornado via
requisição forjada, mais sua explicação de por que o escaping não é
estrutural (Passo 4). - Uma função equivalente a
buscar_usuario_por_idreescrita usando query
parametrizada / prepared statement na linguagem de sua escolha. - Uma suíte de testes passando provando: o payload do Passo 3 retorna zero
linhas / nenhum erro contra a versão corrigida, e uma busca normal ainda
funciona corretamente.
Inclua uma nota de uma linha confirmando que você só testou contra seu
container DVWA local, nunca um sistema de terceiros. - Evidência documentada do exploit contra o DVWA "low" (Passo 2) e a