漏洞简介
CDPwn
系列漏洞是由来自Armis
的安全研究员在思科CDP(Cisco Discovery Protocol)
协议中发现的5个0 day
漏洞,影响的产品包括思科交换机、路由器、IP
电话以及摄像机等。其中,CVE-2020-3119
是NX-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
方式进行拷贝即可。
CDP数据包分析
为了便于后续的分析,需要先了解CDP
数据包的相关格式。在GNS3
中设备之间的链路上捕获流量,看到设备发送的CDP
数据包示例如下。
可以看到,除了开始的version
、ttl
和checksum
字段外,后面的每一部分都是典型的TLV(Type-Length-Value)
格式,Device ID
和Addresses
部分的字段明细如下。其中,在Addresses
部分,其Value
还有更细致的格式。
另外,python
scapy
模块支持CDP
协议,可以很方便地构造和发送CDP
数据包,示例如下。
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
后,将覆盖栈上的返回地址。
// 为方便理解, 对函数/变量进行了重命名
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
程序崩溃后会重启,因此需要通过爆破的方式来猜测程序相关的基地址。
$ 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。
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)
部分的数据进行解析时,由于缺乏对其内容长度的校验,造成栈溢出。
发表评论
您还未登录,请先登录。
登录