CDPwn系列之CVE-2020-3119分析

漏洞简介

CDPwn系列漏洞是由来自Armis的安全研究员在思科CDP(Cisco Discovery Protocol)协议中发现的5个0 day漏洞,影响的产品包括思科交换机、路由器、IP电话以及摄像机等。其中,CVE-2020-3119NX-OS系统中存在的一个栈溢出漏洞,利用该漏洞可在受影响的设备(如Nexus系列交换机)上实现任意代码执行,如修改Nexus交换机的配置以穿越VLAN等。

下面借助GNS3软件搭建Nexus交换机仿真环境,来对该漏洞进行分析。

环境准备

根据漏洞公告,选取Nexus 9000 Series Switches in standalone NX-OS mode作为分析目标,获取到对应的镜像如nxosv.9.2.2.qcow2后,根据GNS3提供的Cisco NX-OSv 9000 appliance中的模板进行操作即可。需要说明的是,

  • 与思科ASAV防火墙不同,模拟Nexus 9000系列交换机除了需要设备镜像外,还需要一个UEFI格式的启动文件;
  • 模拟Nexus 9000系列交换机对虚拟机的配置要求较高(8G内存),建议采用GNS3设备模板中的默认配置,降低配置的话可能导致设备无法启动。

设备启动后,建议连接到设备的Ethernet1/1口,之后对设备进行配置。

Nexus 9000系列交换机上,存在以下3种shell

  • vsh:正常配置设备时CLI界面的shell

  • guestshell:在vsh中运行guestshell命令后进入的shell,可以运行常见的shell命令;

  • bash shell:在vsh中运行run bash命令后进入的shell,可以查看底层系统中的文件,以及设备上的进程信息等;

    需要先在configure模式下,运行feature bash-shell开启bash shell

默认配置下,bash shell中是没有ip信息的。为了方便后续进行分析调试,需要给之前连接的Ethernet1/1口配置ip信息,根据mac地址查找对应的网口,然后配置对应的ip即可。

设备的mgmt口在bash shell下不存在对应的网口

另外,由于采用binwalk工具对设备镜像进行解压提取失败,因而直接通过bash shell拷贝设备文件系统中的文件:将公钥置于/root/.ssh/authorized_keys,然后通过scp方式进行拷贝即可。

Update:从qcow2文件中提取出对应的bin文件如nxos.9.2.2.bin,然后使用7z等工具直接对bin文件进行解压,即可提取出文件系统,其中包含cdpd等程序。

CDP数据包分析

为了便于后续的分析,需要先了解CDP数据包的相关格式。在GNS3中设备之间的链路上捕获流量,看到设备发送的CDP数据包示例如下。

可以看到,除了开始的versionttlchecksum字段外,后面的每一部分都是典型的TLV(Type-Length-Value)格式,Device IDAddresses部分的字段明细如下。其中,在Addresses部分,其Value还有更细致的格式。

另外,python scapy模块支持CDP协议,可以很方便地构造和发送CDP数据包,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from scapy.contrib import cdp
from scapy.all import Dot3, LLC, SNAP, sendp

ethernet = Dot3(dst="01:00:0c:cc:cc:cc")
llc = LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/SNAP()
# Cisco Discovery Protocol
cdp_header = cdp.CDPv2_HDR(vers=2, ttl=180)
deviceid = cdp.CDPMsgDeviceID(val='nxos922(97RROM91ST3)')
portid = cdp.CDPMsgPortID(iface="br0")
address = cdp.CDPMsgAddr(naddr=1, addr=cdp.CDPAddrRecordIPv4(addr="192.168.110.130"))
cap = cdp.CDPMsgCapabilities(cap=1)
power_req = cdp.CDPMsgUnknown19(val="aaaa"+"bbbb")
power_level = cdp.CDPMsgPower(power=16)
cdp_packet = cdp_header/deviceid/portid/address/cap/power_req/power_level

sendp(ethernet/llc/cdp_packet, iface="ens36")

漏洞分析

根据Armis技术报告可知,该漏洞存在于程序/isan/bin/cdpd中的函数cdpd_poe_handle_pwr_tlvs()里,其主要功能是对Power Request(type=0x19)部分的数据进行解析和处理,该部分的协议格式示例如下。

函数cdpd_poe_handle_pwr_tlvs()的部分伪代码如下,其中,cdp_payload_pwr_req_ptr指向Power Request(type=0x19)部分的起始处。可以看到,首先在(1)处获取到Length字段的值,在(2)处计算得到Power Requested字段的个数(Type + Length + Request-ID + Management-ID为8字节,Power Requested字段每项为4字节),之后在(4)处将每个Power Requested字段的值保存到v35指向的内存空间中。由于v35指向的内存区域为栈(在(3)处获取局部变量的地址,其距离ebp的大小为0x40),而循环的次数外部可控,因此当Power Requested字段的个数超过0x11后,将覆盖栈上的返回地址。

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
// 为方便理解, 对函数/变量进行了重命名
char __cdecl cdpd_poe_handle_pwr_tlvs(int *a1, int cdp_payload_pwr_cons_ptr, _WORD *cdp_payload_pwr_req_ptr)
{
v32 = *a1;
result = cdp_payload_pwr_cons_ptr == 0;
v33 = cdp_payload_pwr_req_ptr == 0;
if ( cdp_payload_pwr_cons_ptr || !v33 )
{
v28 = a1 + 300;
if ( v33 && cdp_payload_pwr_cons_ptr ) // version 1
{
// ...
}
if ( !result && !v33 ) // version 2
{
// ...
cdp_payload_pwr_req_len_field = __ROR2__(cdp_payload_pwr_req_ptr[1], 8); // (1)
cdp_payload_pwr_req_req_id = __ROR2__(cdp_payload_pwr_req_ptr[2], 8);
cdp_payload_pwr_req_mgmt_id = __ROR2__(cdp_payload_pwr_req_ptr[3], 8);
// ...
v8 = cdp_payload_pwr_req_len_field - 8;
if ( v8 < 0 )
v8 = cdp_payload_pwr_req_len_field - 5;
cdp_payload_pwr_req_num_of_level = (unsigned int)v8 >> 2; // (2)
// ...
if ( (signed int)cdp_payload_pwr_req_num_of_level > 0 )
{
cdp_payload_pwr_req_level_ptr = (unsigned int *)(cdp_payload_pwr_req_ptr + 4);
v35 = &cdp_payload_pwr_cons_len_field; // (3) cdp_payload_pwr_cons_len_field: [ebp-0x40]
pwr_levels_count = 0;
do
{
*v35 = _byteswap_ulong(*cdp_payload_pwr_req_level_ptr); // (4)
// ...
a1[pwr_levels_count + 311] = *v35; // (5)
++cdp_payload_pwr_req_level_ptr;
++pwr_levels_count;
++v35;
}
while ( cdp_payload_pwr_req_num_of_level > pwr_levels_count ); // controllable
}
v9 = *((_WORD *)a1 + 604);
v10 = *((_WORD *)a1 + 602);
v11 = a1[303];
if ( cdp_payload_pwr_req_req_id != v9 || cdp_payload_pwr_req_mgmt_id != v10 ) // (6)
{
// ...
}

在后续进行漏洞利用时,由于在(5)处将v35指向的内存地址空间的内容保存到了a1[pwr_levels_count + 311]中,而该地址与函数cdpd_poe_handle_pwr_tlvs()的第一个参数有关,在覆盖栈上的返回地址之后也会覆盖该参数,因此需要构造一个合适的参数,使得(5)处不会崩溃。另外,还要保证(6)处的条件不成立,即执行else分支,否则在该函数返回前还会出现其他崩溃。

另外,cdpd程序启用的保护机制如下,同时设备上的ASLR等级为2。由于cdpd程序崩溃后会重启,因此需要通过爆破的方式来猜测程序相关的基地址。

1
2
3
4
5
6
7
$ checksec --file cdpd
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RPATH: b'/isan/lib/convert:/isan/lib:/isanboot/lib'

之后漏洞利用可以执行注入的shellcode,或者通过调用system()来执行自定义的shell命令。

通过/isan/bin/vsh可以执行设备配置界面中的命令,如system('/isan/bin/vsh -c "conf t ; username aaa password test123! role network-admin"执行成功后,会添加一个aaa的管理用户。

补丁分析

根据思科的漏洞公告,该漏洞在如下的版本中已修复。

7.0(3)I7(8)为例,函数cdpd_poe_handle_pwr_tlvs()的部分伪代码如下。可以看到,在(1)处增加了对Power Requested字段个数的判断,其最大值为10。

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
char __cdecl cdpd_poe_handle_pwr_tlvs(int *a1, int a2, _WORD *a3)
{
// ...
if ( !result && !v35 ) // version 2
{
// ...
v38 = __ROR2__(a3[1], 8);
v33 = __ROR2__(a3[2], 8);
v32 = __ROR2__(a3[3], 8);
// ...
v8 = v38 - 8;
if ( v8 < 0 )
v8 = v38 - 5;
v29 = (unsigned int)v8 >> 2;
if ( v29 <= 0xAu ) // (1)
{
// ...
}
else
{
// ...
v36 = 10; // (2)
v28 = 10;
v29 = 10;
}
v39 = 0;
do
{
v9 = *v37;
LOWORD(v9) = __ROR2__(*v37, 8);
v9 = __ROR4__(v9, 16);
LOWORD(v9) = __ROR2__(v9, 8);
v42[v39] = v9;
// ...
a1[v39 + 312] = v42[v39];
++v37;
++v39;
}
while ( v36 > v39 );
goto LABEL_78;
}
// ...
}

小结

  • 通过GNS3软件搭建设备的仿真环境,同时对该漏洞的形成原因进行了分析:在对Power Request(type=0x19)部分的数据进行解析时,由于缺乏对其内容长度的校验,造成栈溢出。

相关链接

  • CDPwn: 5 Zero-Days in Cisco Discovery Protocol
  • Cisco NX-OS Software Cisco Discovery Protocol Remote Code Execution Vulnerability
  • VIRTUALIZING A CISCO NEXUS 9K WITH GNS3
  • CVE-2020-3119 Cisco CDP 协议栈溢出漏洞分析

本文首发于安全客,文章链接:https://www.anquanke.com/post/id/209018