作者: 360漏洞研究院 苏熙杰
原文链接:https://vul.360.net/archives/649
前言
在很多IOT设备中默认存在MQTT服务,这是一个值得关注的攻击面。下面对MQTT协议及其挖掘思路进行分析。
WHAT?
MQTT是基于TCP/IP协议栈构建的异步通信消息协议,是一种轻量级的发布、订阅信息传输协议。 可在不可靠的网络环境中进行扩展,适用于设备硬件存储空间或网络带宽有限的场景。 使用MQTT协议,消息发送者与接收者不受时间和空间的限制。 物联网平台支持设备使用MQTT协议接入。
实现方式
实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。
MQTT传输的消息分为:主题(Topic)和负载(payload)两部分:
(1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);
(2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。
开源实现项目 Mosquitto
大部分设备中的MQTT协议实现是基于Mosquitto 改的,如果开源项目本身存在漏洞的话,将影响大部分设备的MQTT组件。下面简单分析下Mosquitto的实现和我们能够接触到的攻击面
基本启动方式
流程模块:websocket.c -> read_handle.c
在switch case LWS_CALLBACK_RECEIVE:
认证部分security.c
在handle_connect和authentic相关的时候,会执行mosquitto_security_auth_start,通过返回值rc确定数据流
其中mosquitto_security_auth_start 调用auth_start_v4相关
由此可知这个方法恒定返回MOSQ_ERR_SUCCESS,之后会调用到connect__on_authorised,里面的connection_check_acl再调用mosquitto_acl_check函数对权限进行检测
之后的mosquitto_acl_check函数会去配置信息,再根据策略去执行相关的认证函数。
网络net处理数据包
net__socket_accept()函数
mux_epoll__handle()函数
最终来自main函数调用的mosquitto_main_loop
回到生成sock本身,最后调用的是epoll_ctrl(epoll库)来生成
数据接收处理的地方
在callback_mqtt() 中: case LWS_CALLBACK_RECEIVE
case LWS_CALLBACK_RECEIVE:
if(!u || !u->mosq){
return -1;
}
mosq = u->mosq;
pos = 0;
buf = (uint8_t *)in;
G_BYTES_RECEIVED_INC(len);
while(pos < len){
if(!mosq->in_packet.command){
mosq->in_packet.command = buf[pos];
pos++;
/* Clients must send CONNECT as their first command. */
if(mosq->state == mosq_cs_new && (mosq->in_packet.command&0xF0) != CMD_CONNECT){
return -1;
}
}
if(mosq->in_packet.remaining_count <= 0){
do{
if(pos == len){
return 0;
}
byte = buf[pos];
pos++;
mosq->in_packet.remaining_count--;
/* Max 4 bytes length for remaining length as defined by protocol.
* Anything more likely means a broken/malicious client.
*/
if(mosq->in_packet.remaining_count < -4){
return -1;
}
mosq->in_packet.remaining_length += (byte & 127) * mosq->in_packet.remaining_mult;
mosq->in_packet.remaining_mult *= 128;
}while((byte & 128) != 0);
mosq->in_packet.remaining_count = (int8_t)(mosq->in_packet.remaining_count * -1);
if(mosq->in_packet.remaining_length > 0){
mosq->in_packet.payload = mosquitto__malloc(mosq->in_packet.remaining_length*sizeof(uint8_t));
if(!mosq->in_packet.payload){
return -1;
}
mosq->in_packet.to_process = mosq->in_packet.remaining_length;
}
}
if(mosq->in_packet.to_process>0){
if((uint32_t)len - pos >= mosq->in_packet.to_process){
memcpy(&mosq->in_packet.payload[mosq->in_packet.pos], &buf[pos], mosq->in_packet.to_process);
mosq->in_packet.pos += mosq->in_packet.to_process;
pos += mosq->in_packet.to_process;
mosq->in_packet.to_process = 0;
}else{
memcpy(&mosq->in_packet.payload[mosq->in_packet.pos], &buf[pos], len-pos);
mosq->in_packet.pos += (uint32_t)(len-pos);
mosq->in_packet.to_process -= (uint32_t)(len-pos);
return 0;
}
}
/* All data for this packet is read. */
mosq->in_packet.pos = 0;
#ifdef WITH_SYS_TREE
G_MSGS_RECEIVED_INC(1);
if(((mosq->in_packet.command)&0xF0) == CMD_PUBLISH){
G_PUB_MSGS_RECEIVED_INC(1);
}
#endif
rc = handle__packet(mosq);
/* Free data and reset values */
packet__cleanup(&mosq->in_packet);
keepalive__update(mosq);
if(rc && (mosq->out_packet || mosq->current_out_packet)) {
if(mosq->state != mosq_cs_disconnecting){
mosquitto__set_state(mosq, mosq_cs_disconnect_ws);
}
lws_callback_on_writable(mosq->wsi);
} else if (rc) {
do_disconnect(mosq, MOSQ_ERR_CONN_LOST);
return -1;
}
}
break;
数据赋值的地方
可惜定死了mosq->in_packet.to_process
攻击面
比较明显的攻击面就是Mosquitto本身和自定义的topic。未授权的情况下,组件本身的漏洞产生的点可能来自于数据流的处理,认证相关,如果存在弱密码或者配置文件认证信息写死的情况下,还可以关注topic的攻击面。
MQTT主要有两大版本: v3 和 v5,
MQTT v3 不支持 auth,所以遇到这种版本的相当于可以直接看topic的攻击面
Mosquitto本身
下面介绍一些Mosquitto的历史漏洞
CVE-2021-34434
NULL point漏洞,该漏洞是授权后的一个数据请求导致的。Client在连接之后可以在数据包中指定context->in_packet.command,handlepacket函数会根据命令执行相应的函数,如果是CMD_CONNACK命令,将会步入handleconnack函数中。下图是patch后的代码,可以看出添加了对NULL的检查,如果没有检查的情况下,之后调用context->bridge->name会异常崩溃。
CVE-2017-7650
漏洞函数是acl__check_single,由mosquitto_acl_check函数引用,前面提到,mosquitto_acl_check是认证权限相关的函数。当username 带有+#,可以绕过认证check。下图是patch后的代码,添加了对+#的检查
Broker 创建alc 文件可以指定权限配置
user admin
topic readwrite #
user user
topic /iot/user/+
漏洞产生的原因和这个样式有关。
CVE-2017-7651
mosquitto__calloc()这类型的的函数是自定义的内存分配函数,patch的地方就是限制了size,如果在没有限制size的情况,通过大量发包,可能导致资源占用过大dos
这个内存分配函数在前期接收数据包并生成mosquitto context的时候会调用,所以这是一个未授权就可以触发的漏洞。下图是patch后的代码,可以看到添加了大小限制。
自定义的topic
Mosquito本身的订阅和发布方式
//订阅
mosquitto_sub -h 127.0.0.1 -p 1883 -u admin -P 123 -t "#"
//发布
mosquitto_pub -h 127.0.0.1 -p 1883 -u admin -P 123 -t "/iot/user/pub/" -m "message_to_publish"
把mqtt问题转变成http问题:
Topic = url
Payload = data
Sub <--------> broker (1883)<--------> pub
例子
为了更好的理解sub和pub的关系及其topic的攻击面,可以阅读下面的例子。
在发布者像broker发布(pub)消息的时候,订阅者会接收(sub)到消息并执行回调(callback)函数。所以攻击者就相当于发布者的身份,被攻击的目标就是订阅者的身份,所以自定义topic的攻击面就是订阅者的回调函数。通过到了相应的路由,那么漏洞类型和http相关的类型差不多,可能涉及到逻辑漏洞、命令注入、溢出等。
Python实现的小例子
总结
本文介绍了MQTT协议和MQTT协议的开源实现Mosquitto,很多设备的mqtt组件都是基于Mosquitto改的,当然还有些是自定义实现的。不管是哪种,我们去挖掘其攻击面无非还是挖掘组件本身和路由(topic),挖掘的过程可以借鉴Mosquitto的代码和它的框架类型,快速地找到相应的逻辑代码。在实际挖掘过程中发现有些设备的mqtt组件存在协议版本过久和一些配置文件写死的情况,在这种情况下topic的攻击面将大大增加。
代码审计
万变不离其宗:字符串大法好!映射函数有可能在so中,也可能在别的service bin中(根据改写的方式而定),所以如果在单个bin中找不到相应的逻辑,需要在大目录下搜索相关字符串,找到映射路由topic/function ,就可以对应审计了。
比如:
- 找到一个可确定的topic
- 定位1883的bin
- 找引用,比如so库或者system调用后可能申请了子进程
- 自顶向下和自底向上减少搜索路径
- 最后确认路由 ……
Fuzz
思路相通,和http fuzz差不多,还是需要通过逆向找到相应的topic,然后和http fuzz一样怼着topic去发送变异数据包。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2040/