Security · 60 min

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:

Pré-requisitos

Para completar este lab você vai precisar de:

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

  1. 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-dvwa
    

    Abra http://localhost/setup.php no seu navegador e clique em
    "Create / Reset Database". Depois vá para http://localhost/login.php
    e faça login com as credenciais padrão:

    usuário: admin
    senha: password
    

    Checkpoint

    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ções low,
    medium, high, impossible. Deixe em low por enquanto — você vai
    mudar no Passo 4.

    Entregável deste passo: DVWA rodando em http://localhost, logado
    como admin, com o nível de segurança em low.

  2. 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: admin
    

    Agora 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'='1
    

    Se isso retornar todos os usuários em vez de só um, você confirmou que
    a cláusula WHERE está 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 do WHERE.

    Entregável deste passo: dois pares payload/resposta observados,
    provando que o campo User ID é concatenado sem proteção numa query SQL.

  3. Explore com UNION para extrair outros dados

    Uma injeção baseada em UNION acrescenta um segundo SELECT, 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 SELECT com 2 colunas, puxando de uma tabela
    completamente diferente:

    User ID: 1' UNION SELECT user, password FROM users-- -
    

    Isso deve imprimir toda linha de users.user e users.password nos
    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. Dois SELECTs 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 user e password de
    toda linha na tabela users, 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 SELECT funcionando e os
    dados user/password extraídos por ele, demonstrando exposição completa
    de uma tabela que o endpoint nunca deveria tocar.

  4. 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 por mysqli_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 um UNION SELECT puro) 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.

  5. 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 SQL
    

    A 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 SQL
    

    Traduza 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 para UNION, -- ou '
    "escaparem" dela, porque o valor nunca é reinterpretado como sintaxe SQL.


    Critério de submissão

    Submeta quando tudo o que segue for verdade:

    1. Evidência documentada do exploit contra o DVWA "low" (Passo 2) e a
      extração completa de dados via UNION SELECT (Passo 3).
    2. 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).
    3. Uma função equivalente a buscar_usuario_por_id reescrita usando query
      parametrizada / prepared statement na linguagem de sua escolha.
    4. 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.