TP-Link SR20命令执行漏洞复现
前言
针对 TP-Link SR20
做漏洞复现的时候,学习到了 C
程序调用 lua
文件所造成的命令执行漏洞,同时该路由器还存在一个命令注入的点。TDDP
是 TP-LINK
在 UDP
通信基础上设计的一种调试协议,当 TP-Link SR20
运行了 V1
版本的 TDDP
协议,在无需认证的情况下,往 SR20
设备的 UDP 1040
端口发送特定数据就可以造成命令注入漏洞或利用 TFTP
服务下载指定 LUA
文件并以 root
权限将其执行。还好这个协议的逆向量挺小的 😶🌫️
从接收数据开始
用 qemu
用户级仿真情况如下,该进程会一直处于阻塞状态,猜测是由 select
函数造成的。
执行命令 sudo chroot . ./qemu-arm-static -g 1234 ./usr/bin/tddp
,用 gdb-multiarch
进行调试,发现断点下到 select
是可以直接 c
过来的
但是把断点下到 select
函数执行后,就发现是处于了阻塞状态(猜测 select
监视了某个文件一直没有读到数据)
再看 select
函数上面的代码,发现是有进行初始化操作的部分。在 v6[9]
存放的是 socket
函数创建完的套接字(sub_16E5c
函数中实现的)
分析一下 sub_16D68
函数,它的第一个形参是刚刚创建的套接字,第二个形参是 1040
通过下面的代码能分析出 bind
函数将套接字与 0.0.0.0:1040
这个地址和端口所绑定的
此时在主机上运行程序 tddp
后,虽然是阻塞状态,但可以发现 1040
端口已经被监听
这里的 v6[9]
是 socket
函数返回的文件描述符为 5
(找了半天也没看到文件描述符 4
是哪个文件)
timeout.tv_sec = 600; |
下面要分析的核心是 readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F)
这行代码,这个 readfds
里存放的是要检查可读文件描述符的集合。它是用每个 bit
来表示一个文件描述符,因为比特图 __fds_bit
的每个元素的类型都为 unsigned long
,其大小为 4
字节 32
位 bit
,所以理论上 _fds_bit
的每个图都可以表示 32
个文件描述符的状态。其 bit
为 1
就表示该文件描述符要检查,为 0
则表示不检查。
再来看这行代码
readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F) |
v6[9]
表示的是 socket
返回的那个文件描述符 5
,让这个文件描述符右移 5
,也就是除以 32
,拿到的是该文件描述符位于第几个图。5/32
为 0
,这表示它位于第一个图。再看右边的 1 << (v6[9] & 0x1F)
这个 v6[9] & 0x1F
部分,0x1F
的二进制为 11111
,用目标文件描述符和其进行与运算(这是为了避免文件描述符超过了一个图能表示的范围,算比特位属于哪个图由 __fds_bits[v6[9] >> 5]
来做,计算位于具体某个图的哪个比特位由 1 << (v6[9] & 0x1F)
来做 ),最后用 |
将图中的唯一比特位写入具体的一个图中。这里即使不理解也并不影响后面的分析,它就是把文件描述符 5
用 select
函数进行了是否可读的监视。设置的超时时间为 600
秒。
此时 socket
创建的套接字已经和 ip port
所绑定,只要向目标地址发送数据,就可以让 select
监视的可读文件描述符检测到有数据,从阻塞转到就绪,继续往下运行。执行到 sub_16418
,其调用了 recvfrom
函数,接收了发送给 1040
端口的数据包。
看下 recvfrom
函数执行前后,将远端发送的数据读入内存的过程
此时的 exp
如下
from socket import * |
代码里存放数据包的首地址也就是 a1+45083
,接下来就追踪这个数据,看看是否会存在漏洞。
根据功能码执行相应操作是在 handle
函数(已经过重命名),第一个 if
判断的是数据包的版本号,只要第一个字节是 1
就行。sub_15AD8
函数判断的是 *a1
是否为空,它在前面的函数中已经将其赋值为堆地址,所以也能正常通过第二个 if
的检查
handle
函数是用来执行数据包中各功能码对应的功能,switch case
语句处理的就是功能码对应的操作,红框中写的是 *(a1+45084)
,这说明数据包的第二字节代表功能码。漏洞位于 0x31
功能码对应的函数
漏洞所在
sub_A580
结合上图可知,v12
取了 tddp
数据包的首地址。然后在 if(*v12 == 1)
判断了数据包第一个字节(也就是 tddp
协议的版本号)是否为 Version1
,如果是,则让指针 v18
移动 12
个字节,反之移动 28
个字节。
在移动后的位置用 sscanf(v18, "%[^;];%s", s, v9)
函数对其进行 ;
分割,字符串中 ;
前面的部分放入 s
中,;
后面的部分放入 v9
中,随后触发 DIY_system
函数(已被重命名)
DIY_system
查看变量定义的位置,猜测 argv
v4
v5
v6
应该是一个指针数组,execve
函数的第二个参数就是指针数组(是 execve
执行程序的命令行参数),这里实际上为 execve("/bin/sh","sh -c xxx",0)
这个 xxx
也就是变量 s
,s
由 vsprintf
函数拼接而成,原始的数据为 sub_A580
函数中调用 sub_91DC("cd /tmp;tftp -gr %s %s &", s, v15)
进行传递的,此处我将 sub_91dc
函数重命名为了 DIY_system
函数
结论:控制
tddp
协议的版本号为Version1
,功能码为0x31
,在功能码后面填充10
个垃圾字符,然后输入任意命令以;
结尾。这里虽然无法使用命令分隔符;
,但是&&
和||
并没有进行过滤,依然可以造成命令注入。
我发现网上的相关资料都是直接用了 qemu
的系统级仿真,原本好奇为什么不先采用 qemu
的用户级仿真,为了方便我首先采用了 sudo chroot . ./qemu-arm-static -g 1234 ./usr/bin/tddp
的方式来进行调试程序。前面的流程都一切正常,但是会卡在 sub_15110
内的 lua_pcall
函数。不确定这是否因为用户级仿真单个程序所造成的原因,我采用的方法是用 set
命令改变了 pc
寄存器的值,跳过了一些可能造成干扰的函数,sub_15458
函数完成了刚开始的初始化操作后,直接将该函数返回(通过修改 pc
寄存器)。后续执行流就可以正常执行了 ,下图是正常走到 DIY_system
函数时各寄存器的状态
因为用户级仿真不支持多线程,所以后续在调用 fork
函数的时候,子进程是出不来的,导致没办法进入 if ( !pid )
,也就无法执行到 execve
函数。
搭建系统级仿真
所以为了最终验证 POC
,还得用 qemu
搭建系统级仿真,先下载这三个文件
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2 |
下面是 net.sh
脚本和 boot.sh
脚本,先执行 net.sh
配置出一个 qemu
的网络接口 tap0
,然后再执行 boot.sh
脚本启动 qemu
(大概要等待三分钟左右)
!/bin/sh |
sudo qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress \ |
然后把解压出来的文件系统给传到 qemu
中
sudo scp squashfs-root.tar.gz root@10.214.140.139:/root/ |
再传一个 gdbserver
进去,方便调试。 gdbserver下载链接
sudo scp gdbserver root@10.214.140.139:/root/ |
进到 qemu
里,用 chroot
在传入的文件系统里做一个隔离环境,就可以进行调试了
此时调试的过程就不再演示,上文提到 sub_15110
内的 lua_pcall
函数会造成用户级仿真的阻塞,在系统级仿真里面也没有出现问题了。
启动 tddp
服务后可以看到 1040
端口已经被监听(但是不知道为什么在宿主机用 nmap
扫描不出来)
EXP1
根据上文的分析,编写 EXP
如下
from socket import * |
攻击效果:
利用lua文件命令执行
上图中的 v15
为发送数据方 IP
(经下图调试可知),所以正常 DIY_system
函数会利用 tftp
从对应机器上下载一个文件(用户可控),并且该文件在后面被当做 lua
文件进行加载执行(由 lua_call
函数完成),执行的是文件中的 config_test
函数。
只需要构造一个 lua
文件,命名一个 config_test
函数,里面自定义命令执行的代码即可。因为后面执行了两次 lua_pushstring
函数,这意味着正常的 config_test
函数应该有两个参数。
EXP2
所以编写恶意 lua
脚本如下
function config_test(para1, para2) |
把 exp
改成这样
from socket import * |
最终验证需要安装 lua
同时还得配置 TFTP server
,具体细节可以参考 文章
攻击效果:
根据上图的信息可以发现是连续执行了三条命令,分别为 id
xxx
10.214.140.177
。第一个命令 id
很明显是 lua
脚本里写死的,而 xxx
和后面的 ip
,其实是 C
代码传给 lua
脚本中 config_test
的参数。第三个命令是 ip
不可控,但第二个命令是以数据包中 ;
为分隔符,后面的数据 xxx
依然可控,所以最终可以导致同时执行两个命令
报错与解决
binwalk 报错 No such file or directory: 7z
发现是 binwalk
依赖了 7z
,所以还得装一个 7z
。
我首先执行的命令 apt-get install p7zip
,执行完后发现用 binwalk
解压依然是同样的错误,执行 which 7z
,报错为 7z not found
。发现 7z
并没有被安装成功,经过一番搜索后应该安装 p7zip-full
,这样 7z
就被成功安装。 binwalk
正常执行
编译 C 程序报错
gcc -o call call.c -llua
报错:缺少头文件
解决方法:加上 -I
参数,让编译器在指定目录下寻找头文件
gcc -o call call.c -llua -I/home/zikh/Desktop/lua-5.3.5/src -llua
报错:ld
无法找到 -llua
解决方法:在加 -L
参数,在指定目录下寻找库文件
最终命令为 gcc -o call call.c -llua -I/home/zikh/Desktop/lua-5.3.5/src -L/home/zikh/Desktop/lua-5.3.5/src -llua -lm -ldl
成功运行 C
语言编写的 call
调用 lua
程序
tftp没有找到下载文件的目录
/ # tftp -g -r /demo.lua 10.214.140.177 |
我按照网上资料,也将配置文件的目录修改到了 /opt/ftp_dir
,并且也给了 777
的权限,重启服务后,依然报了上面的错误。
解决方法:猜测上传文件的目录和下载文件的目录是同一个,我在宿主机上用 tftp
上传了一个 1.txt
文件,再用 find
进行搜索(如下图)发现上传的文件位于 /srv/tftp
目录下
从 qemu
里下载这个目录下的文件是成功的
尾声
虽然文件系统里有自带的 nc
,但因为是简化版无法造成反弹的命令执行。winmt 师傅自己传入了一个完整版的 nc
进行了测试。命令执行的点到此结束了,因为我也不执着于这个反弹 shell
,就不再进行后续的操作了😶🌫️
参考文章
我的翻译–一个针对TP-Link调试协议(TDDP)漏洞挖掘的故事 - backahasten - 博客园 (cnblogs.com)