3v@l

Published: April 2, 2025

Description

ABC Bank's loan calculator naively feeds user input to Python's eval while blocking a short keyword list. Build around the filter to execute shell commands and read /flag.txt.

Load the calculator page and inspect the script block; keywords like os, eval, ls, cat, /, etc. are blacklisted.

Craft Python expressions with string concatenation and chr() so the filter fails to spot forbidden tokens.

__import__('o'+'s').popen('l'+'s').read()
__import__('o'+'s').popen('c'+'at '+chr(47)+'*').read()

Solution

  1. Step 1Bypass the filter
    Instead of typing os directly, build it dynamically (`'o'+'s'`) and likewise for commands. chr(47) gives `/`, enabling path traversal without literal slashes.
    Learn more

    Python's eval executes any arbitrary Python expression passed to it as a string. When web applications expose eval to user input - even with a blocklist - they create a code injection vulnerability. Blocklists that operate on raw string matching are fundamentally weak because Python offers many ways to construct the same string at runtime.

    String concatenation bypass works because the filter scans for the literal token os but never sees it: 'o'+'s' produces the same string only after Python evaluates the expression. The chr() built-in similarly converts an integer to a character, so chr(47) yields / without ever writing a slash in the input. These techniques exploit the fact that static string matching cannot track runtime values.

    __import__ is the lower-level function that backs Python's import statement. Because it accepts a plain string argument, it can import any module dynamically - including os - even when the import keyword itself is blocked. Once os is imported, os.popen opens a subprocess whose output is readable as a file object.

    The real-world lesson here is that blocklists are not a safe defense for code injection. Proper remediation means never passing user input to eval, exec, or similar functions. If dynamic evaluation is genuinely required, use an allowlist restricted to the exact operations the feature needs.

  2. Step 2Dump the filesystem
    List the root directory (`ls /`) to confirm flag.txt, then run a cat payload (e.g., `__import__('o'+'s').popen('c'+'at '+chr(47)+'flag.txt').read()`) to output the flag.
    Learn more

    Once arbitrary command execution is established, an attacker follows a standard enumeration pattern: first list directories to understand the filesystem layout, then read target files. Flags in CTF challenges are conventionally placed at /flag.txt or /root/flag.txt on Linux containers.

    os.popen launches a shell command and returns a file-like object. Calling .read() on it captures all stdout output as a Python string, which then gets returned to the web application and displayed in the response - completing the exfiltration loop without any separate network channel.

    In real penetration testing this step is called post-exploitation enumeration. Attackers typically run whoami, id, uname -a, and then read /etc/passwd to understand privilege level and available users. The same obfuscation techniques used in this challenge apply to those commands as well, demonstrating how a single bypass technique enables full system access.

Flag

picoCTF{D0nt_Use_Unsecure_f@nctionsd06...}

Any payload that spawns /bin/sh via the obfuscated os import works; the concatenation trick keeps the blacklist asleep.

Want more picoCTF 2025 writeups?

Useful tools for Web Exploitation

Related reading

What to try next