Brandon T. Elliott
CTF Writeup: picoCTF 2023 - "Tic-Tac"
The CTF
picoCTF 2023 took place from March, 14th, 2023 to March 28th, 2023.
The Challenge
This binary exploitation challenge began with the following description:
After ssh’ing into my challenge instance, running an ls
showed the following files were in our home directory:
The Binary
The primary file of interest is the txtreader
binary which takes an argument of a file name, of which the binary will print the contents of if the user owns the file, as the challenge description stated.
Of course, just supplying the flag.txt
file will not work, since we don’t own it.
The Source
The next file to take a look at is src.cpp
which presumably is the source code for the txtreader
binary.
Upon inspecting the source code, it appears that the binary checks the ownership information using the stat()
function.
Symbolic Links
My first thought for solving the challenge was to create a symbolic link to the flag.txt
file and supply the symbolic link as an argument to the txtreader
binary.
This should work… right?
No. But we own the symbolic link…
So, why doesn’t it work?
The Stat() Function
A quick search for the stat() function gives us the answer as to why.
If the file is a symbolic link, the stat() function first follows the symbolic link, and then returns the file info only after the path has been resolved.
Apparently, if the lstat()
function had been used instead, our attempt would have worked, as lstat() would’ve returned the file info for the symbolic link.
TOCTOU
Going back to the challenge description, there was an interesting tag for the challenge, which was toctou
.
Reading up on this, we see that this stands for “time-of-check to time-of-use”.
This is a race condition that appears to exist in the txtreader
binary due to the fact that it first checks whether the user owns the file, and then uses the results of that check in order to read the contents of the file.
Although it may be only milliseconds in length, there is a time period in between those two actions taking place, which could allow us to modify the state of the file at precisely the right time window, and subsequently be able to read files that we don’t own.
Exploitation
Setting the stage for our exploit, first we will need to continuously create a symbolic link to a file that we do own. Then, we need to immediately change the symbolic link to a file which we want to read that we don’t necessarily own (in this case, obviously we are interested in the flag.txt
file).
Although this could be achieved in a myriad of ways, for simplicity’s sake, I chose to use a bash one-liner for this:
timeout 30s bash -c 'while true; do ln -sf src.cpp flag; ln -sf flag.txt flag; done' &
I chose to use a while true
loop for simplicity and wrap it in a timeout 30s
command so that the loop will end after 30 seconds no matter what, which should give us more than enough time. Placing the &
at the end will send it to the background and will allow us to simultaneously run the second part of our exploit from within the same shell.
Inside the loop, since we already know we can read the src.cpp
file, we will first create a symbolic link to this file and call it flag
.
Next, it will forcefully create a new symbolic link to flag.txt
and will also call it flag
.
In a nutshell, this loop will repeatedly create and modify the flag
symbolic link, switching between a file we own, and a file we don’t.
While this is running in the background, we will simultaneously run the second part of our exploit, which is also a bash one-liner:
while ! ./txtreader flag 2> /dev/null | grep "picoCTF"; do :; done
The second part will also run a while loop, and will attempt to run ./txtreader flag
while sending errors to /dev/null
and grepping for picoCTF
(which would indicate the flag was successfully printed out). When the flag is printed out, the loop will end.
Putting both pieces together, the underlying idea here is that eventually the timing will line up so that the binary will first check whether we own the src.cpp
file, which we do, and then the symbolic link will be modified to point to the flag.txt
file in time before the binary reads the file, and subsequently it will print out the flag.
Nothing left to do now but to run both parts of our exploit, and wait for the timing to be just right.
As seen in the screenshot above, the flag was successfully printed out after only a second or two. We can now do a fg
and Ctrl+C
to stop the first part of our exploit as that’s no longer needed (although it will stop after 30 seconds no matter what).