Yesterday we participated in LakeCTF 2024, a 24-hour jeopardy event for LakeCTF Finals. We ended up in first place! Here are some of the writeups for the challenges we solved.
Binary Exploitation
bash plus plus
What if you had a shell where you could only do maths?
nc chall.polygl0ts.ch 9034
We used a heap overflow from strcat
, which allowed unbounded input into a variable of fixed size, to modify variables stored in chunks created by C++ classes for address leaks and function calls.
Analysis
We are given a Dockerfile, binary, and source code for this challenge. Reading the source code, the program defines classes Log
and Variable
, Variable
having two child classes stringVariable
and longVariable
, along with enum TYPE
:
A map variables
is also defined to store Variable
objects:
The program also defines functions to deal with setting and getting the value of Variable
objects and allowing for basic arithmetic on longVariable
objects. The program also defines a win()
function, which prints the flag.
The main()
function allows for repeated input, and appears to process input to match certain syntaxes. The input goes to a char array each time, and although the input is unbound, it only seems to cause a program crash, but will come in handy. The inputs are processed as follows:
$((${var1_name}{op}${var2_name})
will callprocess_arithmetic()
${var_name}={var_value}
will callsetvar()
${var_name}
will store the output ofgetvarbyname()
in aVariable*
and callprint()
from the outputVariable
log
will set theLog *log log
variable to a newLog
object.- If the input does not match any of the formats above, it will just print the input and proceed
Getting a heap leak
The first input should be b"log"
, so the chunk it creates will be right next to chunks created by later commands instead of the cin
/cout
chunks it initially borders. The Log
implementation allows for infinite heap overflows, since each iteration adds the command using strcat
to a char array of length 10, which is stored on the heap.
After reinitializing the log, we initialize a stringVariable
and a longVariable
:
The heap starting from the log looks something like this:
The chunks created by variables can be ignored
We will trigger a heap overflow to overwrite the type of $a
from a stringVariable
to a longVariable
. Sending 29 trash bytes will change the type of $a
to the null byte at the end of the input, or 0. Now that $a
is a longVariable
, it can be used in process_arithmetic
calculations. This is important because process_arithmetic
does not use the print()
function, but just sends the output of the math operation to stdout, since the value of the vtable entry for print()
has been overwritten with trash to overwrite the Variable
type.
Since the longVariable
stores its value directly, sending $(($a+$b)
will print &$a->value + 0
, or just &$a->value
, which is relative to the heap base:
Getting a PIE Leak
Next, we will find the PIE base to calculate the win()
function address. A similar approach to the heap leak will be used, but this time it will use getLongVar
’s invalid variable message to leak a PIE address. When getLongVar
detects a Variable
does not have type LONG
, it will execute this:
where v
is:
If v->value.s
is an address that points to a PIE address (heap addr->PIE addr
), that PIE address will be leaked and the ELF base can be calculated. To start, reset the log so heap overflows will be easy to calculate.
Create one stringVariable
and one longVariable
:
The heap starting from the new log will look something like this:
Overflowing 37 trash bytes will reach $c->value
, which can be overwritten with the heap address that points to a PIE address. To find this, we will look at the chunks created by our Variable
objects. These chunks contain vtable entries for the their print
functions, which are located in the section of memory affected by PIE therefore usable to calculate the ELF base. We can use xinfo
to calculate the offset of the address we leak to the ELF base.
Removing the absolute path
So, our payload will be:
win
ning
Our final goal is to overwrite a Variable
object’s print
function with win
, then call it. First we reset the log.
To call win()
, we need to create a pointer of the win()
function (ptr->win
). This can be done by creating a longVariable
that contains the win()
function, then creating a stringVariable
and overwriting its print()
function address with the address to the longVariable
containing win
.
This is the heap starting from the new log:
Overwriting the print()
function address with &$e->value
using the heap overflow:
To call win()
, we just need to try to print the variable (${var_name}
).
Running the script on remote:
Final solve script:
Web
o1
This challenge was generated by chatgpt o1-mini. The website contains three main components:
The only port that’s exposed is port 9222, and goal is to fetch 127.0.0.1:9222/secret
from your local machine.
Look into app1.py
’s code, it’s trying to get the request IP address by request.remote_addr
. There shouldn’t be any http header bypass in here; however, it’s always a good idea to check. So I just used this payload list and, of course, it didn’t work:
So the only approach for solving is to bypass the checkers in the program: app1.py
-> app2.py
-> app3.js
-> app1.py
.
Checkers?
app1.py
will replace all the backslashes, and then makes sure the URL schema is HTTP or HTTPS without custom ports allowed. If our goal is to let app3.js
request http://127.0.0.1:9222/secret
, we need to have a way to smuggle in the custom port.
Now looking into app2.py
, it’s even more harsh. It checks the URL if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'): return 400
, which totally removes the idea of requesting 127.0.0.1
.
The good thing here is that there is no checker on app3.js
; however, it’s using node.js’s fetch()
which we only can use HTTP and HTTPS protocol URLs (file:///
is not supported, so we can’t just fetch file:///app1.py
).
URL confusion attempts
One possibility here is to bypass the checker by using the difference between node.js’s fetch()
and Python’s urllib.parse.urlparse(url)
. Python’s urllib
strictly follows RFC3986 protocol (formal definition from here):
For a more simple picture:
The first idea is that everything in front of the @
is actually the credentials of the URL, not the actual domain. Interestingly enough, everything after \
in Python will be just ignored.
So in combination, if I request https://attacker.com\@@example.com
, it would actually take me to https://attacker.com
in normal browser:
Sadly in fetch()
, due to safety issues they don’t allow credentials in the URL:
By adding extra \
, can bypass both node.js’s fetch()
and also bypass the Python checker:
Now that fetch()
is working, we have another issue: app2.py
is checking if the port is 80 or 443 or not. The scheme is what determines the port:
My second idea is to smuggle in some wired scheme such that it would go to some interesting place. The socket.getservbyname()
function will read the /etc/services
. In the Docker that’s provided, there isn’t anything on port 9222, which is sad. Plus, we can’t really smuggle in a scheme because it’s strictly checked by RF3986.
ChatGPT moment
I always feel that since this code is generated by ChatGPT that there must be some error in the programming logic that’s hard to find. After a day of struggling, we saw this section:
Here, it’s checking the parsed port. What if the port is 0? Then if parsed.port
will be always False
and pass the app1.py
checker!
And in the second checker:
The if
statement only checks the domain when the port is 443 or 80. If the port is 0, it won’t check at all. Thus, the second checker gets passed!
Now the question becomes: is hosting server on port 0 possible?
Port 0
Node.js’s fetch()
supports port 0 and we tested it, so it won’t randomly send to a port. After tons of research, it seems impossible to host a server on port 0:
Here is the post.
Everything hosted there will be assigned a random port.
However, not being able to host on port 0 doesn’t mean it can’t be redirected to another port.
In this post, the author uses iptables
to redirect port 0 to 80 and successfully curls port 0 to get the response.
So we used the same method and set up our server:
Where on port 80 we set a redirection to http://127.0.0.1:9222/secret
(thankfully, Node.js’s fetch()
don’t care about unsafe fetch).
Flagg!!
EPFL{gpt ch411zz is the nEw m3t4}
Misc
VerySusOrganization
We are given a link to a website that when opened allows us to generate an invite to a github organization. After accepting our invite we gain access to the highly suspicious: https://github.com/VerySusOrganization2 where we find only two repositories
Looking into the sus-image-generator-repo we are able to find a rather interesting github actions script
Whenever a comment is added on issues that starts with the string “/run-build” the action will clone the repository, write our flag from the FLAG environment variable to a flag.txt in the home directory, and run npm install.
Luckily for us, looking at the package.json (which specifies the dependencies to be installed) we find that our repository from earlier “suspicious-random-number-generator” is listed as a dependency. That way we can write to our suspicious-random-number-generator repository and that code will be installed upon leaving a “/run-build” comment.
How can we get code to run upon installing our other repo as a dependency though? Easily enough we can just rely on npm’s preinstall/postinstall scripts. Initially, the package.json for the suspicious-random-number-generator-repo looks like
but we can modify it to include a postinstall script that looks like
and additionally we add a postinstall.js script that will read the flag and send it to an endpoint we control
This way we can write any javascript code in our postinstall.js and it will get triggered right after the npm install
runs. With the above script we can read the flag file from the directory it was created in and send it to an endpoint we control like a webhook.
After committing the changes to our repo we go back to the sus-image-generator-repo, create an issue, and leave the necessary “/run-build” comment to trigger our build.
after which we check our webhook and see that we’ve gotten the flag.