This post details my favorite CTF challenge that I’ve written so far with arxenix for UIUCTF 2025.

Here’s the chal: can you read ~/flag.txt by inserting only a single line comment in the middle of a python script?

import os

user_input = input().replace("\r", "").replace("\n", "")

code = f'''
print("hi")
# {user_input}
print("bye")
'''

open("submission.py", "w").write(code)
os.system("python3 submission.py")

Stop here if you’d like to give it a try without spoilers. I like this challenge because it scores highly on my personal checklist for good CTF challenges:

Good CTF chals have clear success criteria

  1. The objective is specified in the description: print the contents of ~/flag.txt to stdout.
  2. No information is hidden; all source code was provided with a Dockerfile. Exploits can be developed and debugged locally.

Good CTF chals are simply stated

This chal has only a few lines of python. It takes seconds to grasp the objective and why it’s interesting.

Good CTF chals defy expectations

At a glance this challenge looks impossible – comments should be ignored in python, right?

Good CTF chals require outside-the-box thinking

Outside-the-box thinking is central to the hacker ethos:

A hacker … achieves goals and solves problems by non-standard means.

Wikipedia: Hacker

To solve this problem you have to peel back the abstraction layers one at a time once you realize that the layer is not enough to explain any exploitable behavior.

For example, at the python language spec level, this comment should be entirely ignored. It can’t be parsed as an encoding statement. We need to dig deeper.

One might think about the linux host that it’s running on – is there a chance it’s shebang related? A quick experiment dismisses this.

What if it’s an oddity in CPython? A review of CPython’s comment parsing source doesn’t reveal anything. But what about how CPython initially loads and runs submission.py? Is there any weird behavior? Any exploitable memory or logic bugs maybe?

At this point, you may learn that CPython can execute zip files:

$ echo 'print("Hello from zip!")' > __main__.py
$ zip foo.zip __main__.py
  adding: __main__.py (stored 0%)
$ python3 foo.zip
Hello from zip!

A natural question to ask is how does CPython know a file is a zip file? The extension?

$ mv foo.zip foo.py
$ python3 foo.py
Hello from zip!

So the .py extension is not relevant to how CPython interprets the file! A quick test shows that inserting the bytes of a zip file as the “comment” is enough to cause CPython to locate and execute that zip file.

Does the challenge allow us to insert the bytes we need for the zip file? Yes, input() will accept a wide range of characters, including null characters. The only conditions we need to work around are:

  1. There can be no \n nor \r in our input, since those are stripped.
  2. Because open(...) did not specify binary mode “wb”, it defaulted to text mode, meaning the input must parse as utf-8, else it will raise.

One can manually construct a zip file from the binary spec to avoid these characters.

Submitting will give you the flag!

LLMs can solve CTF chals now

Claude Code could one shot my other two challenges, but it couldn’t solve this one itself. One of my other chals embedded a SAT problem in OCaml’s type system (my writeup) (Nikhil Chapre’s writeup). Another was an OCaml jail (Nikhil Chapre’s writeup). In fact, all of our crypto chals were one shot by LLMs. The number of solves at the end of the competition on this python chal revealed that LLMs were unable to solve it themselves.

We’re at a place now where mechanical CTF chals are easily solved by LLMs, but chals that require creative insight and outside-the-box thinking still have LLMs struggling. To prevent CTFs from being a race of the LLMs, let’s keep inventing great CTF chals!

Thanks for reading!