Challenge description
Every Halloween, an enigmatic blog emerges from the depths of the dark web—Phantom's Script. Its pages are filled with cursed writings and hexed code that ensnare the souls of unwary visitors. The blog's malicious scripts weave dark secrets into the fabric of the internet, spreading corruption with each click. Rumor has it that interacting with the site in unexpected ways can trigger hidden incantations. Will you dare to delve into this haunted scroll, manipulate the scripts, and purge the malevolent code before it claims more victims?
Challenge Overview
This challenge presents a blog website where users can search for posts by title, description, or author. The goal is to exploit a Cross-Site Scripting (XSS) vulnerability in the search functionality to trigger a bot and retrieve the flag.

Code Review
index.js
The search functionality processes the user's input in the /search endpoint.
router.post("/search", (req, res) => {
const { query } = req.body;
if (query) {
try {
visit(query); // Pass the query to the bot for processing
return res.status(200).json({ message: "Bot triggered successfully." });
} catch (err) {
return res.status(500).json({ message: "Error triggering bot.", error: err.message });
}
} else {
return res.status(400).json({ message: "No search query provided." });
}
});- Key Functionality: The
visitfunction is called with thequeryparameter, which directly influences how the bot processes the request.
botHelper.js
The visit function is responsible for navigating the bot to the constructed URL. It uses Puppeteer to simulate a browser and processes alert dialogs.
const visit = async (query) => {
if (isBotRunning) {
console.log("Bot is already running. Skipping new request.");
return;
}
isBotRunning = true;
let page;
try {
page = await (await browser).newPage(); // Open a new page
console.log("New page opened successfully!");
// Navigate to the URL with the user query appended
await page.goto(`http://127.0.0.1:1337?q=${query}`, { waitUntil: "domcontentloaded" });
let alertHandled = false;
// Handle any alert dialogs triggered on the page
page.on("dialog", async (dialog) => {
await dialog.accept(); // Accept the alert
alertHandled = true;
if (io) {
io.emit("flag", { message: "Alert detected!", flag: flag.trim() }); // Emit the flag
}
await page.close(); // Close the page after handling the alert
isBotRunning = false;
});
// Timeout to ensure the page closes if no alert is detected
setTimeout(async () => {
if (!alertHandled) {
await page.close();
isBotRunning = false;
}
}, 500); // 500ms delay before cleanup
} catch (e) {
console.error(`Failed to navigate: ${e.message}`);
if (page) {
await page.close();
}
isBotRunning = false;
}
};Key Observations:
- Dynamic URL Construction: The
queryparameter is appended directly to the URL without sanitization, enabling malicious input to manipulate the bot's navigation. - Alert Handling: The bot listens for alert dialogs. If triggered, it emits the flag using a WebSocket event.
- Puppeteer Integration: The bot relies on Puppeteer's headless browser to process user-supplied input.
Exploitation
The vulnerable search functionality can be exploited by injecting a malicious payload into the query parameter. When the bot processes this payload, it triggers an alert dialog, which the bot interprets to extract and emit the flag.
<IMG SRC=x onerror="alert('xss')">Payload Breakdown
- The payload abuses the
onerrorattribute of the<IMG>tag to execute arbitrary JavaScript. - When the bot navigates to the URL containing the payload, the malformed image (
SRC=x) triggers theonerrorhandler, which executes thealertfunction.

With this technique, the same payload can be used on the HTB server to retrieve the real flag.

