Bypassing Python eval() Filters: ASCII Encoding Attack in picoCTF 2025

Challenge Info:

  • Platform: picoCTF 2025 - 3v@l
  • Category: Web Exploitation
  • Difficulty: Medium
  • Vulnerability: Command Injection / Insecure eval() Implementation

Understanding the Challenge

So ABC Bank has this "Loan Calculator" web app. Looks innocent enough - you type in math like 500 * 12 and it spits out the answer.

But here's the thing: under the hood, they're using Python's eval() function. And if you know anything about eval(), you know that's a massive red flag. This function doesn't just do math - it runs any Python code you throw at it.

Our goal: Get this calculator to read a secret file called flag.txt sitting on the server.


Why eval() is Dangerous

Full System Access

When people say "eval() runs code," they don't usually explain just how bad that is. Here's what actually happens:

  • Full Access: Your code runs in the same space as the web app. Everything the app can see, you can see. Every module it imported, you can use.
  • Same Privileges: Your code has the exact same permissions as the server. Can read files? You can too. Can delete stuff? Yep, you can do that.
  • Total Control: It's not a calculator anymore - it's a direct line to the server's operating system.

The Blacklist Filter

The devs knew eval() was risky, so they added a filter. Think of it like a bouncer checking what you're trying to send in.

The filter blocks:

  1. Quotes (' or "): Without quotes, you supposedly can't make strings like "flag.txt" or "os".
  2. Keywords: Things like import, os, system, and flag are straight-up banned.

Classic blacklist approach - block everything bad. Problem is, you can't list every possible way to say something.

The ASCII Bypass Technique

Computers don't read letters - they read numbers. Every character has an ASCII value.

  • 'a' is number 97
  • 'b' is number 98

Python has chr() that turns numbers into letters:

  • chr(97) gives you 'a'

Why the filter can't block chr(): Because it's a totally normal function! People use it for legitimate stuff. Blocking it or blocking numbers would break the calculator completely.

The trick: Instead of typing 'flag' (blocked), we type chr(102) + chr(108) + chr(97) + chr(103). The filter sees math and functions (allowed), but when eval() actually runs it, boom - it becomes 'flag'.


Reconnaissance: Testing the Environment

Let's figure out what we're working with.

Test 1: Confirming Python

Try len('abc') - asking Python for the length of 'abc'.

  • Result: Returns 3
  • Conclusion: Yep, definitely Python.

Test 2: Testing the Filter

Try the classic: __import__('os').system('ls')

  • Result: "Forbidden" error
  • Conclusion: Filter's active. Gotta be sneaky.

Test 3: Validating chr()

chr(65)  # Should give us 'A'

Result: Success! Got A back.

Test 4: Testing Concatenation

chr(102)+chr(108)  # Should give us 'fl'

Result: Works! Got fl.


Planning the Exploit

To read flag.txt, we need Python code like:

open('/flag.txt').read()

Translation: "Open that file and give me what's inside."

Problem: Can't type /flag.txt - has quotes and the word "flag".

Solution: Build it character by character with chr().

ASCII Mapping

  • / is 47
  • f is 102
  • l is 108
  • a is 97
  • g is 103
  • . is 46
  • t is 116
  • x is 120
  • t is 116


Building the Payload

Time to put it all together.

Step 1: Construct the Filename

Chain all those numbers with +:

# This spells out '/flag.txt' without using quotes!
chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)

Step 2: Embed in File Operation

open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)).read()

Looks wild to us, but to the computer it's totally valid. And the filter? It just sees functions and numbers - nothing forbidden.


Executing the Attack

Copy that payload into the calculator and hit "Calculate".

Server-Side Execution Flow

  1. Filter checks our input. Sees open, chr, +, and numbers. Nothing's blacklisted. Green light.
  2. eval() runs our code.
  3. All those chr(..) calls turn into letters: /flag.txt
  4. Executes open('/flag.txt').read()
  5. Reads the secret file
  6. Displays it like it's just another math answer

Flag: picoCTF{D0nt_Use_Unsecure_f@nctions463f23d8}


Key Takeaways

This whole challenge shows why blacklists are a bad idea.

The devs tried to lock down eval() by blocking specific words. But code is flexible - there's almost always another way to say the same thing (like using ASCII numbers instead of letters).

The Real Solution

The real fix? Ditch eval() completely. Use a proper math parser that only understands numbers and operators, not arbitrary code.

Security Lessons

  • Blacklist filters can always be bypassed
  • eval() should never accept user input
  • Use whitelisting and proper input validation
  • Consider safe alternatives like ast.literal_eval() for simple use cases