Proforma
Proforma is a simple invoicing tool for freelancers: build an invoice, preview it, and download a PDF to send to your client.
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