A HawkSec resolveu entrar no ramo do varejo e abriu a Lojinha da HawkSec, cheia de swags exclusivos pra quem curte um estilo hacker. De brinde, jogamos a flag desse desafio no catálogo. Mas não se engane: não é só clicar e comprar. A loja tem seus truques, e só os mais espertos vão conseguir colocar a flag no carrinho. Seu dinheiro chega lá? Ou vai precisar de algo a mais pra levar esse item pra casa?
O desafio foi inspirado no Shopping Time do TUCTF24. Temos writeup aqui
Ao acessar o site, vemos que é uma loja online da HawkSec, onde podemos buscar por produtos.
Página principal
Entretanto, temos o produto "Flag do Desafio", que ao ser acessado, recebemos o erro de Termo Proibido. O site acessa a rota /review?item=Flag do Desafio.
Produto Flag do Desafio
Termo Proibido
Analisando o código provido no desafio, temos o arquivo server.js:
A rota /review é responsável por mostrar os detalhes do produto. Ela utiliza o md5 do nome do item como id para buscar no banco de dados. O md5 é cortado para os 6 primeiros caracteres, e esse valor é utilizado para buscar o item no banco de dados.
Caso o item seja encontrado, a rota retorna os detalhes do produto, como nome, preço, descrição e imagem.
app.get('/review', async (req, res) => {
const item = req.query.item?.toString().trim();
if (!item) {
const items = await dbAll('SELECT name, image FROM items');
return res.render('index', { items, error: 'Nenhum item fornecido.' });
}
if (BLOCKLIST.includes(item)) {
const items = await dbAll('SELECT name, image FROM items');
return res.render('index', { items, error: 'Termo proibido!' });
}
try {
const row = await dbGet('SELECT * FROM items WHERE id = ?', [md5(item).slice(0, 6)]);
if (!row) {
const items = await dbAll('SELECT name, image FROM items');
return res.render('index', { items, error: 'Item não encontrado!' });
}
res.render('review', {
name: row.name,
price: row.price,
description: row.description,
image: row.image,
});
} catch (err) {
return handleDbError(res, err, 'Erro ao consultar o banco de dados.');
}
});
O md5 é uma função de hash, que transforma uma string em um valor fixo de 32 caracteres. Porém o servidor só utiliza os 6 primeiros caracteres do hash. Isso significa que existem várias strings diferentes que podem gerar o mesmo hash. Esse tipo de ataque é conhecido como collision attack.
Então por mais que o item "Flag do Desafio" esteja na BLOCKLIST, podemos ter uma string que gere os mesmos 6 primeiros caracteres do hash. Para isso, podemos utilizar o código:
import hashlib
def find_collision(target):
target_hash = hashlib.md5(target.encode()).hexdigest()
target_prefix = target_hash[:6]
print(f"Prefixo alvo: {target_prefix}")
i = 0
while True:
candidate = f"hawk{i}"
hash_value = hashlib.md5(candidate.encode()).hexdigest()
if hash_value.startswith(target_prefix):
print(f"Colisão Encontrada: Entrada = {candidate}, Hash = {hash_value}")
return
i += 1
find_collision("Flag do Desafio")
Você não precisa fazer a string começar com hawk e nem ser incremental. Basta apenas gerar uma string que tenha o mesmo hash. O código acima é apenas um exemplo de como fazer isso.
Com isso, conseguimos acessar a rota /review?item=hawk90435475, e conseguimos ver os detalhes do produto, onde a flag está localizada.