CSI CTF 2020: pwn-intended-0x3 with Unnecessary Arbitrary RCE
I participate in Capture the Flag (CTF) events in a non-serious way in my free time. Unfortunately, I wasn’t able to play in the recent jeopardy-style CSI CTF–you can find more details about the event here–but I did get to do some interesting things with it after the fact.
After the CTF closed, I had a coworker ask me to check over his exploit for the pwn-intended-0x3 challenge to see what was going wrong and offer some pointers. As I was going through the provided exploit, it was so close to correct that I thought, “Surely he should have gotten the flag from trial and error. Maybe the challenge is actually harder than intended?”
To show you what I mean, in this blog post I will first provide a walk-through of the exploit and what turns out to be the intended solution, followed by showing you how I went beyond with arbitrary remote code execution (RCE).
Reversing
This is an easy challenge, so you could just go straight to the disassembly of main
and know just about everything. However, it’s usually best to start with a high level perspective before you get excited and start running after false paths.
I use radare2; the i
command gives you all the good stuff you want to know early.
Protip: add e cfg.fortunes.clippy = true
to ~/.radare2rc
so clippy will encourage you when you start your reversing journey.
So, the file is a Linux ELF with 64 bit x86 instructions. The nx
value tells us that the stack is non-executable. We can assume the system on which this is running has ASLR (added space layout randomization) but since pie
is false
, all the locations in the binary will be unchanged.
Let’s see what functions are used.
Life is good! We have a reference to the libc functions gets
and system
. Surely the vulnerability is in gets
and now we don’t need to leak the address of system
or execve
.
Also, notice the sym.flag
function. There is no imp
in the name, so it is not an imported function; it is in the binary itself. Let’s disassemble that.
No need for the Ghidra decompiler; this is super simple. The system
function is called on the string “cat flag.txt”. If we get the instruction pointer anywhere near here, we should get the flag. We still haven’t found the vulnerability, but surely it will be in the main function.
I will spare you the disassembly of main, it is also trivial. Really it just calls gets
with a stack address. This is a basic stack overflow. We simply have to overwrite the return address at the bottom of the stack frame with sym.flag
and we should get the flag.
The exploit
So, my buddy sent me his broken exploit. I checked it and everything seemed good. What? Why isn’t it working?
The exploit was in Python and already had the pwnlib imported. So I added a gdb.attach
call to get a debugger before the exploit was sent. I continued in gdb until I was at the last instruction in main. Then I checked what was on the stack using a command like x/10gx $rsp-0x20
. Turns out, my buddy missed his offset by 4 bytes.
Here is a working exploit.
Off by 4
I do not mention this mistake to insult my friend. Anyone who has ever attempted a binary exploitation challenge will know the pain of being off by a few bytes. Anyone who has attempted a second challenge will know the pain again. Even experts in this will continue to make this mistake. If you are new to binary exploitation, don’t let this discourage you. Keep at it–you have a long road of pain ahead. 🙂
I have another reason for mentioning this, though…
Going beyond via foolish assumptions
While the exploit works on my system, will it work on the real CTF? I don’t know; the CTF was over, so the challenge server was likely down.
That raises a question, though: what if my exploit failed?
Maybe my buddy did get the offset correct at one time, but started moving it around because the exploit failed on the real server. Could sym.flag
be a false path? If the server had the flag.txt
file in a different directory, then cat flag.txt
would fail. This challenge does appear very conducive to arbitrary code execution. Maybe there is even a secret extra flag or a pwn-intended-0x4 challenge where you need arbitrary code execution.
Turns out the answer to all these questions is no. However, since I did not know any better, I went ahead and got arbitrary remote code execution. Here is how I did it:
Arbitrary RCE exploit

The exploit above proves we can overwrite the return address with a call to system. Since gets
is lax about NULL bytes, we can send a nearly arbitrarily long ROP chain. All we need is a ROP chain that calls system
with “/bin/sh\x00” as the only parameter.
Getting control of the parameter is easy. Linux 64 bit executables use the rdi
register as the first parameter. Nearly every linux executable will have a pop rdi
ROP gadget as part of the so called BROP gadget. Radare2 can quickly find this location with the following command:
All we need now is an address to pop into rdi
that will point to the 8 byte string “/bin/sh\x00”. This string will occur in libc, but libc will be at a random offset due to ASRL.
I don’t want to have to construct a leak primitive, so let’s just shove the “/bin/sh\x00” string into a memory ourselves. This is easy because we have the gets
function, which only accepts one parameter. We already found the gadget to control the parameter, so we need a memory location in the binary that has read/write permissions (use iS
command in r2). I chose the space just after the stdout pointer.
The following exploit code is surprisingly straightforward and should be easy to follow. This will get arbitrary code execution on the challenge.
Challenges are back up!
Turns out the challenge servers are up after everything is said and done. I’d guess this is a kindness to people like me who are writing things up. This means I can actually check if my exploit works on the real server and if the flag.txt file is really where it should be!
Yup… My buddy was just off by 4.
Conclusion
There are two big takeaways here. First, offsets are hard. You have to be perfect and it’s really easy to commit off-by-one errors. There’s a lot to keep in mind. Forbidden bytes, endianness, and architecture size. Don’t be discouraged; hacking is all about failing until you get it right. Consider it all debugger practice. Personally, I consider the ability to debug and correct such mistakes to be of much higher value than the ability to get it right on the first try.
The other lesson here is about proper programming practices. The use of the system function was lazy. I am not criticizing the real developer here. The developers intention here was to design something vulnerable, and they did a great job.
Now, of course, if the file was compiled with pie and stack cookies, as is default nowadays, this exploit likely would have been impossible. Just avoiding the system call through would have made the exploit quite a bit more difficult. If the flag file was just opened and read to stdout, we would not know the location of the system function. We would have had to use multiple ROP gadgets to leak addresses in libc, all the while keeping a program state that won’t crash. Then, we would have to use the information we gained in a second stage ROP chain to execute our payload. Even little things like avoiding the system can go a long way toward frustrating attackers.
Hopefully, I showed how fun exploitation can be. It is like a puzzle, dividing the problems and then conquering them independently. Thanks for reading!
About Hurricane Labs
Hurricane Labs is a dynamic Managed Services Provider that unlocks the potential of Splunk and security for diverse enterprises across the United States. With a dedicated, Splunk-focused team and an emphasis on humanity and collaboration, we provide the skills, resources, and results to help make our customers’ lives easier.
For more information, visit www.hurricanelabs.com and follow us on Twitter @hurricanelabs.
