
CVE-2026-33701の技術的分析。OpenTelemetry Java Agent RMIインストルメンテーションにおける安全でないデシリアライゼーションの脆弱性であり、悪用条件、パッチの詳細、および緩和策を含む。
深刻度: 緊急 (CVSS v4.0: 9.3)
影響を受けるバージョン: opentelemetry-javaagent < 2.26.1
修正バージョン: 2.26.1 (2026年3月23日リリース)
OpenTelemetry は、現在 Java エコシステムで最も広く採用されている可観測性フレームワークの1つです。特に Java エージェントは、数千の本番サービスで、開発者がコードを変更することなく、トレーシング、メトリクス、ロギングのためにアプリケーションを自動的に計装するために使用されています。javaagent フラグとしてアタッチするだけで、すべてをバックグラウンドで実行してくれます。
CVE-2026-33701 が興味深いのは、脆弱性そのものだけでなく、それがどのようにして導入されたかという性質にあります。このエージェントは、RMI 計装の副作用として、カスタム RMI エンドポイントを静かに登録します。開発者はその存在を知りません。アプリケーションコードには含まれていません。設定したものでもありません。エージェントが自動的に配置したものであり、2.26.1 より前のバージョンでは、そのエンドポイントはフィルタを一切適用せずに受信データをデシリアライズしていました。
アプリケーションがすでに RMI または JMX ポートを公開していた場合、そのポートへのネットワークアクセスを持つ攻撃者は、エージェントのカスタムエンドポイントに細工されたシリアライズペイロードを送信し、JVM プロセスの権限でリモートコード実行 (RCE) を達成できる可能性があります。ただし、RCE には、アプリケーションのクラスパスに互換性のあるガジェットチェーンが存在する必要があります。これについては後述します。
OTel Java エージェントが JVM にアタッチすると、コンテキスト伝播のために RMI 呼び出しを計装します。その考え方は、RMI クライアントがリモートメソッドを呼び出すとき、エージェントが現在のトレースコンテキストをサーバー側に伝播し、スパンがサービス境界を越えて正しく接続されるようにするというものです。
これを実現するために、エージェントはハードコードされた ObjID を使用して独自のカスタム RMI オブジェクトを登録します。ContextPropagator.java 内:
public static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
この ObjID は、エージェントが RMI ランタイム内で自身のエンドポイントを識別する方法です。計装された RMI クライアントがサーバーに接続すると、まずサーバーにこの ObjID が登録されているかどうかを確認します。登録されていれば、コンテキスト伝播ペイロードを送信します。そうでなければ、コンテキスト伝播をスキップして通常の呼び出しを行います。
問題は、このエンドポイントが JVM RMI ランタイム内に登録されるため、アプリケーションが開いている RMI または JMX ポートと同じトランスポートを共有することです。アプリケーションに -Dcom.sun.management.jmxremote.port が設定されている場合、または独自の RMI サービスをエクスポートしている場合、エージェントのエンドポイントは、接続できる誰からでも同じポートで到達可能になります。
デシリアライゼーションは ContextPayload.java で発生します。2.26.1 より前のバージョンでは、read() メソッドは次のようになっていました:
@SuppressWarnings("BanSerializableRead") // fine
public static ContextPayload read(ObjectInput oi) throws IOException {
try {
Object object = oi.readObject();
if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) object;
return new ContextPayload(map);
}
} catch (ClassCastException | ClassNotFoundException ex) {
logger.log(FINE, "Error reading object", ex);
}
return null;
}
@SuppressWarnings("BanSerializableRead") アノテーションと // fine というコメントは、実は示唆的です。チームの誰かがこの問題を懸念事項として指摘し、抑制が追加され、コメントはそれを正当化するためのものでした。明らかに、それは問題ありませんでした。
シリアライゼーションフィルタなしの oi.readObject() は、古典的な安全でないデシリアライゼーションパターンです。Java のシリアライゼーションは、デシリアライゼーション中にクラスパス上の任意のクラスを喜んでインスタンス化します。攻撃者がシリアライズストリームを制御できる場合、どのクラスをどの順序でインスタンス化するかを選択できます。これがガジェットチェーン攻撃の基礎です。
コードはデシリアライゼーション後に instanceof Map をチェックしますが、その時点で被害はすでに発生しています。ガジェットチェーンは、型チェックが行われる前に、readObject() 呼び出し自体の中で実行されます。instanceof チェックは、セキュリティの観点からは完全に無関係です。
2.26.1 の修正は、デシリアライゼーションのアプローチを完全に置き換えます。Map オブジェクトをシリアライズして readObject() を呼び出す代わりに、新しい実装はコンテキストエントリをプリミティブ型として手動で読み取ります:
@Nullable
public static ContextPayload read(ObjectInput oi) throws IOException {
int size = oi.readInt();
if (size > MAX_CONTEXT_ENTRIES) {
logger.log(
FINE,
"RMI context propagation payload size {0} exceeds maximum allowed of {1}, skipping context propagation.",
new Object[] {size, MAX_CONTEXT_ENTRIES});
return null;
}
Map<String, String> map = new HashMap<>();
for (int i = 0; i < size; i++) {
String key = oi.readUTF();
String value = oi.readUTF();
map.put(key, value);
}
return new ContextPayload(map);
}
書き込み側もそれに合わせて更新されました:
public void write(ObjectOutput out) throws IOException {
int size = context.size();
if (size > MAX_CONTEXT_ENTRIES) {
out.writeInt(0);
return;
}
out.writeInt(size);
for (Map.Entry<String, String> entry : context.entrySet()) {
out.writeUTF(entry.getKey());
out.writeUTF(entry.getValue());
}
}
このアプローチは、ストリームからプリミティブ型 (int と UTF 文字列) のみを読み取ります。オブジェクトのインスタンス化は発生しません。readInt() や readUTF() を通じてガジェットチェーンが実行されることはありません。この修正は正しく、完全です。
もう1つの変更点も注目に値します。エージェントのカスタムエンドポイントを識別するために使用される ObjID がバージョン管理されました:
// Before (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// After (2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
これは、パッチ適用済みエージェントが古い ObjID にはまったく応答しないことを意味します。古いエンドポイントを標的にする攻撃者は、パッチ適用済みサーバーから応答を得られません。これは、デシリアライゼーション修正に加えての優れた追加の強化策であり、たとえ何らかの方法でデシリアライゼーションがまだ到達可能であったとしても、v1 エンドポイントに対して構築されたエクスプロイトを効果的に無効化します。
これを悪用可能にするには、次の3つの条件がすべて同時に真である必要があります:
1. JDK 16 以下の OpenTelemetry Java エージェントがアタッチされている
これが最も重要な制約です。JDK 17 では RMI 内部に大幅な変更が導入され、Java Platform Module System によるより厳格なモジュールカプセル化が適用されます。Java デシリアライゼーションに対して機能するほとんどのガジェットチェーンは、リフレクションを使用して JDK 内部クラスにアクセスしたり、通常はアクセスできないメソッドを呼び出したりすることに依存しています。JDK 17 は、強力なカプセル化によりこれらのリフレクションパスをデフォルトで無効化するため、既知のガジェットチェーンの大部分は JDK 17 以降では単純に機能しません。
ただし、JDK 8、11、16 では、リフレクションアクセスははるかに寛容で、ガジェットチェーンは期待どおりに機能します。これらの JDK バージョンは、特に古いエンタープライズ環境では、依然として本番 Java デプロイメントのかなりの部分を占めています。
2. RMI または JMX ポートが攻撃者からネットワーク到達可能である
アプリケーションには -Dcom.sun.management.jmxremote.port=9010 のようなものが設定されているか、独自の RMI サービスをエクスポートしている必要があります。OTel エージェントのエンドポイントは、すでに使用されている RMI トランスポートに便乗します。RMI ポートが開いていない場合、エンドポイントは到達可能ではありません。
3. ガジェットチェーン互換のライブラリがアプリケーションのクラスパスに存在する
エージェント jar 自体には、ガジェットチェーン互換のクラスは含まれていません。これは、2.26.0 エージェント jar から抽出したクラスリストを検査して確認しました。エージェントには、OTel 計装コード、シャードされた API クラス、semconv 定義、ブートストラップインフラストラクチャが含まれています。Commons Collections、Commons BeanUtils、Spring Framework クラスなどの一般的に悪用されるガジェットソースは、エージェント内にバンドルされていません。
これは、ガジェットチェーンがアプリケーションから提供される必要があることを意味します。開発者は、悪用可能なシリアライゼーションガジェットを含むライブラリを含める必要があります。つまり、悪用可能性はアプリケーションが依存するものによって異なります。
上記の3つの条件は制限的に聞こえるかもしれませんが、実際のエンタープライズ Java 環境では、これらが一緒に発生することは非常に一般的です。典型的な本番シナリオを考えてみましょう: JDK 11 または JDK 17 (コンテナ化されたデプロイメントで一般的であり、ガジェットチェーンを部分的に再び有効にできる --add-opens フラグ付き) で実行されているバックエンドサービス、可観測性のために OTel エージェントで計装され、運用監視のために JMX が有効化され、ガジェット互換クラスを含むのに十分な依存関係ツリーを持つもの。
重要な洞察は、開発者が可観測性エージェントを受動的なインフラストラクチャとして信頼していることです。彼らはエージェントが観測することを期待しており、新しいネットワーク到達可能なエンドポイントを開くことは期待していません。ここで作成された攻撃面は、アプリケーション開発者の視点からは見えません。彼らは RMI コードを書いていません。RMI エンドポイントを設定していません。エージェントが計装の結果として静かに実行したのです。
ガジェットチェーンの要件に関するさらなる調査のために、ysoserial プロジェクト (公開されており、学術的および専門的なセキュリティ研究で広く参照されています) は、このクラスの脆弱性に関連するいくつかの Java デシリアライゼーションガジェットチェーンを文書化しています。CommonsCollections、Spring1、Spring2 などのチェーンは、既存のライブラリクラスを連鎖させて安全でないデシリアライゼーションを通じてコード実行を達成する方法の、よく文書化された例です。特定の環境でどのチェーンが適用可能かは、そのアプリケーションがクラスパスに持っているライブラリに完全に依存します。
opentelemetry-javaagent バージョン 2.26.1 以降にアップグレードしてください。これが唯一の完全な修正です。
即時のアップグレードが不可能な場合、JVM 起動フラグに次のシステムプロパティを追加することで、RMI 計装を完全に無効化できます:
-Dotel.instrumentation.rmi.enabled=false
これにより、脆弱なエンドポイントを登録する計装が無効化され、攻撃面が完全に除去されます。このフラグが設定されている間は、RMI 呼び出しをまたぐコンテキスト伝播は機能しませんが、一時的な緩和策としては許容できます。
この CVE とは関係なく、JMX が認証なしでネットワーク到達可能なポートに公開されている場合、OTel エージェントの有無に関係なく、重大な設定ミスとして扱う必要があります。