Proforma
mediumPro

Proforma

Proforma is a simple invoicing tool for freelancers: build an invoice, preview it, and download a PDF to send to your client.

sstijinja2rceprivilege-escalationcron
pythonflasknginx

The Scenario

Proforma keeps freelancers' invoicing tidy: fill in the client details, preview the

invoice, and hit Download PDF to send it off. The web preview is careful about what it

shows, so the obvious tricks in the bill-to field go nowhere.

Come at Proforma as a user building an invoice, and pay attention to the difference

between what you see in the browser and what comes out of the PDF.

Lab Intel

Synopsis

The bill-to field is HTML-escaped in the web preview (render_template with a variable), so reflected XSS looks impossible. The Download-PDF path (POST /invoice.pdf) builds a separate template by CONCATENATING bill_to into the template source and calls render_template_string on it, so a Jinja payload like {{cycler.__init__.__globals__.os.popen('id').read()}} is SSTI only when rendering the PDF -> RCE as the app user 'pforma'. Post-shell, /opt/invoicing/reports.sh is world-writable and is run every ~20s as the 'billing' user by a supervisord scheduler; overwriting it with a command runs as billing, which can read its own /home/billing/flag.txt (0600) and copy it out.

Architecture

A medium lab on two containers (nginx gateway + Flask/gunicorn under supervisord). Two render paths treat the bill-to field differently: the web preview escapes it (variable into an autoescaped template), the PDF export concatenates it into the template source and render_template_string's it (SSTI). The SSTI yields RCE as pforma. The flag belongs to a second account, billing (0600), so you escalate: overwrite the world-writable /opt/invoicing/reports.sh, which the billing scheduler executes, to copy billing's flag somewhere readable. The flag is read as the non-root billing user.

Who It's For

Players who know SSTI and want the full picture: a realistic safe-vs-unsafe render split (web vs PDF), and a post-exploitation hop via a writable scheduled job to a second non-root user, rather than stopping at the first shell.

Skills You'll Practice

  • Spotting that two render paths treat the same input differently (escaped vs raw)
  • Landing Jinja2 SSTI via a concatenated template source (render_template_string)
  • Using the cycler->os gadget for RCE
  • Escalating through a writable, scheduled script that runs as another user

What You'll Gain

  • Test export/print/PDF paths separately from the web view
  • The cycler SSTI RCE gadget and why render_template_string on user input is fatal
  • A writable-cron / scheduled-job privilege-escalation pattern

Ready to hack Proforma?

Upgrade to Pro to unlock this lab and the full library.

Proforma | WebVerse Pro Lab