71's blog

宏願縱未了 奮鬥總不太晚

0%

cve-2021-34730

更新中… 11.1:还在调😅

Pre

漏洞信息:Cisco Small Business RV110W、RV130、RV130W 和 RV215W 路由器远程命令执行和拒绝服务漏洞

UART调试

上一篇 CVE-2020-3331复现 用的设备是 RV110W,在升级固件的过程中发现有的固件没有开启 telnet 服务,有的会默认开启 telnet 服务。应该是厂商在打补丁更新的时候加入了 telnet 服务,又刚好把哈希明文写si在固件代码中,所以才能在较新的版本中通过 telnet 后门远程连接。

但并非每台设备、每个版本都这样。搜索 RV130W ,几乎所有的 CVE 里都没提到过 telnet 后门。我往里面刷了几个不同版本的固件,发现都没有开启。也许是出于安全问题的考虑,没有加上 telnet 后门。

除了远程连接调试的方式外,还可以通过 UART 口进行调试。参考:

RV130W 的 UART 口不太好找,于是我拿 110W 的对比了下。上一篇博客提到过,130W 比 110W 的网更快、更稳定、更安全。下图中左边是 RV130W,右边是 RV110W。从两者用的芯片型号、大小以及整个电路板的设计可以直观地感受出区别。整体布局还是很相似的(RV130W的芯片分布是我根据印刷的型号判断的,正确与否还得拆除散热片进行验证,这点留到复现完再做),最终找到的 UART 口都是在闪存旁边。

虽然 130W 的 UART 口附近只标注了电阻名,但我们可以借助 标注详细的 110W 的 UART口来分析。以下是 RV110W 上的 UART 口,可以判断出它用的是 RS232 标准。焊上排针后,用万用表分别测 110W 和 130W 的 UART 口,发现它们的顺序是一样的,只是整体排列方向不同。调试的时候用 TX、RX、GND 就够了。

连接到 PC 上需要用 FT232。买的时候商家一般会发使用手册,里面有驱动和串口调试工具。我用的是 HyperTerminal ,putty也行,支持串口通讯的都行。受这篇文章影响,一开始选择了默认的波特率9600,一直接收到乱码。后来想起在通信原理课上用过这个软件,其实收发数据的过程也是完整的信号调制-传输-解调的过程,波特率不一致就会导致解调结果错误。因此逐个波特率尝试了一遍,最终试出了正确的数值 。

开机的时候会打印出一些配置信息。因此我先关闭电源,串口通信连接成功后(此时会接收到一堆 \x00),再接通电源,这样就能接收到完整的开机信息了。

38400

UPnP协议

参考:

SSDP协议是UPnP协议的核心,主要是完成了服务发现的工作。消息类型可以分为 m-search 和 notify。SSDP 协议是 UDP 传输实现的,设备端总是在 239.255.255.250:1900 这个组播地址上监听。因为是建立在无连接基础上的传输,消息总是会不断地重复发送,我本地抓包的时候发现确实如此。

假设我们现在有一台 PC(控制点)和路由器(UPnP设备)。当 UpnP 设备进入网络时,会广播发送 NOTIFY 信息给网络上的所有设备。当PC接入网络的时候,首先通过路由器的 DHCP 服务获得局域网内的一个 ip,接着发送 ssdp 的 m-search 消息,寻找 UPnP 设备。ST 字段指定了搜索设备类型,抓包过程中发现一般都有网关设备、根设备这几种类型。

为了理清楚这些包之间的关系,我们可以用 upnpy 运行对应的服务看看。以下是在 ubuntu 上运行的discover服务,然后在 VMnet8 网卡截取流量。

1
2
3
4
import upnpy
upnp=upnpy.UPnP()
devices=upnp.discover()
print(devices)

一开始是从 ens33 网卡的 ip 发出 discover 包的,寻找的是根设备。但我们的虚拟机用的是 NAT 模式,这个包最终会通过 VMnet8 虚拟网卡转发出来。可以看到后续发的包全是代理转发的包,加上了 UASER-AGENT 字段,ST也变了。

随后在WLAN网卡抓取到由它进一步转发的包,ST类型变化如下:

1
2
ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n
ST: urn:dial-multiscreen-org:service:dial:1\r\n

查看前面定义的 devices 变量,发现可用设备为空。因为我们的测试环境是在校园网下的,没有开启upnp服务,也收不到答应包。但是我连接了路由器的 LAN 接口后,发现 devices 还是空。在以太网网卡截取不到ssdp流量,倒是从WLAN网卡转发出去了,应该是没跟 192.168.1.1 建立起 socket 连接的原因。

因此我们需要手动发送 discover 的 socket 包到网关上测试一下。根据前面的测试来看,本机发送的ST字段应该是根设备。当这个包被代理转发后,ST字段才会改变。

这里的脚本我用的是前面参考链接中的脚本。把每个变量输出一下大概知道得到的是什么内容了。简而言之就是:

  • device:Device <(device name)>
  • device.get_services():service数组,有服务名和服务id两个元素,且名字相似。
  • service.get_actions():action数组,有每个服务对应的功能和需要的参数。
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
import upnpy
import socket
import requests
from upnpy.ssdp.SSDPDevice import SSDPDevice
msg = \
b'M-SEARCH * HTTP/1.1\r\n' \
b'HOST:239.255.255.250:1900\r\n' \
b'ST:upnp:rootdevice\r\n' \
b'MX:2\r\n' \
b'MAN:"ssdp:discover"\r\n' \
b'\r\n'

# Set up UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.bind((b"192.168.1.100",9999))
s.settimeout(2)
s.sendto(msg, (b'239.255.255.250', 1900))
addr = ('192.168.1.1', 1900)
data = b""

print("=== receive data ===")
try:
while True:
data, addr = s.recvfrom(65507)
print(addr,data)
except socket.timeout:
pass

device = SSDPDevice(addr, data.decode())#Device <RV130W>
services = device.get_services()
services_id = [services[i].id.split(":")[-1] for i in range(len(services))]

for id in services_id:
service = device[id]
actions = service.get_actions()
for action in actions:
for argument in action.arguments:
print(id,action.name,argument.name)

从运行结果可以看到UPnP设备能提供的服务,基本都是与端口映射、建立连接相关的。随后能抓到路由器发来的回复包,说明这台路由器就是根设备。

端口扫描

UPnP 漏洞影响严重的其中一个原因是很多路由器都默认开次该服务,思科官网针对停产设备提出的解决方案是用户禁用此服务。在 1.2.44 的版本下测试的时候,我发现 RV130W 是默认关闭此服务的。打开后,只是多了一个 444 端口,似乎并没有什么用。

1
2
3
4
5
6
7
8
9
10
11
$ nmap 192.168.1.1
Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-29 08:15 PDT
Nmap scan report for router751C7F (192.168.1.1)
Host is up (0.0088s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE
53/tcp open domain
80/tcp open http
81/tcp open hosts2-ns
443/tcp open https
444/tcp open snpp

尝试扫了所有端口,也没有结果。但是通过前面的流量分析可以肯定 UPnP 服务是开启的,找不到端口也没有关系。

漏洞程序

同一类路由器的设计思路可能是相似的,因此这里借助了《Cisco RV110w UPnP stack overflow》进行分析。

只开启了NX保护。从运行结果来看,程序用的是 uClibc 库。这个库可以通过buildroot交叉编译出来。

1
2
3
4
5
6
7
8
9
10
$ file upnp
upnp: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
$ checksec upnp
[*] Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
$ qemu-arm -L /usr/arm-linux-gnueabihf ./upnp
/lib/ld-uClibc.so.0: No such file or directory

IDA 打开后发现大部分函数都去了符号表。但是 RV110W 的 upnp 文件没有去除,可以用 bindiff 对比着还原些函数名(右键>import symbols)。

ida对下面这段地址的解析出现错误,无法进行反汇编。观察一下汇编指令,基本没什么问题,但是从PUSH指令开始解析失败。在这里右键 create function就可以了。

这段程序还原符号表后,应该是 parse_uri 函数,结尾调用了ssdp_msearch_response,看来是msearch包响应的处理程序。下面这段代码使用了危险函数 strcpy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 if ( !strcmp(device, "upnp:rootdevice") )
{
v7 = 2;
goto LABEL_14;
}
if ( !memcmp(device, "uuid:", 5u) )
{
strcpy(uuid, device + 5); // strcpy栈溢出
v7 = 3;
goto LABEL_14;
}
......
LABEL_14:
ssdp_msearch_response(socket, uuid, v7);

"upnp:rootdevice" 可以判断注入点在 ST 字段。但是传入 uuid 的时候,返回相应包之前会做一个检查。这里的 v11 具体是什么呢,我是用gdb调出来的。

1
2
3
4
5
6
7
case 2:
if ( v11[11] == (const char *)1 )
goto LABEL_7;
socket = strcmp(uuid, (const char *)v11 + 4);
if ( socket )
goto LABEL_7;
return ssdp_response(s, v11, 3);

漏洞利用

参考:

……

调试

(跟上一篇的步骤差不多)

先上传 gdbserver。

发现 netstat 也没有 -p 参数,从 busybox官网 下载对应的版本再上传。

1
2
3
# uname -a
Linux RV130W 2.6.31.1-cavm1 #1 Fri Jun 8 15:31:53 CST 2018 armv6l unknown
# ./busybox-armv6l netstat -pantu

测出偏移(160)

发短一些的数据时还是会崩,这次我们把断点下在strcpy函数 (0xb79c) 和 strcmp(0xb59c) .跟进分析找到了 strcmp 用的 id 号,为了通过检查把它写在payload头部,后面先用 ‘\x00’ 填充。

0x000B79C: strcpy

0x0000B764: call msearch

0x0000B59C: strcmp

0x0000B5B8: response

0x0000B770: return 0

发现这个设备跟 RV110W 一样,每个程序的 libc 基址都是固定的。所以我们同样地用 libc 的 gadget ,而不用程序本身的(地址含’\x00’,会被截断)。

image-20211103213837207

en 调着调着win10 不给连了,“收集到未知错误信息”emm。调之前应该想好怎么调,减少错误,不然一直 reboot(

利用

Unsolved

官网发布的漏洞解决方案提到,鼓励用户迁移到 RV132W、RV160 或 RV160W 路由器,那么他们在代码上有什么变化呢?是直接把 UPnP 功能删除了还是用其他函数替代危险函数呢?

7.6对bindiff的支持挺好。

130W有三块芯片用屏蔽或散热片封起来,尚未确定具体是什么芯片。

怕拆坏,复现完再拆。