今年的西湖论剑IOT部分提供了一块搭载了openwrt的开发板,选手需要对开发板以及提供的Firmware.zip进行分析,回答主办方提出的问题
由于没有对题目进行截图保留,所以凭借个人以及队友仅存的记忆对赛题进行了整理:
比赛过程中也提供了几个公告:
(1)比赛提供的Firmware.bin为不完整固件请选手不要尝试将其烧录进开发板
(2)mqtt服务存在rce
(3)mqtt的用户名为xhlj2024
(ps:公告(1)是笔者当时修改root密码重打包尝试烧录后放出的XD)
板子上有个相对比较明显的丝印"PORST",用万用表测试一下它的常态为高电平:
用公对公的杜邦线将它接地后观察开发板成功重置
可以从两个地方两个地方获取flash的型号
首先从miscro usb的串口输出中得知flash型号为W25Q256FV:
然后通过读板子上的丝印也能获取flash型号为w25q256jveq:
发现这俩居然还不一样,询问主办方说以丝印为准
查阅官方手册可以获取flash的信息,包括flash容量大小:
在板子重启的时候按4进入uboot shell,并进行printenv查看boot时候的一些参数
其中就有波特率的信息,为57600
everthing查找一下"8888"发现在nginx.conf里存在:
发现是对1883也就是mqtt服务的端口转发
(4)中查找到了mqtt服务,首先默认这个xxx端口为mqtt的8888,寻找一下mqtt有没有密码
在/etc/mosquitto.conf文件中找到的mqtt的密码文件/etc/pwfile
其中的内容为:
(ps:这里的内容是赛后对板子上运行的固件dump下来的内容,并非主办方一开始提供的firmware.bin里的内容)
询问misc佬ymnh得知这个是经过sha-512然后base64加盐编码后的内容,密码是7位数字的话写过个爆破脚本就很轻易的能得出答案了
爆破脚本:
输出:
所以mqtt的登陆密码为2758934
到了这里我们再通过给出的固件进行静态分析是很难找到题目6~8的答案的,包括(5)倘若没有主办方的提示,我们也需要对板子进行固件提取获取真实完整的固件才能爆破出答案
总而言之,如果想进一步的解题目前我们还是需要getshell,有两种思路:
(1)通过硬件手段对板子进行固件的dump,修改固件的root密码然后通过uboot的tftp烧写上去从而获得root的shell
(2)通过一些服务的漏洞来RCE
通过放大镜可以看出这是wson8的flash,可以通过wson8的芯片夹和ch341编程器进行固件提取
板子上也有和flash有关的引脚以及3v3的测试点,也可以飞八根线出来连到ch341编程器或者树莓派上进行固件提取:
但无论哪种方式提出来的固件用binwalk解压貌似不太完整。。。赛后和天枢的badmonkey师傅交流也说提出的固件不太完整,很疑惑不知道为什么,用我的树莓派4b甚至只能读到芯片型号但提不出固件
同时板子上也有jtag口:
猜测是连接mcu内部flash的,但是它是一个6pin的非标准jtag,没有jtagulator去读一下的话很难看出哪个是哪个,(但笔者没有jtagulatorQAQ)于是就很难通过jlink进行读取测试
所以就暂时放弃dump固件这一条道路,转去研究RCE
比赛过程中主办方给出了mqtt存在rce的提示,但当时把八根线飞出来还没提出固件比赛已经濒临结束了。。于是只能赛后进行一波研究
赛后比赛方提供了root用户的ssh密码,于是能够连上去查找和mqtt相关的进程:
通过和github上的源码进行比对,发现mosquitto的逆向代码和源码差不大多,但mosquitto_sub不太一样,而且它指定了"block"和"logs"这两个topic,感觉就是出题人自己实现的一个进程
通过ida逆向可以找到和这两个topic相关的伪代码:
可以看出发送消息的JSON格式为:
而且如果往"logs"这个topic进行publish操作,就会调用system在"/var/log"目录生成和给定的时间戳相关的文件,看上去可以通过构造特殊的时间戳进行命令注入
但"sub_4100dc"这个函数对时间戳进行了格式的校验:
具体的时间戳应该是这样的格式:
也就是说很难通过时间戳进行注入
再看后面和info相关的处理:
可以发现info被进行了两轮操作变成info2,然后对info2有一个检验,通过后info2就被当作参数传入snprintf并最终被命令执行。如果最终的info2是'"\n/bin/sh\n'这种类型的字符串的话,就能实现命令注入
要确定是否能注入,我们需要逆向出info变成info1和info1变成info2进行了什么操作
在逆向大爹void的帮助下,成功恢复了几个关键函数的符号表:
(ps:其中的SM4Crypt经尝试为SM4的解密过程)
filter的过滤:
但没有过滤"\n",还是有被注入的可能
倒推一下,输入的info应该是如'"\n/bin/sh\n'这样的字符串先进行sm4加密然后进行base64编码后的过程
值得注意的是,info1也就是输入info的base64解码结果的第一个byte位是控制位ctrl,为了使得ctrl满足要求且后面的memcpy能复制尽可能长的内容,ctrl应该为0xBF
所以构造的info应该是注入字符串进行sm4加密,然后头部加上0xbf再做base64编码后的内容
这里我选择的是通过wget获取msf生成的反弹到宿主6666端口的可执行文件,然后chmod +x后执行的RCE方法,实际测试注入的字符串长度上限为0x20,所以选择了下面三个命令:
RCE脚本:
RCE效果:
rce后可以登陆上去执行"netstat -pantu"查看端口和对应的进程:
可以看出5679是network这两个进程
对于(6)和(8)这两个题目,感觉是偏流量分析的题目,就可能传一个tcpdump上去捕获一下流量进行取证,不是笔者太擅长的内容,就浅尝辄止了
从复盘整个过程下来,今年IOT的题目对硬件和固件的层面分析都有涉及,但短时间能RCE感觉还是很有难度,而且容易陷入提不出固件又RCE不了的尴尬局面(也是笔者当时遇到的情况2333)但总而言之整个复盘下来还是收获颇丰
xhlj2024:$6$pvCQQygWXp04MJao$aYIMt03T2goWCa6JLX7QY6/p4C3lUzGZIUueePaHibbiihShFGufRHXzhCEeVGW4u7o39DCUEeTPASKH/0N6mQ==
xhlj2024:$6$pvCQQygWXp04MJao$aYIMt03T2goWCa6JLX7QY6/p4C3lUzGZIUueePaHibbiihShFGufRHXzhCEeVGW4u7o39DCUEeTPASKH/0N6mQ==
import hashlib
import base64
# 目标哈希值(Base64解码前的字符串)
target_hash_base64 = "aYIMt03T2goWCa6JLX7QY6/p4C3lUzGZIUueePaHibbiihShFGufRHXzhCEeVGW4u7o39DCUEeTPASKH/0N6mQ=="
# 将Base64编码的目标哈希值解码为十六进制
target_hash = base64.b64decode(target_hash_base64).hex()
print(target_hash)
# 盐值
salt = base64.b64decode("pvCQQygWXp04MJao")
# 哈希函数,这里使用SHA-512,并且考虑了盐值
def hash_password(password, salt):
return hashlib.sha512((password).encode()+salt).hexdigest()
print(hash_password(str('0123456').rjust(7,'0'), salt))
# 尝试所有7位数字的密码
for password in range(0, 10000000):
if hash_password(str(password).rj ust(7,'0'), salt) == target_hash:
print(f"找到密码:{password}")
break
else:
print("没有找到匹配的密码。")
import hashlib
import base64
# 目标哈希值(Base64解码前的字符串)
target_hash_base64 = "aYIMt03T2goWCa6JLX7QY6/p4C3lUzGZIUueePaHibbiihShFGufRHXzhCEeVGW4u7o39DCUEeTPASKH/0N6mQ=="
# 将Base64编码的目标哈希值解码为十六进制
target_hash = base64.b64decode(target_hash_base64).hex()
print(target_hash)
# 盐值
salt = base64.b64decode("pvCQQygWXp04MJao")
# 哈希函数,这里使用SHA-512,并且考虑了盐值
def hash_password(password, salt):
return hashlib.sha512((password).encode()+salt).hexdigest()
print(hash_password(str('0123456').rjust(7,'0'), salt))
# 尝试所有7位数字的密码
for password in range(0, 10000000):
if hash_password(str(password).rj ust(7,'0'), salt) == target_hash:
print(f"找到密码:{password}")
break
else:
print("没有找到匹配的密码。")
{"log":1,"timestamp":"xxxx","info":"xxxx"}
{"log":1,"timestamp":"xxxx","info":"xxxx"}
aa-bb-cc:dd:ee
aa为标准月份格式
bb为标准天数格式
cc dd ee分别为标准时 分 秒格式
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课