BugForge — 2026.06.03

Hacker's Paradise: Full-Response SSRF to Internal Admin Service

BugForge Full-Response SSRF medium

Part 1: Pentest Report

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.

  1. POST /api/limewire/download takes a torrent_url and returns the fetched content inside a data field. The legitimate value is http://localhost:4000/torrent/download/<file>, an internal localhost address. 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.
  2. The admin route group /api/redpillconsole/* (stats, users, orders, guestbook) returns 403 to a bluepill account uniformly across every route, so direct access-control drift on the public admin routes was ruled out early.
  3. Authentication is JWT, HS256, payload {id, username, role, iat, exp}. The role claim is in the token, which raised JWT forgery as a fallback path that was not needed.
  4. The legitimate torrent_url value 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 redpill role 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


Part 2: Notes / Knowledge

Key Learnings

  • Map a capability’s full reach before chasing the first lead it surfaces. The moment a fetch, read, or injection capability is confirmed, the pull is to go deep on the first thing it touches. The cheaper, higher-yield move is breadth first: sweep the allowlist boundary, enumerate every reachable host, port, and path, then go depth-first on the best lead. Here, confirming the SSRF against localhost:4000 and immediately mining it missed localhost:4001 until a port sweep surfaced it, and 4001 was where the flag lived. The miss is capability-shaped, not SSRF-shaped: the same trap waits on SQLi (every injectable parameter before dumping one table), file read (the whole path space), and IDOR (all sibling routes and verbs).

  • Behind a path that answers 200 to everything, the discriminator is the body, and the winning guesses are file and flag names, not dictionary words. An internal service that returns 200 "You're so close" to every path defeats word-list fuzzing by status code, so paths have to be told apart by diffing response bodies, not statuses. Because frameworks register specific routes before wildcard catch-alls, a real file route (/admin/flag.txt) is reached before the decoy answers. Dictionary words found nothing; curated flag-file names (flag.txt, /app/flag.txt, /admin/flag.txt) won.


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

#ssrf #full-response-ssrf #internal-service #broken-access-control #cwe-918 #bugforge