Buckeye
Buckeye 2024 was kinda fun ctf(except for web I had struggle little bit). But in the end .;,;. full cleared and first place! Gentleman was a interesting chall.
TL;DR:
There is a format string vuln inside a class , and has arbitrary file upload. Trying to archive RCE. Payload:
Generic
Description of the challenge Time to do the impossible.
The challenge provide a link: gentleman.challs.pwnoh.io, and the source code of the website.
Open the website, it’s a normal TypeMonkey website, has login and signup function.
Inside the source code, it provides a docker with python:3.12-slim-bookworm
,
The email is interesting, but it’s super unclear what is the email referencing to. I have also tried to search, everything on this email is not real. ¯\*(ツ)*/¯
There is a readflag
, and the flag.txt is chown root:root /readflag && chmod 4755 /readflag
, so clearly, there is something inside the challenge can lead to remote code execution in order to get the flag.
The TypeMonkey uses flask
as the main server and and sqlalchemy
as the user database. Flask session is encrypted using SECRET_KEY = token_urlsafe()[:32]
so it’s pretty safe.
Reading through the source, the models.py
looks little bit suspicious.
Python Formating String
Inside the User Model, there is this extra __repr__
function. When we try to print out a python object, it will try to call this object’s repr function. Every object inherit this repr, but you can choose to overwrite it.
Overwrite a repr function is not that harmful at all. But at this place, we have control of the username, and it uses format string twice. That creates a problem
'<User {u.username} (id {{[i.id](http://i.id/)}})>'.format(u=self).format(i=self)
there is two format. Format will find the {}
and pass in the content. At here, the first format formats the username. We can craft a bad username that lead to access other objects.
Username: {i.username}
Formated String: '<User {i.username} (id {[i.id](http://i.id/)})>'.format(i=self)
However, python format string has a restriction of can’t call functions inside the format string. More information this is a good place to look at: HackTricks
The __repr__
function is being called at here, str(user)
,I just added a print statement inside the User class and find it’s being called here
No Function Call? Access Denied?
Now we know the goal is to get RCE, and the only vuln we have right now is this formating string. The format string passes in a User object. What can we do with it?
Usually, we can steal secret with it!
For example:
We can steal something from the main code, however, the issue is that the server won’t return anything:
So we need to fine something that can result in RCE.
As you can see in the previous example, we actually might be able to call functions using []!
By default, [ ]
will use the __getitem__
function inside the python’s object. And also when we trying to get a object’s attribute it will also call function like object.something
it will call object.__getattr__(self, something)
(In fact, every python operation is a function!)
So if there actually exist something that we can call, it just we can’t control the code or control what we can call. However, if we find deep enough we can find something that will cause issue with __getitem__
or __getattr__
, we can get RCE!
Amazing python! https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access
Find the needle in the haystack
Although inside format string we don’t have that many builtins and we only passed in class, we still can access other objects inside python. We will discuss later about how we can get it, but assume we have all the object accessible, what do we have right now?
Interesting packages are imported in here, such as numpy
, json
, werkzeug
, jinja2
, flask
, sqlalchemy
.……
Not even just consider the imported classes, just inside python standard library we have more than two hundred specialled defined __getitem__
.….
It’s too much!!! How can we find some dangerous __getitem__
or __getattr__
?
Well this github search first object is pretty interesting,
Which we were considering we might be able to do some sql injection here to lead to something. However, 1. This is sqlite, we can’t really do system call or read flag. 2. _execute
and the LOOKUP_KEY
is very safe here
We were even thinking about to use sql injection to read other people’s username here however it’s safe sql execution… QuQ….
So we did a more systemtic search to find all the function that has get item and get attr defined:
Too much functions QuQ….
We read all of it one by one, trying to find useful ones.
tempfile._TemporaryFileWrapper, looks sus but not useful
sqlalchemy.orm.util.AliasedClas, not useful too…
weakref.WeakValueDictionary
EntryPoints from importlib.metadata
All of them have potential, yet none of them is helpful!!!!!
Time to do the impossible!
Quasar knew this before we did all of these analysis.
But we all ignored this and was so hyper focus on the author’s hint about python standard library.
Authors hint in the server.
After a while trying to find the needle, we turn back to the idea of ctypes.
Ctype code snip selection
ctype.cdll
has a specialized get item function, which means every time when we do ctype.cdll['libc.so.6']
it will auto load the libc.so.6
! Which is amazing.
However we were struggling and don’t know what to load.
We tried to load /readflag
, however, it’s compile limited it to being loaded.
We also tried to load cpython module, but nothing really helpful at here. So we went through the source code again, and find this:
gentleman/typemonkey-app/app/routes.py
it will save the score(no limit of how many score here), which is a numpy list, to a file! And according to the numpy doc, it don’t have a header!
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tofile.html
Which means we got arbitrary file upload, it’s just we never found it before, and didn’t know it might be helpful. But now everything changed, we just miss a library, why don’t we jut upload a library with bad code?
Payload time!
When we got to this place, there was only 50 min left in the CTF, it was super stressful and fun. We decided to just set up a reverse shell:
Compile this to libtest.so
, and convert to python float. Yet the server side only will save score when score is bigger than previous one, which means if the float have null, it won’t work.
Now we have a score that will result in the file that we want, just need to figure out how to recover ctype:
Where the init recovers a function, access global from the function, get the builtins, use loader to load system, use ctypes inside system module, and load cdll.
Final solve script:
Flaggg!
bctf{now_that_is_a_powerful_class_4e507}