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.
Setup
Open DevTools (F12) > Elements and search the HTML/JS for banned or blacklist to see the exact filter logic.
The blocklist sits in plain sight: substrings like os, eval, exec, import, ls, cat, /, flag, sh, system are rejected client-side, then sent to a server eval.
Probe the filter: send os and watch it get rejected, then send 'o'+'s' and watch it pass. That confirms static substring matching with runtime concatenation as the bypass.
Before reading /flag.txt, list the root with the obfuscated ls / to confirm the file actually lives there.
__import__('o'+'s').popen('l'+'s '+chr(47)).read()__import__('o'+'s').popen('c'+'at '+chr(47)+'fl'+'ag.txt').read()Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Bypass the filter
ObservationThe calculator runs a keyword blocklist over input before handing it to Python's eval. That is static substring matching, so build the banned tokens at runtime with concatenation and chr() rather than typing them.Open DevTools and grep the JS forbannedto see the exact rejected substrings. Probe withos(rejected) vs'o'+'s'(accepted) to confirm static matching. Then build forbidden tokens at runtime:'o'+'s'dodges the os check,chr(47)gives/without a literal slash, and__import__dodges the import keyword block.What didn't work first
Tried: Trying to find the filter logic by looking at the page source with View Source.
View Source shows the HTML as sent, but filter logic is often injected or modified by JavaScript after load. The Elements panel reflects the live DOM, dynamically added scripts included, so search there or in Sources.
Tried: Attempting standard eval bypass payloads like os.system('cat /flag.txt') without checking which substrings are blocked.
Guessing which tokens are banned burns time and can get your session rate-limited. Read the blocklist and you know exactly which patterns need a substitution, instead of testing payloads one at a time.
Learn more
Python's eval executes any arbitrary Python expression passed to it as a string. When web applications expose
evalto 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
osbut never sees it:'o'+'s'produces the same string only after Python evaluates the expression. Thechr()built-in similarly converts an integer to a character, sochr(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
importstatement. Because it accepts a plain string argument, it can import any module dynamically - includingos- even when theimportkeyword itself is blocked. Onceosis imported,os.popenopens 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. See the Command Injection guide for filter-bypass patterns that map cleanly to shell injection too, and the Burp Suite for picoCTF guide for the Repeater loop that lets you iterate through these bypass payloads without retyping the request every send.Step 2Dump the filesystem
ObservationThe filter blocks 'ls', 'cat', '/', and 'flag' as literal substrings. The same runtime string building from the previous step covers all four, so use it to list the root directory and then read the flag.First list/with__import__('o'+'s').popen('l'+'s '+chr(47)).read()to confirm flag.txt is there rather than guessing the path. The trailing chr(47) matters: without an argument the command lists the app's working directory, and a literal slash is on the blocklist. Then a cat payload like__import__('o'+'s').popen('c'+'at '+chr(47)+'fl'+'ag.txt').read()exfiltrates the contents through the calculator's response field.What didn't work first
Tried: Jumping straight to reading /flag.txt without listing the directory first.
If the flag lives under a subdirectory instead, the read returns an empty string or an error the calculator may not display clearly. One extra payload listing the root removes the guesswork.
Tried: Trying to split 'cat' as 'c'+'at' but leaving the path as a literal /flag.txt string in the same payload.
The blocklist matches each substring independently, so splitting 'cat' still leaves a literal slash and the word 'flag' in the input, both blocked. Every blocked substring needs its own bypass: chr(47) for the slash, a split for '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.txtor/root/flag.txton Linux containers.os.popenlaunches 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/passwdto 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.
Interactive tools
- Reverse Shell GeneratorGenerate reverse shell payloads (bash, nc, python, perl, ruby, php, node, powershell) and matching listeners. Set host and port once, copy any variant.
Flag
Reveal 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.