sn4k3 archive

write-ups de CTF

ORDER ORDER

picoCTF 2026 · 22 set 2026 · hard · web exploitation
ctf writeup sqli second-order web

Classe: Second-Order SQL Injection. O dado controlado pelo usuário é gravado no banco e só é interpretado como parte de uma consulta SQL depois, em outro ponto da aplicação.

O problema

Ao acessar http://crystal-peak.picoctf.net:58279/, observamos uma aplicação com a função de controlar despesas.

Tela inicial da aplicação de controle de despesas

Criamos uma conta comum para explorar e entender melhor como a aplicação funciona.

Cadastro de uma conta comum
Username: test
Password: test

Notei que existem algumas rotas interessantes, como /dashboard, /expenses e /inbox.

Rotas /dashboard, /expenses e /inbox

Ao acessar /expenses, podemos criar uma despesa e, em seguida, gerar um relatório dela.

Criação de uma despesa em /expenses

Em /inbox, podemos visualizar o relatório e também fazer o download dele.

Relatório listado em /inbox Download do relatório gerado

A partir daqui, vou utilizar o Burp Suite para facilitar a análise das requisições e respostas.

Inicialmente, suspeitei de XSS, então criei um usuário utilizando o nome:

<script>alert(1)</script>
Cadastro de usuário com payload de XSS no nome

Porém, não funcionou.

Payload de XSS não executado

Também testei uma aspa simples (') para tentar quebrar uma possível consulta SQL e obter um error-based, mas também não tive resultado.

Teste com aspa simples no nome de usuário Resposta ao teste com aspa simples

O próximo passo foi testar a criação do usuário. Dessa vez, criei um usuário utilizando apenas: (')

Cadastro de usuário com o nome (')

Ao acessar o /inbox, recebemos um erro.

Erro do banco exibido ao acessar /inbox

De cara, isso chamou bastante atenção. O que temos aqui é uma Second-Order SQL Injection (SQLi de segunda ordem).

Esse tipo de vulnerabilidade acontece quando o sistema armazena o tainted data fornecido pelo usuário no banco de dados e só executa esse conteúdo posteriormente, em outro ponto da aplicação.

Nesse caso, o cadastro do usuário funciona como o injection point/source, enquanto o /inbox acaba funcionando como o sink/execution point, onde o dado armazenado é utilizado em uma consulta SQL.

Sendo assim, agora ficou mais fácil entender o que estava acontecendo e testar o comportamento da consulta.

'ORDER BY 1-- -
'ORDER BY 2-- -
'ORDER BY 3-- -
'ORDER BY 4-- -
Teste ORDER BY 1 Teste ORDER BY 2 Teste ORDER BY 3 Teste ORDER BY 4

Depois dos testes, descobri que a consulta possuía 3 colunas, então podemos utilizar NULL padding e continuar a enumeração.

'UNION SELECT name,type,sql FROM sqlite_master-- -
Enumeração do schema com UNION SELECT em sqlite_master

A partir disso, conseguimos enumerar informações do banco e identificar as tabelas existentes.

Listagem das tabelas do banco

Na primeira tabela que consultei, já encontrei o retorno da flag.

'UNION SELECT name,value,null FROM aDNyM19uMF9mMTRn-- -
Retorno da flag na tabela aDNyM19uMF9mMTRn Flag capturada

A solução

A vulnerabilidade acontece porque a aplicação armazena diretamente a entrada fornecida pelo usuário e posteriormente utiliza esse valor na construção de uma consulta SQL. O problema não está apenas no cadastro em si, mas no fato de o dado contaminado continuar armazenado e ser interpretado como parte da consulta quando o /inbox é acessado. Por isso, trata-se de uma Second-Order SQL Injection, e não apenas de uma SQLi convencional.

Do lado defensivo, a principal correção seria utilizar parameterized queries / prepared statements em todas as consultas que utilizam dados controlados pelo usuário, evitando string concatenation para montar SQL dinamicamente. Também seria importante aplicar validação de entrada, utilizar uma conta de banco com o menor privilégio possível e evitar retornar erros detalhados do banco para o usuário, já que nesse caso o erro acabou funcionando como um error-based oracle e facilitou a exploração.

Flag:

picoCTF{s3c0nd_0rd3r_1t_1s_3ad6ac82}