1. MQTT协议
1.1 介绍
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),可以以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务,是一种低开销、低带宽占用的即时通讯协议。MQTT协议运行于TCP之上,属于应用层协议。
1.2 消息格式
一个固定的报头,一个可变报文头、一个负荷
固定报文头|可变报文头|负荷
固定报文头(Fix Header)
至少两个字节,第一个字节包含消息类型和QoS级别等标志位。第二个字节是剩余长度字段,该长度是后面的可变报文头加消息负载的总长度,该字段做多允许四个字节。
单个字节最大长度是127字节,为什么不是256字节呢?MQTT协议规定,单个字节第八位若为1,则表示后续还有字节存在,第八位起"延续位"的作用。
MQTT协议最多允许使用四个字节表示剩余长度,并且最后一个字节最大值只能是0x7F不能是0xff,所以只能发送信息长度是256MB,而不是512MB。
可变报文头(Variable Header)
包含协议名、协议版本、连接标志、心跳间隔时间、连接返回码、主题名等
有效负荷(Payload)
理解为消息主体(body)
MQTT消息类型
MQTT发送的消息类型
CONNECT(连接)
PUBLISH(订阅)
SUBSCRIBE(订阅确认)
UNSUBSCRIBE(取消订阅)
固件报文头中的第一个字节包含连接标志(Connect Flags),该标志用来区分MQTT的消息类型
类型名称 | 类型值 | 流动方向 | 说明 |
---|---|---|---|
Reserved | 0 | 禁止 | 保留 |
CONNECT | 1 | 客户端到服务端 | 发起连接 |
CONNACK | 2 | 服务端到客户端 | 连接确认 |
PUBLISH | 3 | 两个方向都可以 | 发布消息 |
PUBACK | 4 | 两个方向都可以 | Qos1消息确认 |
PUBREC | 5 | 两个方向都可以 | Qos2消息回执 |
PUBREL | 6 | 两个方向都可以 | Qos2消息释放 |
PUBCOMP | 7 | 两个方向都可以 | Qos2消息完成 |
SUBSCRIBE | 8 | 客户端到服务端 | 订阅请求 |
SUBACK | 9 | 服务端到客户端 | 订阅确认 |
UNSUBSCRIBE | 10 | 客户端到服务端 | 取消订阅 |
UNSUBACK | 11 | 服务端到客户端 | 取消订阅确认 |
PINGREQ | 12 | 客户端到服务端 | 心跳请求 |
PINGRESP | 13 | 客户端到服务端 | 心跳响应 |
DISCONNECT | 14 | 客户端到服务端 | 断开连接 |
Reserved | 15 | 禁止 | 保留 |
消息质量
三个等级
QoS 0
最多分发一次。消息的传递完全依赖底层的TCP/IP网络,协议里没有定义应答和重试,消息指挥到达一次。
QoS 1
至少分发一次。服务器的消息接收有PUBACk消息进行确认,如果通信链路或发送设备异常,或者指定时间内没有收到确认消息,发送端会重发这条在消息头中设置了DUP位的消息。
QoS 2
只分发一次。这是最高级别的消息传递,消息丢失和重复都是不可接受的,使用这个服务质量等级会有额外的开销。
举例
共享单车锁,定时使用Qos Level 0质量消息请求服务器,发送单车的当前位置,如果服务器没有收到这个消息也没有关系,过段时间也会再发送一次。找到单车后需要进行解锁,这时候可以使用QoS level 1质量消息,手机APP不断向单车发送解锁消息,确保有一次消息能到达以解锁单车。用户用完单车之后,需要提交付款表单,可以使用QoS Level 2质量消息,这样确保只传递一次数据,否则用户就会多付钱。
遗愿标志(Will Flag)
标志字段
Will Flag
Will Qos
Will Retain Flag
服务端和客户端通信时,当遇到异常或客户端心跳超时的情况,MQTT服务器会替客户端发布一个Will消息。如果服务器收到来自客户端的DISCONNECT消息,则不会触发Will消息的发送。
Will字段可以应用于设备掉线后需要通知用户的场景。
连接保活心跳机制(Keep Alive Timer)
MQTT客户端可以设置一个心跳间隔时间(Keep Alive Timers),表示再每个心跳间隔时间内发送一条消息。如果在这个时间周期内,没有业务数据相关的信息,客户端会发一个PINGREQ消息,相应的,服务器返回一个PINGRESP消息进行确认。如果服务器在一个半心跳间隔时间周期内没有收到来自客户端的消息,就会断开于客户端的连接。心跳间隔时间最大值大约可以设置位18个小时,0值意味着不断开。
异步发布/订阅实现
MQTT可以双向通信,MQTT支持服务端反向控制设备,设备可以订阅某个主题,然后发布者对该主题进行发布消息,设备收到消息后即可进行一些列操作。
MQTT安全
MQTT运行于TCP层以上并以明文传输,这就相当于HTTP的明文传输。
风险:
设备被盗用
客户端和服务端的静态数据被修改
拒绝服务攻击
通信被拦截、修改、重定向
虚拟控制报文注入
MQTT安全从三个方面考虑
应用层:MQTT提供客户标识以及用户名和密码,在应用层验证设备
传输层:类似于HTTPS,MQTT基于TCP连接,也可以加上一层TLS,防止中间人攻击
网络层:专线或者vp/n来连接设备与MQTT代理
认证
两种层次的认证:
1、应用层:MQTT支持客户标识、用户名和密码
2、传输层:传输层可以使用TLS,除了加密通讯,还可以使用x509证书来认证设备
客户标识
一般来说可以使用嵌入式芯片的MAC地址或者芯片序列号
用户名和密码
MQTT协议可以通过CONNECT消息发送username和password
由于以明文形式传输,所以使用抓包工具可以轻易的获取。
传输层认证
MQTT代理在TLS握手成功后可以继续发送客户端的x509证书来验证设备,如果不合法便可中断连接。使用x509认证的好处是,在传输层就可以验证设备的合法性,在发送Connect消息之前便可以阻断非法设备的连接,节省不必要的资源浪费,确保消息的完整性和保密性。
用户数据格式
十六进制或二进制
流量控制在非常小
字符串
JSON
JSON层次结构清晰,易于阅读和编写,易于机器解析和生成,有效提升网络传输效率
{“String":"Hello World!","Value":12345} JSON字符串
XML
MQTT协议只负责通信部分,用户可以自己选择
MQTT+JSON是目前最优方案。
1.3 MQTT通信
多个节点通过订阅同一个主题进行相互通信,通过tomqtt来指定发送目标及获取消息来源
tomqtt概念(类似目录结构)
某品牌汽车锁车协议主题remote_control/result/rvs_set_doorlock
一个或者多个主题
主题层级分隔符"/"
单层通配符"+"
多层通配符"#"
一般mqtt代理都会使用$开头的tomqtt作为系统调试、存储信息专用的tomqtt
1.4 例子
mosquitto_sub
基于订阅发布的
http://mosquitto.org/download/MQTT服务端程序
订阅消息
mosquitto_sub -h 127.0.0.1 -t debug(主题)
发布消息
mosquitto_pub -h 127.0.0.1 -t debug -m "Hello World" -r(保留在服务器上最后一条信息功能)
node-red
增加/test主题
mqtt.fx
订阅/test
接收消息
发布消息
1.5 批量扫描设备
fofa搜索1883端口(port="1883" && protocol="mqtt")
python扫描
扫描结果
2. 安全问题
1. MITM攻击
MQTT默认基于1883端口,由于认证信息默认明文传输,容易收到中间人攻击。可以使用SSL加密,其默认端口为8883。
2. 未授权问题
MQTT消息服务器存在配置错误,使用者没有配置认证造成未授权访问。
利用通配符获取订阅所有Tomqtt
MQTT主题支持"+,#"的通配符,假设我们有两个Tomqtt分别为CMD/123/456 CMD/789/666,那么我们可以订阅CMD/#来获取CMD下的全部消息。在攻击中我们首先就可以利用,监听所有的不易开头的。对于开头的Tomqtt。对于开头的Tomqtt我们可以使用$SYS/#来订阅
https://github.com/mqtt/mqtt.github.io/wiki/SYS-Tomqtts
权限控制问题
MQTT服务器没有对普通用户权限进行管理,允许其使用通配符。
对用户没有权限验证导致其可以任意订阅内容,获取其他成员消息
比如说打开电灯、打开车门的类似操作
3. 密码爆破
匿名登录问题
import paho.mqtt.client as mqtt def on_connect(client, userdata, flags, rc): print("[+] Connection successful") client.subscribe('#', qos=1) client.subscribe('$SYS/#') def on_message(client, userdata, msg): print('[+] Tomqtt: %s - Message: %s' % (msg.tomqtt, msg.payload)) # Press the green button in the gutter to run the script. if __name__ == '__main__': client = mqtt.Client(client_id="MqttClient") client.on_connect = on_connect client.on_message = on_message client.connect('扫描设备IP', 1883, 60) client.loop_forever() # client.publish('/test', "I am from pycharm client!")
emqttd/etc/emq.conf # 默认开启是任何人都能登录需要改为false
mqtt.allow_anonymous = true
注意认证插件
EMQ中,emq_plugin_template为一个开发插件的模板,自动开启导致任意用户密码都可以连接成功。
暴力破解
开启认证模式
开启插件
https://github.com/zombiesam/joffrey
密码爆破
总结
MQTT协议作为物联网常用协议,带来技术便利的同时也产生了许多安全问题,需要开发者注意。