From May 5th to 7th the second edition of the PWNME CTF was held. Our team participated in the student category.
For this write-up, I will explain how I was able to bypass the intended way to flag the Pwn CPython101 challenge.
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.
- *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 220.127.116.11 1338*
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.
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.
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.
Great! We are in the directory where the flag is. Let’s try to directly read it.
Hmm, subprocess is not present. The ctypes library is removed when the Dockerfile is processed, which is certainly the root cause.
Let’s try a different approach, this time only with Python.
Okay! I am sure that this is not the content of flag.txt. This string is for sure an output of the hook.so.
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.
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.
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”.
Good for us, we see the error messages previously seen. Now, we know that the problem comes from this library.
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.
We first start by creating an assembly code that does a syscall on open, read, and write to read the flag.txt file.
Before going further, we check our code locally to make sure that it works.
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.
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.
After compiling, we check again that the code is working locally.
Nice, that works!
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.
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.
Finally, we imported our module and called the function containing the assembly code.
And voila, we got the flag!
In my opinion, the unintended solution was super cool and it deserves to be a challenge. Thank you to the 2600 students who organised this CTF!