mqtt攻击面和挖掘思路浅析

作者: 360漏洞研究院 苏熙杰 分类: 代码审计,安全研究,漏洞分析 发布时间: 2023-01-12 07:35

前言

在很多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,handle__packet函数会根据命令执行相应的函数,如果是CMD_CONNACK命令,将会步入handle__connack函数中。下图是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 ,就可以对应审计了。

比如:

  1. 找到一个可确定的topic
  2. 定位1883的bin
  3. 找引用,比如so库或者system调用后可能申请了子进程
  4. 自顶向下和自底向上减少搜索路径
  5. 最后确认路由 ……

Fuzz

思路相通,和http fuzz差不多,还是需要通过逆向找到相应的topic,然后和http fuzz一样怼着topic去发送变异数据包。