WEBVERSE

Loading...

hardSSTIPro

Quoted

An indie reading-highlights service for power-readers. Users can customise the layout of their weekly digest email through a small sandboxed templating language — the kind of feature a careful developer adds, then tries to lock down.

The Scenario

Quoted is a four-person team out of Edinburgh who built the tool they

wished existed: a calm, opinionated home for the sentences you underline

on your Kindle, save in Pocket, or pull out of an RSS feed. The weekly

digest is the main product loop — Sunday morning, a single email with

six highlights you'd otherwise forget. Power-users kept asking to

rearrange the layout, so Cate (their backend lead) shipped a small

templating surface in /settings/digest-template last quarter and went

to some lengths to lock it down before launch. She read the Jinja2

sandbox docs, denied the obvious class-walk attributes, and called it

done. The QA tasks for that PR were closed in an hour.

Challenge Intel

Synopsis

The digest-template editor uses Jinja2's SandboxedEnvironment with an overridden is_safe_attribute that bans the usual class-walk dunders (__class__, __bases__, __subclasses__, __mro__, mro). The block is real — the canonical {{ ''.__class__.__mro__[1].__subclasses__() }} payload returns a SecurityError. But the override does not block __globals__, and Jinja2 ships several callable globals (lipsum, cycler, joiner, namespace) whose function/class objects expose __globals__ directly. From there it's a one-hop to os.popen.

What It Is

Logged-in users have a /settings/digest-template page with a textarea and a "Preview" submit. The submitted template is rendered through a SandboxedEnvironment subclass whose is_safe_attribute returns False for the five class-walk dunders ('__class__', '__bases__', '__subclasses__', '__mro__', 'mro') and True for everything else — intentionally relaxing Jinja2's default UNSAFE_FUNCTION_ATTRIBUTES set that would normally also block __globals__. The player must recognise that the blocklist is incomplete and reach a Python object through a non-class-walk path. The cleanest payload: {{ lipsum.__globals__['os'].popen('cat /flag.txt').read() }} `lipsum` is a Jinja2 utility function; `lipsum.__globals__` is the `jinja2.utils` module dict; `['os']` is the imported `os` module; `popen('cat /flag.txt').read()` returns the flag. The rendered preview is reflected back in the page, so this is in-band exfil. Equivalent payloads via `cycler.__init__.__globals__.os.popen(...)` or `joiner.__init__.__globals__.os.popen(...)` work too — any Jinja2 callable global with `__globals__` reachable via its function object will do.

Who It's For

Players who have hit the easy unsandboxed Jinja2 SSTI case (where `{{ ''.__class__.__mro__[1].__subclasses__() }}` walks straight to `os.popen`) and want to learn what to do when the sandbox actually has a blocklist. Prereq: comfort with Python attribute traversal and an awareness that `__class__` is not the only path to executable Python objects.

Skills You'll Practice

  • Reading a sandbox's allow/block-list and finding the gap
  • Reaching Python module objects through callable globals (`lipsum`, `cycler`, `joiner`, `namespace`)
  • Using `__globals__` to access imported modules without a class walk
  • Invoking `os.popen` from a function-object's globals dict

What You'll Gain

  • Mental model: sandbox blocklists fail by omission, not by being weak
  • A second SSTI primitive (the `lipsum.__globals__` family) for when the class walk is blocked
  • Reading Jinja2's sandbox source to know what's actually filtered vs documented

Ready to hack Quoted?

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