Hacker's Paradise: Full-Response SSRF to Internal Admin Service
Executive Summary
Hacker’s Paradise is a 90s/2000s warez-nostalgia web application themed around The Matrix, backed by a Node/Express JSON API. The objective is the redpill role and the admin console at /api/redpillconsole/*, which holds the flag. The public admin route group is correctly gated: every /api/redpillconsole/* request from an unprivileged bluepill account returns 403. That gate is irrelevant. The application exposes a LimeWire “download” feature that fetches an attacker-supplied URL server-side and returns the upstream response body verbatim. The fetch reaches an internal admin service on localhost:4001 that has no authentication of its own, and the flag is served from http://localhost:4001/admin/flag.txt.
Testing confirmed 1 finding:
| ID | Title | Severity | CVSS | CWE | Endpoint |
|---|---|---|---|---|---|
| F1 | Full-response SSRF reaches internal admin service | High | 7.7 | CWE-918, CWE-441 | POST /api/limewire/download |
The headline finding is a server-side request forgery that returns the fetched body to the caller. Any registered user can read internal-only services that the public application’s role gate was meant to protect. The role gate on the public admin routes is never defeated and never needs to be: the SSRF talks to the backend directly.
Objective
Capture the flag on the Hacker’s Paradise lab. The flag lives behind the redpill admin role; the engagement goal was to reach it from an unprivileged bluepill starting position.
Scope / Initial Access
# Target Application
URL: https://lab-1780004722867-yjv70t.labs-app.bugforge.io
# Auth details
POST /api/auth/register {username,password} -> {success, token, user:{id,username,role:"bluepill"}}
POST /api/auth/login {username,password} -> {success, token}
GET /api/auth/me Bearer -> {authenticated, user:{id,username,role,iat,exp}}
Authorization: Bearer <jwt> # HS256, payload {id, username, role, iat, exp}
# A freshly registered account is issued role="bluepill" (unprivileged).
Registration is open. A new account receives a signed HS256 JWT with role:"bluepill". The privileged role is redpill. The static page /redpillconsole.html is served 200 to anyone, including bluepill accounts, so the real access control is on the API, not the page.
Reconnaissance: Mapping the API and the Internal Hint
The application surface was mapped from the per-page JavaScript bundles and the API responses. The LimeWire download feature and the admin route group shaped the entire test plan.
POST /api/limewire/downloadtakes atorrent_urland returns the fetched content inside adatafield. The legitimate value ishttp://localhost:4000/torrent/download/<file>, an internallocalhostaddress. A fetch feature whose normal input is an internal URL, and that hands the response body back to the caller, is a read into internal services.- The admin route group
/api/redpillconsole/*(stats, users, orders, guestbook) returns403to abluepillaccount uniformly across every route, so direct access-control drift on the public admin routes was ruled out early. - Authentication is JWT, HS256, payload
{id, username, role, iat, exp}. Theroleclaim is in the token, which raised JWT forgery as a fallback path that was not needed. - The legitimate
torrent_urlvalue disclosed an internal host and port (localhost:4000). That hint is the lead the SSRF was built on.
Application Architecture
| Component | Detail |
|---|---|
| Backend | Node / Express, JSON API under /api/* |
| Frontend | Static HTML pages with per-page JS bundles (/js/*.js) |
| Auth | JWT (HS256) in localStorage.token, sent as Authorization: Bearer |
| Internal services | localhost:4000 (torrent service), localhost:4001 (admin service) |
API Surface
| Endpoint | Method | Auth | Notes |
|---|---|---|---|
/api/auth/register |
POST | No | Self-service registration, returns bluepill JWT |
/api/auth/login |
POST | No | Returns JWT |
/api/auth/me |
GET | Bearer | Returns decoded user from token |
/api/limewire/download |
POST | Bearer | Fetches torrent_url, returns body in data (F1) |
/api/limewire/files |
GET | Bearer | Search by ?q= |
/api/guestbook |
POST | Bearer | Stored guestbook entry |
/api/redpillconsole/* |
GET/DELETE | Bearer + redpill |
Admin console, returns 403 to bluepill |
Roles
| Role | Privilege |
|---|---|
bluepill |
Default on registration, unprivileged |
redpill |
Privileged, required for /api/redpillconsole/* |
Attack Chain Visualization
┌───────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ Register account │ │ POST /api/limewire/ │ │ Allowlist permits │ │ GET (via fetcher) │
│ bluepill JWT │ ──▶ │ download torrent_url= │ ──▶ │ localhost:4000 + │ ──▶ │ localhost:4001/ │
│ (any user) │ │ internal URL; body │ │ localhost:4001 │ │ admin/flag.txt │
│ │ │ returned in `data` │ │ (4001 = admin svc) │ │ -> flag in body │
└───────────────────┘ └───────────────────────┘ └──────────────────────┘ └─────────────────────┘
Findings
F1: Full-Response SSRF Reaches Internal Admin Service
Severity: High
CVSS v3.1: 7.7 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
CWE: CWE-918 (Server-Side Request Forgery), CWE-441 (Unintended Proxy or Intermediary)
Endpoint: POST /api/limewire/download
Authentication required: Yes (any registered bluepill account)
Description
The LimeWire download feature fetches the URL supplied in torrent_url server-side and returns the upstream response body to the caller in the data field. The host and port are restricted by an allowlist that accepts the literal strings localhost:4000 and localhost:4001 (127.0.0.1 and other hosts are rejected with “Invalid service URL”), but any path on those two hosts is attacker-controlled and the body is reflected back unchanged.
The internal service on localhost:4001 has no authentication of its own. The public application enforces a redpill role gate on /api/redpillconsole/* (a bluepill account receives 403), but that gate protects only the public routes. The SSRF reaches the internal service directly, where the flag is served from /admin/flag.txt.
Impact
Any registered user can read internal-only services, including the admin service that holds the flag and order records containing calling-card numbers.
Reproduction
Step 1: Register an unprivileged account
POST /api/auth/register HTTP/1.1
Host: lab-1780004722867-yjv70t.labs-app.bugforge.io
Content-Type: application/json
{"username":"recon01","password":"Passw0rd!"}
Response: {"success":true,"token":"<jwt>","user":{"id":...,"username":"recon01","role":"bluepill"}}. The account is bluepill.
Step 2: Confirm the fetch reflects the upstream body
POST /api/limewire/download HTTP/1.1
Host: lab-1780004722867-yjv70t.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json
{"torrent_url":"http://localhost:4000/"}
Response contains the upstream body inside data. The server fetched an internal URL and returned its content to us.
Step 3: Map the allowlist boundary
A sweep of hosts and ports shows the allowlist accepts only localhost:4000 and localhost:4001. 127.0.0.1, other ports, and external hosts return Invalid service URL. The service on localhost:4001 is an admin service distinct from the 4000 torrent service.
Step 4: Read the flag from the internal admin service
POST /api/limewire/download HTTP/1.1
Host: lab-1780004722867-yjv70t.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json
{"torrent_url":"http://localhost:4001/admin/flag.txt"}
Response: the data field carries {"flag":"bug{iFAQhChtqvSNhpWICVvYZ4HBewayueVp}"}.
Remediation
Fix 1: Do not return the upstream response body to the caller
// BEFORE (Vulnerable): the fetched body is reflected back verbatim
const upstream = await fetch(torrent_url);
const data = await upstream.text();
return res.json({ success: true, data });
// AFTER (Secure): act on the response server-side; never reflect it
const upstream = await fetch(torrent_url);
if (!upstream.ok) return res.status(502).json({ error: "fetch failed" });
await persistTorrentMetadata(await upstream.json());
return res.json({ success: true }); // no upstream body in the response
Fix 2: Restrict the fetch target to an explicit, validated resource, not a free-form URL
// BEFORE (Vulnerable): caller supplies an arbitrary URL,
// host:port allowlist is the only check
const { torrent_url } = req.body;
// AFTER (Secure): caller supplies an opaque identifier; the server
// builds the URL from a fixed base it controls
const { torrent_id } = req.body;
if (!/^[a-zA-Z0-9_-]+$/.test(torrent_id)) return res.status(400).end();
const url = `http://torrent-service.internal/torrent/download/${torrent_id}`;
Additional recommendations:
- Require authentication on the internal services (
localhost:4000,localhost:4001); do not assume the network boundary is the only caller. - Enforce the allowlist on the resolved address, not the literal host string, to defeat DNS-rebinding and redirect bypasses.
- Disable redirect following on the server-side fetch, or re-validate every redirect hop against the allowlist.
OWASP Top 10 Coverage
- A10:2021 Server-Side Request Forgery (SSRF): The download feature fetches an attacker-supplied URL and returns the body, reaching internal services that are not meant to be client-accessible.
- A01:2021 Broken Access Control: The
redpillrole gate on the public admin routes is bypassed in effect: the internal admin service it protects is reached directly through the SSRF, which has no equivalent gate. - A04:2021 Insecure Design: A “fetch a URL” feature that reflects the upstream body, behind an internal network that trusts itself, is exploitable by design regardless of the host allowlist.
Tools Used
| Tool | Purpose |
|---|---|
| Browser DevTools | Reading per-page JS bundles and API responses |
| HTTP client (curl / Burp) | Issuing the download requests and the port sweep |
References
- CWE-918: Server-Side Request Forgery (SSRF)
- CWE-441: Unintended Proxy or Intermediary
- OWASP A10:2021 Server-Side Request Forgery
- OWASP SSRF Prevention Cheat Sheet
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
Direct access on public /api/redpillconsole/* GET routes |
Uniform 403 to bluepill |
Role gate is enforced consistently across the route group |
Word-list fuzzing of localhost:4001/admin/* paths |
200 “You’re so close” on every path |
Deliberate catch-all anti-fuzz response; only curated file/flag names hit a real route |
localhost:4000/redpillconsole/* and /rabbithole enumeration |
Decoy taunt trees (“How deep does the rabbit hole go?”) | Pure distraction; no flag-bearing route on 4000 |
SSRF to 127.0.0.1, other ports, cloud metadata |
Invalid service URL |
Allowlist accepts only the literal strings localhost:4000 / localhost:4001 |
Mass-assignment role:"redpill" on register; JWT alg=none / weak-secret forge |
Not exercised | Lab solved via SSRF; these remain unconfirmed secondary candidates |
SQLi on /api/limewire/files?q=; change-password IDOR; guestbook author spoofing |
Not exercised | Parked once the SSRF reached the flag |
Tags: #ssrf #full-response-ssrf #internal-service #broken-access-control #bugforge
Document Version: 1.0
Last Updated: 2026-06-03