FilteredObjectInputStream 許可リストのバイパス(java.rmi.MarshalledObject 経由)Apache log4j2 の課題 #4255 を、公式 Log4j 2.26.1 アーティファクトを JDK 17 上でエンドツーエンドに再現する、自己完結型のコンテナ化ラボです。ポジティブコントロール、堅牢化されたオラクル、検証済みの緩和策を備えています。
⚠️ 責任ある利用について
このラボには、現在未パッチの Log4j の課題に対する実動するデシリアライゼーション RCE が含まれています(#4255 は執筆時点で OPEN /
waiting-for-maintainerであり、CVE は未割り当て)。これはお使いのマシン上の使い捨て Docker コンテナ内でのみ実行され、外部とは Maven Central(公式 jar のダウンロードのみ)以外には接続しません。ターゲットへの接触は一切ありません。
- 元の報告者(U-Sec / Wujie Security)は、修正が行われるまで PoC を公開していません。これは、防御側の検証と検知エンジニアリングのために構築された独立した再現です。
- 所有していない、または明示的にテストを許可されていないシステムに対しては、これを実行しないでください。
- これはアプリケーション依存の RCE であり、普遍的な Log4j RCE ではありません(スコープを参照)。
Log4j の FilteredObjectInputStream(FOIS)は、resolveClass ベースのデシリアライゼーション許可リストです。その許可リストには java.rmi.MarshalledObject が含まれています。MarshalledObject はペイロードを不透明な byte[] として格納し、MarshalledObject.get() はそのペイロードを新規の、フィルタリングされていない ObjectInputStream でデシリアライズします。そのため、許可リストは内部のグラフを検査しません。
Log4j はこれを自らトリガーします。Log4jLogEvent$LogEventProxy(2.8 以降の LogEvent のシリアライズされたワイヤ形式)は、イベントの Message を MarshalledObject 内に保持し、デシリアライゼーション中に自動的に .get() を呼び出します(readResolve() → message())。したがって、FOIS を通じてシリアライズされた LogEvent を読み取るアプリケーションは、攻撃者のバイト列のフィルタリングされていないデシリアライゼーションを実行します。さらに、message() は結果の例外を飲み込んで SimpleMessage にフォールバックするため、受信側は無害なイベントをログに記録して動作を継続します。このエクスプロイトは静かに実行されます。
rel/2.26.1 で検証済み)| # | 場所 | 欠陥 |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES に java.rmi.MarshalledObject が含まれている |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | resolveClass() のみをオーバーライドしており、MarshalledObject の objBytes ペイロードはそれから見えない |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage は MarshalledObject<Message> である |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() が marshalledMessage.get()(フィルタなし)を呼び出し、すべての例外を飲み込む |
非推奨の log4j-samples の ObjectInputStreamLogEventBridge の忠実な代替品です。接続ごとに 1 つのシリアライズされた LogEvent を FOIS 経由で読み取る、認証なしの TCP レシーバーです。攻撃者は単一のシリアライズされたオブジェクトを送信します。オラクルは、レシーバーコンテナにのみバインドマウントされたディレクトリに書き込まれる証明ファイルであり、その出現は、デシリアライゼーションを介してレシーバー内でコードが実行されたことを証明します。
| # | シナリオ | 被害者のクラスパス | jdk.serialFilter | 期待される結果 |
|---|---|---|---|---|
| S1 | 許可されていないガジェットをトップレベルで送信 | + ガジェット | なし | 拒否 — FOIS が許可リストを強制 |
| S2 | 同じガジェットを LogEvent にラップ | + ガジェット | なし | rce — 自動トリガー(被害者側にガジェットクラスが必要) |
| S3 | 生の CommonsCollections6 をトップレベルで送信 | log4j + cc-3.2.1 | なし | 拒否 — FOIS が CC をブロック |
| S4 | CC6 を MarshalledObject にスプライス | log4j + cc-3.2.1 のみ | なし | rce — 被害者側に攻撃者クラスは不要 |
| S5 | S4 のペイロード | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | 拒否 — 緩和策 |
| S6 | S4 のペイロード | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | サイレント — 内部ストリームがフィルタを継承。CC6 は深すぎる |
| S7 | S4 のペイロード | log4j + cc-3.2.2 | なし | サイレント — 3.2.2 は安全でないファンクターのデシリアライゼーションを無効化 |
rce = コード実行。reject = FOIS が外部ストリームで例外をスロー。silent = 外部オブジェクトは処理されたが、コードは実行されなかった(より深い層でブロックされた、またはガジェットのバージョンが安全)。
./run.sh # または: make run
要件: Docker のみ(JDK は eclipse-temurin:17-jdk としてプルされます)。スクリプトは公式 jar をダウンロードし、使用前に Maven Central の SHA-1 に対して検証します。脆弱な範囲内の別の Log4j バージョンを指定するには、LOG4J_VERSION=2.20.0 ./run.sh を使用します。
LogEvent の単一の認証なし ~2.8 KB の TCP 書き込み。これは(Log4Shell とは異なり)文字列をログに記録させることでトリガーできるものではありません。ソケットブリッジに生のシリアライズされたバイト列が到達する必要があります。LogEvent を構築 → Log4j の writeReplace()/writeObject() が Message を MarshalledObject にラップ → ガジェットはその不透明な byte[] 内に隠れる → レシーバー側で readResolve() → message() → MarshalledObject.get() が新規のフィルタリングされていないストリームを開く → ガジェット → Runtime.exec。src/attacker/Attacker.java(poc2)は、純粋な CommonsCollections6 グラフを MarshalledObject の objBytes にスプライスするため、被害者側に攻撃者クラスは不要です。-Djdk.serialFilter='!java.rmi.MarshalledObject'(S5)。
注意: これにより、正当なシリアライズされた LogEventProxy オブジェクトもブロックされます(これらも MarshalledObject を使用します)。効果はありますが、シリアライズされたログ転送に対して透過的ではありません。maxdepth/maxbytes フィルタ。プロセス全体のフィルタは内部の MarshalledObject ストリームに伝播し、そこで深さがリセットされるため、maxdepth=5 は CC6 をブロックします(S6)が、浅いガジェットは通過します。チェーン深さに依存するため、境界ではありません。MarshalledObject を削除し、かつマーシャリングされたメッセージを Log4j のフィルタリングされた writeWrappedObject/readWrappedObject に移動する。LogEvent レシーバーを公開するアプリケーションかつクラスパスに使用可能なガジェットバージョンを持つアプリケーションが必要です。通常の Log4j デプロイメントはそのようなレシーバーを実行しません。net.server.TcpSocketServer は 2017 年に log4j-core から移動され、2.9.0 以降には存在しません)。現代のレシーバーはアプリケーション/サンプルコードであり、このラボはそれをモデル化しています。uid=0 はコンテナの root です。Docker エスケープはありません。RCE はレシーバープロセスとして実行されます。MarshalledObject が resolveClass フィルタを無効化すること)は既知の先行技術です。Apache の議論 #4168(「Log4j 2.x deserialization hardening」)を参照してください。Log4j 固有の自動トリガーが #4255 の貢献です。run.sh ポータブルランナー(チェックサム固定、堅牢化オラクル)
Makefile make build | run | clean
src/victim/Receiver.java FOIS ログレシーバー(ObjectInputStreamLogEventBridge の代替)
src/attacker/Attacker.java ペイロードビルダー: コントロール、PoC-1、PoC-2(CC6 + バイトスプライス)
src/attacker/EvilMessage.java PoC-1 自己完結型ガジェット
docs/RESULTS.md エビデンスマトリックス + 分析
脆弱性は U-Sec(Wujie Security) によって Apache log4j2 #4255 で報告されました。このリポジトリは、防御研究と検知エンジニアリングのための独立した再現/検証ラボです。