Marked as easy, Safe is a contentious box from HTB requiring a custom developed ROP (return-oriented programming) exploit tied into cracking a KeepPass database. Personally I absolutely loved the box from the perspective of digging into ROP and practicing the techniques. The box itself however was quite barren and could have definitely used a bit more "makeup" to get away from the CTF-like vibes. If you have a base in binary exploitation but haven't dived into ROP exploits before this is a great box to make that jump. The Ellingson box was then a good next-step as it took the same ROP concepts and required use of ret2lib which wasn't required here.
Initial Recon
Per normal kicking off the recon phase with an nmap query.
Already this seems interesting with port 1337. What happens when we go to the page manually
Hum...that output seems suspciously like the output of uptime. So this should mean that at somepoint during the call it's executing the command and returning the result. Let's see if we can fuzz this quick and dirty.
So when passing 120 characters to the call it still prints out the uptime result, however seemingly the execution flow afterwards breaks.
I spent a bit of time attempting different inputs however all I was able to glean was the difference with >=120 characters. Decided to go back to the drawing board and look at port 80. Looks like just a default apache page - nothing special here. Or is there...
Sneaky... I went to http://10.10.10.147/myapp and grabed a copy of the myapp program. Great now it's definitely looking more like a RE/Binex based challenge.
User exploitation
First thing I do is open up Ghidra to try and go through the source code decompile. The program itself seems quite simplistic.
Now that we understand the logic flow let's replicate the overflow with a debugger attached and see what we can find. While we're at it let's also take a quick look at the security settings of the app.
With NX set we know we won't be able to push our own code to the Stack and have it executed so we will need to look for a different avenue. Continuing with the execution flow we can see that we completely overwrite $rbp. A quick look online gives a good breakdown on various register purposes - %rbp points to the current stack frame, %rsp points to the top of the stack. With our ability to overwrite %rbp we should be able to leverage that to hijack execution flow. Let's get a bit more details using pattern create.
Ok so the first 112 bytes are garbage that fills up the gets() array, the next 8 bytes will overwrite $rbp and from 120 on looks like we're overwriting the stack.
Enter the ROP
Firstly, what is ROP? Good old wikipedia to the rescue.
In this technique, an attacker gains control of the call stack to hijack program control flow and then executes carefully chosen machine instruction sequences that are already present in the machine's memory, called "gadgets". Each gadget typically ends in a return instruction and is located in a subroutine within the existing program and/or shared library code. Chained together, these gadgets allow an attacker to perform arbitrary operations on a machine employing defenses that thwart simpler attacks.
Essentially we are trying to pass memory addresses to the stack that when sequentially executed will perform actions we want. At the end of the execution flow of myapp there is a leave/ret combo.
Looking deeper into code theory I found that with leave - "...the old frame pointer is then popped from the stack..." and ret - "Transfers program control to a return address located on the top of the stack". Since we have control of the stack we should be able leverage this to load the stack with function addresses we want to execute. Thankfully there is a system() call directly in the main function. We won't have to use any advanced techniques, we only need to reference the address of the function directly - 0x0040116e. Padding the address appropriately for 64 bit, let's give it a shot and check in the debugger.
Excellent. We see the system() call at the top of the stack and our breakpoint at myapp's return 0; show's the execution flow is being redirected to system() instead of exiting. Now this in itself wasn't particularly useful. I wanted to see how /usr/bin/uptime was being passed to the call. I set up a breakpoint on the initial call and compared between this one and my forced call at the end.
Alright, so I need to find a gadget that can overwrite $rdi to something of my choosing. Going back to the ghidra decompile I found a handy function built in to the app. Despite test() never being called, it does provide us the appropriate gadgets necessary to update $rdi.
Now there is one extra complication here. While test()moves the value of $rsp into $rdi like we need, it then jumps to the value of $r13 which is currently/will be empty at the moment. We need to chain this call with another gadget that also pops an address off the stack (which we control) into $r13. GDB has a nice ropper function for this exact purpose.
Ok so there are three pops as part of this gadget. We need to take that into consideration and include fluff to ensure we get the right address into r13 and the rest our stack isn't adversely affected. Thinking about this logically we want to form the exploit as follows:
Ok, let's put it together and see how it goes:
Wait a minute, that doesn't look right... It seems when attempting to execute it is pulling the first 8 characters before the stack in addition to what is on the stack. Essentially bytes 112-120, or what we overwrite into $rbp. Ok we can work with that. Let's adjust the exploit.
This also didn't work. But knowing where to look we took a look at our system() call and see what $rdi looks like.
Ok, we are calling /bin/sh properly. Now we need to replace that second instance with the command we want to run and also terminate the command to avoid chaining the rest of the stack. I forgot to terminate on the next attempt, however that irrelevant as I managed to get my test command executed! At this point I was over the moon.
With some tweaking I was ready give it a shot with the actual myapp instead of my local variant. With baited breath I pressed enter and hoped for the best.
*Happy dance* while not a shell, I was able to cross user flag off the list. With my moment of joy subsiding, let's see how we can use this.
At this point, I essentially have single-command RCE. There are quite a few different ways I could do this. My initial thought was adding my ssh keys to authorized_keys. For whatever reason I couldn't get it to work with the single command RCE. Up next, I attempted to execute a nc back to my local listener, but it seemed like it (among a lot of other common binaries) was not installed. No worries, let's get a static version of nc and host it on our machine, then point our RCE to wget it.
I then repeated the command setting it to executable with chmod +x /home/user/nc. With my local listener setup, I format the command to connect back to me.
Now that there is a limited shell, let's add the ssh keys and connect back properly.
Bam. Ok what a ride. It's not over yet though, we still have root to conquer.
Root exploitation
Unfortunately root was not as exciting as user. I did however still learn a thing or two going through the process. With our proper user shell I take a look around and notice a .kdbx file along with 6 JPG files. Let's move those over to our machine for further analysis.
After a bit of searching around find out that .kdbx is a Keeppass database file. This article gave me a bit of background in how to use JTR to crack the database. After going through the full rockyou.txt wordlist it didn't seem like I had this right. A little deeper searching and I found this article that explained how Keeppass databases can also be locked using a keyfile. Considering we have 6 different JPG files this seems like an interesting angle. Using the various JPGs used keepass2john to generate the hash and pass it along to JTR. After a few attempts - victory!
Well that's a password alright. Let's get install kpcli and take a poke at what we can see.
Unfortunately both eMail and Internet were empty. Ok I wonder what else I can do with kpcli.
Oooohhh amazing! Ok so we dumped the "root" password into _found.
Now this wasn't the flag itself, so let's try and pivot to root using the kpcli password as root's password.