Unintended CPython101

From May 5th to 7th the second edition of the PWNME CTF was held**. Our team participated in the student category.

Scoreboard

For this write-up, I will explain how I was able to bypass the intended way to flag the Pwn CPython101 challenge.

CPython101

What if you could do memory corruption in Python ? Well this is an opportunity for you to discover that !  
Find a way to read the flag on the remote service.  
**Note**:  
- *This challenge is not a pyjail, mesures have been taken to block unintented way. If you find a bypass to the challenge please report to the challenge maker :)*  
- *You must spawn an instance for this challenge. You can connect to it with netcat: nc 51.254.39.184 1338*  
Author: Express#8049

Analysis

The challenge gave us an archive that contains a Dockerfile and the vulnerable pwnme.so library, which is a Python module written in C. There is also a wrapper that takes our Python code and places it into a file that will be executed later.

Wrapper

The LD_PRELOAD environment variable is set to hook.so library. In short, all functions present in this library are used instead of the ones in the dynamically linked library. This also affects child processes.

LD_PRELOAD

After the first analysis, we can see that the challenge seems not too easy. The number of solves tells us that it will be an advanced exploitation. But! We see that maybe some teams have already bypassed the environment to flag this challenge. So, if the environment seems permissive, why can’t I bypass it too? With my experience, I know that it’s really difficult to set up a safe environment for a Python challenge. So, let’s go find an unintended solution.

Read the flag

First, we need to find where the flag is stored.

Flag PATH

Great! We are in the directory where the flag is. Let’s try to directly read it.

ModuleNotFoundError

Hmm, subprocess is not present. The ctypes library is removed when the Dockerfile is processed, which is certainly the root cause.

Dockerfile

Let’s try a different approach, this time only with Python.

stop bypass

Okay! I am sure that this is not the content of flag.txt. This string is for sure an output of the hook.so.

hook.so analysis

I think for a minute, if we can’t read the flag, then it’s certainly not possible to read hook.so? WRONG! We can get the content of the library to analyze it locally.

hook.so

meme

We can import it into Ghidra to know what we are facing. We see rapidly that a hook exists for each function of the libc.

libc hook

After a little research, we see that they have thought about all the functions that can open a file (cf: https://www.gnu.org/software/libc/manual/html_node/Opening-and-Closing-Files.html).

That’s not good for us. Moreover, there is a function that checks that the arguments passed to functions that could open a file do not contain the string “flag.txt”.

open function

check_value function

Good for us, we see the error messages previously seen. Now, we know that the problem comes from this library.

Bypass ?

Nice or not, there are some hooks. Maybe we can execute assembly code and do some syscalls without being restricted?

But it’s impossible to execute a binary because the execve function is hooked. How can we do it then?

Previously, we saw that the challenge maker created a Python module written in C. If we do the same, we can add assembly code inside and import it into our Python script.

Assembly

We first start by creating an assembly code that does a syscall on open, read, and write to read the flag.txt file.

Assembly

Before going further, we check our code locally to make sure that it works.

debug

We’re good! The most difficult step is over. We now need to create a Python module that contains the assembly code.

We start by getting assembly instructions in AT&T format.

The asm function doesn’t work with Intel format.

AT&T

There are multiple resources on the internet to create a simple module in Python. I personally used this one: https://github.com/starnight/python-c-extension.

Python C, Extension

After compiling, we check again that the code is working locally.

Compiling & Testing

Nice, that works!

Remote exploit

With all these elements, the last step is to build the exploit. When I first exploited it, I wrote my library inside the /tmp directory because it is inside the sys.path. However, when I rechecked it later, I found out that it was no longer possible to write inside the /tmp directory.

But that’s not a problem since /var/tmp works well. We just need to add this path to the sys.path variable in our final exploit. So we divided our exploit into two parts.

Exploit First Part

First, we added /var/tmp to the sys.path variable. Then we converted our library to hexadecimal and wrote it to the /var/tmp/athr.so file. The input() function gave us the possibility to bypass the maximum size limit of the script.

limit

Finally, we imported our module and called the function containing the assembly code.

Flag

And voila, we got the flag!

Flag

In my opinion, the unintended solution was super cool and it deserves to be a challenge. Thanks to the 2600 students who organised this CTF!