前段時間 ecology 密集發(fā)布了一系列補丁,修復(fù)幾個筆者之前儲備的 0day,本文就來介紹其中一個列比較有意思的,以及分享一下相關(guān)的挖掘思路。
背景
最近幾個月筆者都在研究 Java Web 方向,一方面是工作職責(zé)的調(diào)整,另一方面也想挑戰(zhàn)一下新的領(lǐng)域。對于漏洞挖掘而言,選擇一個具體目標(biāo)是非常重要的,經(jīng)過一段時間供應(yīng)鏈和生態(tài)的學(xué)習(xí)以及同事建議,兼顧漏洞挖掘難度和實戰(zhàn)效果選擇了 ecology OA作為第一個漏洞挖掘的目標(biāo)。
代碼審計
雖然本文介紹的是 Fuzzing,但之前也說過很多次,自動化漏洞挖掘只能作為一種輔助手段,是基于自身對代碼結(jié)構(gòu)的理解基礎(chǔ)上的提效方式。
在真正開始挖漏洞之前,筆者花了好幾周的時間去熟悉目標(biāo)的代碼,并且對一些不清晰的動態(tài)調(diào)用去進(jìn)行運行時分析,最終才能在 20G 代碼之中梳理出大致的鑒權(quán)和路由流程。
通過分析 JavaEE 應(yīng)用注冊的路由,注意到其中一個映射:
ServletMapping[url-pattern=/services/*,name=XFireServlet]^/services(?=/)|^/servicesz]
其對應(yīng)的類為org.codehaus.xfire.transport.http.XFireConfigurableServlet
:
<servlet>
<servlet-name>XFireServletservlet-name>
<display-name>XFireServletdisplay-name>
<servlet-class>org.codehaus.xfire.transport.http.XFireConfigurableServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>XFireServletservlet-name>
<url-pattern>/services/*url-pattern>
servlet-mapping>
XFire 考古
XFire[1]并不是 ecology 自己的業(yè)務(wù)代碼,而是一個 SOAP Web 服務(wù)框架,它是作為 Apache Axis 的有效替代方案而開發(fā)的。除了通過使用 StAX 實現(xiàn)良好性能的目標(biāo)外,XFire 的目標(biāo)還包括通過各種插件機制實現(xiàn)靈活性,API 的直觀操作以及與通用標(biāo)準(zhǔn)的兼容性。此外 XFire 非常適合集成到基于 Spring Framework 的項目中。
值得一提的是,XFire 目前已經(jīng)不再進(jìn)行開發(fā),其官方繼任者是Apache CXF[2]。
XFire 的用法比較簡單,首先在META-INF/xfire/services.xml
中定義需要導(dǎo)出的服務(wù),比如:
"1.0"encoding="UTF-8"?>
<beansxmlns="http://xfire.codehaus.org/config/1.0">
<service>
<name>WorkflowServicename>
<namespace>webservices.services.weaver.com.cnnamespace>
<serviceClass>weaver.workflow.webservices.WorkflowServiceserviceClass>
<implementationClass>weaver.workflow.webservices.WorkflowServiceImplimplementationClass>
<serviceFactory>org.codehaus.xfire.annotations.AnnotationServiceFactoryserviceFactory>
service>
beans>
這樣weaver.workflow.webservices.WorkflowService
就被認(rèn)為是導(dǎo)出服務(wù)。
可以直接被客戶端進(jìn)行調(diào)用。調(diào)用方式主要是通過 SOAP 請求到 XFireServlet,例如調(diào)用上述服務(wù)可以發(fā)送 POST 請求到/services/WorkflowService
:
"1.0"encoding="UTF-8"?>
<Body>
<getUserId>
<p>evilpanp>
<p>2333p>
getUserId>
Body>
表示以指定參數(shù)調(diào)用服務(wù)的getUserId
方法。
SQL 注入
接下來回到漏洞本身,WorkflowService 服務(wù)的具體實現(xiàn)為WorkflowServiceImpl
,例如其中的 getUserId 就是服務(wù)導(dǎo)出的一個方法,其具體實現(xiàn)為:
@Override
publicStringgetUserId(Stringvar1,Stringvar2){
if(Util.null2String(var2).equals("")){
return"-1";
}elseif(Util.null2String(var1).equals("")){
return"-2";
}else{
RecordSetvar3=newRecordSet();
var3.executeQuery("selectidfromHrmResourcewhere"+var1+"=?andstatus<4?",var2);
return!var3.next()?"0":Util.null2s(var3.getString("id"),"0");
}
}
可以看到,一個教科書式的 SQL 注入就已經(jīng)找到了。
Service 鑒權(quán)
現(xiàn)在漏洞點找到了,觸發(fā)路徑也找到了,可實際測試的時候發(fā)現(xiàn)這個接口有些特殊的鑒權(quán),其鑒權(quán)邏輯為判斷請求地址是否是內(nèi)網(wǎng)地址,如果是的話就放行。
考慮到很多系統(tǒng)是集群部署的,且前面有一層或者多層負(fù)載均衡,因此實際請求服務(wù)的可能是經(jīng)過反向代理的請求,此時客戶端的真實 IP 只能通過X-Forward-For
等頭部獲取。
這本來無可厚非,但是 HTTP 請求頭是可以被攻擊者任意設(shè)置的,因此 ecology 在此基礎(chǔ)上進(jìn)行了復(fù)雜的過濾,精簡后的偽代碼如下:
privateStringgetRemoteAddrProxy(){
Stringip=null;
StringtmpIp=null;
tmpIp=this.getRealIp(this.request.getHeaders("RemoteIp"),ipList);
if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=tmpIp;
}
booleanisInternalIp=IpUtils.internalIp(ip);
if(isInternalIp){
ipList.add(this.request.getRemoteAddr());
tmpIp=IpUtils.getRealIp(ipList);
if(!IpUtils.internalIp(tmpIp)){
ip=tmpIp;
}
}
returnip!=null&&ip.length()!=0&&!"unknown".equalsIgnoreCase(ip)?ip:null;
}
IpUtils#internalIp
的判斷更為復(fù)雜,連byte[]
都出來了:
publicstaticbooleaninternalIp(Stringip){
if(ip!=null&&!ip.equals("127.0.0.1")&&!ip.equals("::1")&&!ip.equals("0000:1")){
if(ip.indexOf(":")!=-1&&ip.indexOf(":")==ip.lastIndexOf(":")){
ip=ip.substring(0,ip.indexOf(":"));
}
byte[]addr=(byte[])null;
if(isIpV4(ip)){
addr=textToNumericFormatV4(ip.trim());
}else{
addr=textToNumericFormatV6(ip.trim());
}
returnaddr==null?false:internalIp(addr);
}else{
returntrue;
}
}
publicstaticbooleaninternalIp(byte[]addr){
byteb0=addr[0];
byteb1=addr[1];
byteSECTION_1=true;
byteSECTION_2=true;
byteSECTION_3=true;
byteSECTION_4=true;
byteSECTION_5=true;
byteSECTION_6=true;
switch(b0){
case-84:
if(b1>=16&&b1<=?31){
returntrue;
}
case-64:
switch(b1){
case-88:
returntrue;
}
default:
returnfalse;
case10:
returntrue;
}
}
其邏輯是對 getRemoteAddrProxy 取出來的 IP,如果路徑匹配webserviceList
且 IP 匹配webserviceIpList
前綴,就認(rèn)為是內(nèi)網(wǎng)地址的請求從而進(jìn)行放過:
webserviceList=[
"/online/syncOnlineData.jsp",
"/services/",
"/system/MobileLicenseOperation.jsp",
"/system/PluginLicenseOperation.jsp",
"/system/InPluginLicense.jsp",
"/system/InMobileLicense.jsp"
];
webserviceIpList=[
"localhost",
"127.0.0.1",
"192.168.",
"10.",
"172.16.",
"172.17.",
"172.18.",
"172.19.",
"172.20.",
"172.21.",
"172.22.",
"172.23.",
"172.24.",
"172.25.",
"172.26.",
"172.27.",
"172.28.",
"172.29.",
"172.30.",
"172.31."
]
根據(jù)上面的代碼,你能發(fā)現(xiàn)鑒權(quán)繞過的漏洞嗎?
Fuzzing
也許對代碼比較敏感的審計人員可以通過上述鑒權(quán)代碼很快發(fā)現(xiàn)問題,但說實話我一開始并沒有找到漏洞。于是我想到這個鑒權(quán)邏輯是否能單獨抽離出來使用 Fuzzing 的思路去進(jìn)行自動化測試。
之前發(fā)現(xiàn) Java 也有一個基于 libFuzzer 的模糊測試框架Jazzer[3],但是試用之后發(fā)現(xiàn)比較雞肋,因為和二進(jìn)制程序會自動 Crash 不同,Java 的 fuzz 需要自己指定 Sink,令其在觸達(dá)的時候拋出異常來構(gòu)造崩潰。
雖然說沒法發(fā)現(xiàn)通用的漏洞,但是對于現(xiàn)在這個場景來說正好是絕配,我們可以將目標(biāo)原始的鑒權(quán)代碼摳出來,然后在未授權(quán)通過的時候拋出一個異常即可。構(gòu)建的 Test Harness 代碼如下:
publicstaticvoidfuzzerTestOneInput(FuzzedDataProviderdata){
Stringpoc=data.consumeRemainingAsString();
fuzzIP(poc);
}
publicstaticvoidfuzzIP(Stringpoc){
if(containsNonPrintable(poc))return;
XssRequestWeblogicx=newXssRequestWeblogic();
Stringout=x.getRemoteAddr(poc);
booleancheck2=check2(out);
if(check2){
thrownewFuzzerSecurityIssueHigh("FoundIP["+poc+"]");
}
}
publicstaticbooleancheck2(Stringipstr){
for(Stringip:webserviceIpList){
if(ipstr.startsWith(ip)){
returntrue;
}
}
returnfalse;
}
其中精簡了一些 ecology 代碼中讀取配置相關(guān)的依賴,將無關(guān)的邏輯進(jìn)行手動剔除。
編譯好代碼后,使用以下命令開始進(jìn)行 fuzz:
$./jazzer--cp=target/Test-1.0-SNAPSHOT.jar--target_class=org.example.App
不多一會兒,就已經(jīng)有了一個成功的結(jié)果!
fuzz.png
可以看到圖中給出了127.0.0.10
這個 payload,可以觸發(fā) IP 鑒權(quán)的繞過!反過來分析這個 PoC,可以發(fā)現(xiàn)之所以能繞過是因為webserviceIpList
只檢查了前綴,而127.0.0.10
可以在internalIp
返回False
,即認(rèn)為不是內(nèi)部 IP,但實際上 webserviceIpList 卻認(rèn)為是內(nèi)部 IP,從而導(dǎo)致了繞過。
如果只是從代碼上去分析的話,可能一時半會并不一定能發(fā)現(xiàn)這個問題,可是通過 Fuzzing 在覆蓋率反饋的加持下,卻可以在幾秒鐘之內(nèi)找到正解,這也是人工審計無法比擬的。
漏洞補丁
通過 IP 的鑒權(quán)繞過和 XFire 組件的 SQL 注入,筆者實現(xiàn)了多套前臺的攻擊路徑,并且在 HW 中成功打入多個目標(biāo)。因為當(dāng)時提交的報告中帶了漏洞細(xì)節(jié),因此這個漏洞自然也就被官方修補了。如果沒有公開的話這個洞短期也不太會被撞到。
漏洞修復(fù)的關(guān)鍵補丁如下:
diff--gita/src/weaver/security/webcontainer/IpUtils.javab/src/weaver/security/webcontainer/IpUtils.java
index6b3d8efc..e7482511100644
---a/src/weaver/security/webcontainer/IpUtils.java
+++b/src/weaver/security/webcontainer/IpUtils.java
@@-48,12+48,16@@publicclassIpUtils{
}
publicstaticbooleaninternalIp(Stringip){
-if(ip!=null&&!ip.equals("127.0.0.1")&&!ip.equals("::1")&&!ip.equals("0000:1")){
+if(ip==null||ip.equals("127.0.0.1")||ip.equals("::1")||ip.equals("0000:1")){
+returntrue;
+}elseif(ip.startsWith("127.0.0.")){
+returntrue;
+}else{
if(ip.indexOf(":")!=-1&&ip.indexOf(":")==ip.lastIndexOf(":")){
ip=ip.substring(0,ip.indexOf(":"));
}
其中把 equals 換成了 startsWith,并且還過濾了我們之前使用的 WorkflowService 組件。當(dāng)然還是沿襲 ecology 一貫的漏洞修復(fù)原則,不改業(yè)務(wù)代碼,只增加安全校驗,這也是對歷史遺留問題的一種妥協(xié)吧。
總結(jié)
-
?對于 Java 這樣的內(nèi)存安全編程語言也是可以 fuzz 的,只不過目的是找出邏輯漏洞而不是內(nèi)存破壞;
-
?漏洞挖掘初期花時間投入到代碼審計中是有必要的,有助于理解項目整體結(jié)構(gòu)并在后期進(jìn)行針對性覆蓋;
-
?漏洞挖掘的時候重點關(guān)注邊界的系統(tǒng)和服務(wù),處于信任邊界之外的組件更有可能過于信任外部輸入導(dǎo)致安全問題;
-
?對于看起來很復(fù)雜的數(shù)據(jù)處理模塊,可以充分利用 Fuzzing 的優(yōu)勢,幫助我們快速找出畸形的 payload;
-
?模塊化 Fuzzing 的難點在于抽離代碼并構(gòu)建可編譯或者可以獨立運行的程序,即構(gòu)建 Test Harness,跑起來測試用例你就已經(jīng)成功了 90%;
-
?軟件開發(fā)和漏洞挖掘正好相反。開發(fā)者會出于厭惡情緒刻意避開復(fù)雜的歷史遺留代碼,而這些代碼卻是更可能出現(xiàn)問題的地方。因此安全研究員要學(xué)會克服自己的厭惡情緒,做到 —— “明知山有屎,偏向屎山行”。
-
自動化
+關(guān)注
關(guān)注
29文章
5620瀏覽量
79526 -
編程語言
+關(guān)注
關(guān)注
10文章
1950瀏覽量
34900 -
代碼
+關(guān)注
關(guān)注
30文章
4821瀏覽量
68893
原文標(biāo)題:總結(jié)
文章出處:【微信號:哆啦安全,微信公眾號:哆啦安全】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論