作者: 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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2040/