
SolarWinds ARM CVE-2026-28326 向けの非破壊的なパッチ状態チェッカーで、TCP 55555 上の gRPC リスナーをプローブし、VULNERABLE、PATCHED、または INCONCLUSIVE を報告します。
SolarWinds Access Rights Manager (ARM) の未認証リモートコード実行脆弱性である CVE-2026-28326 に対する未認証のパッチ状態チェックです。この脆弱性は 2026-09-17 に
ARM 2026.2.1
で修正されました。CWE-798、CVSS 8.8 High
(CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)。Armadin の Kai Huang により SolarWinds に報告されました。
このベクターの AV:A は、TCP 55555 が隣接ネットワークからのみ到達可能であることを前提としており、これは ARM 自身のファイアウォールツールが想定している構成です。ポートがより広く開放されている環境では、このスコアはその環境の露出を過小評価します。
TCP 55555 上の ARM の gRPC リモーティングリスナーは、クライアント証明書でピアを認証します。未パッチのビルドは、すべてのインストールで同一のハードコードされたキーから派生したフォールバック資格情報も受け入れるため、この認証をバイパスし、安全でないデシリアライゼーションパスを露出させます。その結果、ARM サービスアカウントとしてコードが実行されます。このアカウントはデフォルトで LocalSystem として動作します。
このスクリプトは、各ターゲットに修正が存在するかどうかを報告します。認証インターセプターが返すステータスコードを読み取るだけで、キーの使用、認証、デシリアライゼーションパスへのデータ送信は行いません。VULNERABLE 以外の結果は、それ自体がターゲットがパッチ済みであることを示すものではありません。脆弱でない結果の解釈を参照してください。
# single target (port defaults to 55555)
./cve_2026_28326_check.py arm.example.com
# explicit port
./cve_2026_28326_check.py arm.example.com:55555
# several targets
./cve_2026_28326_check.py arm-a.example.com arm-b.example.com
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_28326_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_28326_check.py -f targets.txt --json > results.json
Python 3.8+、標準ライブラリのみ — サードパーティパッケージは不要です。
ツールはコレクターではなく ARM サーバーに対して実行してください。どちらのロールも 55555 で待ち受けますが、コレクターは修正の前後で同じように外部リクエストを拒否するため、ツールはそれを INCONCLUSIVE と報告します。
脆弱なサーバー (デフォルト出力。TTY では [!] マーカーと VULNERABLE が赤で表示されます):
$ ./cve_2026_28326_check.py arm.example.com
[!] arm.example.com:55555: VULNERABLE [unavailable-on-server-role]
Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)
パッチ済みサーバー。修正されたビルドは grpc-message を返し、ツールはそれを判定とともに出力します:
$ ./cve_2026_28326_check.py arm-dr.example.com
[+] arm-dr.example.com:55555: PATCHED [unauthenticated-gate]
Remoting/Send returned 16 UNAUTHENTICATED, the localhost and per-process-token gate introduced by the fix (>= 2026.2.1.7)
grpc-message: Client certificate not found. Please register your application first.
分類できないリスナー。コレクター、または外部リクエストを拒否するサーバーは、どちらのビルドでも両方のプローブに同じように応答します:
$ ./cve_2026_28326_check.py collector.example.com
[?] collector.example.com:55555: INCONCLUSIVE [external-requests-refused]
the listener refuses external requests, which a collector role and a restricted server both do identically on either build; scan the ARM server itself
--brief を使用した複数ターゲット:
$ ./cve_2026_28326_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE arm.example.com:55555 unavailable-on-server-role
PATCHED arm-dr.example.com:55555 unauthenticated-gate
INCONCLUSIVE collector.example.com:55555 external-requests-refused
UNAFFECTED db.example.com:55555 not-arm
ERROR offline.example.com:55555 unreachable
exit: 1
--json を使用した JSON 出力。判定を生の応答と照合できるよう、両方のプローブ結果が含まれます:
$ ./cve_2026_28326_check.py arm.example.com --json
[
{
"target": "arm.example.com:55555",
"verdict": "VULNERABLE",
"reason": "unavailable-on-server-role",
"detail": "Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)",
"grpc_message": "",
"probes": [
{
"path": "/SolarWinds_ARM_GRPC_ConnectionAuthrization.ConnectionAuthorization/GetCertificate",
"status": 0,
"message": "",
"alpn": "h2",
"note": ""
},
{
"path": "/SolarWinds_ARM_Remoting.Remoting/Send",
"status": 14,
"message": "",
"alpn": "h2",
"note": ""
}
]
}
]
このチェックは本番システムでの使用を想定しています:
Remoting/Send メッセージ本文内のシリアライズされたオブジェクトによって引き起こされます。両方のプローブは本文長ゼロの gRPC メッセージを送信し、インターセプターはメッセージ本文がディスパッチされる前に呼び出しを拒否します。GetCertificate プローブは空の登録コードを送信し、サーバーは保留中のコードを検索または削除する前にそれを拒否するため、このプローブは登録途中のクライアントの登録コードを消費しません。| # | 呼び出し | 目的 |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | エンドポイントが ARM であり、サーバーロールであることを確認する |
| 2 | Remoting/Send | パッチ状態のプローブ |
未パッチのサーバーでは、拒否された匿名リクエストはデバッグレベルでログに記録され
(Unable to validate remote request token)、ピアを名指しする情報行も記録されます。パッチ済み
ビルドは、ピアと上記の grpc-message で引用された登録ヒントを名指しする拒否をログに記録します。
どちらのビルドもセッションは記録しません。
露出ではなく悪用を探すには、ARM の NetworkConnectionEntry レコードを確認してください。
IsAuthenticated=false かつ ClientCertThumbPrint が設定されているエントリは、認証バイパスと
整合しており、通常のトラフィックでは発生しません。
この修正により、クライアント証明書を提示しないピアに対して認証インターセプターが返すステータスが
変更されました。パッチ前は、クライアント証明書チェックのすべての拒否パスが Unavailable を設定して
いました。パッチ後は、localhost とトークンのチェックが特徴的なメッセージとともに Unauthenticated を
設定し、そのパス上の他の何も 16 を返しません。2つの匿名呼び出しでビルドを区別します:
判定は Remoting/Send から読み取られます。インターセプターのステータスを伝播するのは
双方向ストリーミングと単項のハンドラのみであり、サーバーストリーミングとクライアントストリーミングの
ハンドラはどちらのビルドでもそれを上書きするためです。
このチェックはパッチ済みコードパスの挙動を観察するもので、バージョン文字列には依存しません。 パッチ済みサーバーがどのビルドを実行しているかは報告しません。
ARM リスナーは ALPN h2 をネゴシエートしない TLS 接続をリセットするため、ツールはそれを明示的に
設定します。ALPN なしで TLS 経由で接続するスキャナーは、そのポートからデータを受信しません。
VULNERABLE と PATCHED はそれぞれ、観察されたサーバー応答に基づいています。INCONCLUSIVE と
ERROR は、プローブがターゲットを分類できなかったことを意味するため、そのパッチ状態は不明です。
このため INCONCLUSIVE は PATCHED とは別に報告されます。
すべての判定には短い reason タグが付きます。--brief はそれを3列目として出力し、--json は
reason として含めます。
3つすべての INCONCLUSIVE の理由において、パッチ状態は不明です。reason タグは、チェックを
再実行する前に対処すべき内容を示します。
| コード | 意味 |
|---|---|
終了コード 0 は PATCHED、UNAFFECTED、および3つすべての INCONCLUSIVE の理由をカバーします。
パッチ済みターゲットと分類できなかったターゲットを区別するには、終了コードではなく判定
(--brief の1列目、または --json の verdict フィールド) を読んでください。
INCONCLUSIVE と
報告されます。このケースは、応答を再現するモックに対してテストされたものであり、デプロイされた
コレクターに対してではありません。grpc-status をリテラルな HPACK フィールドとしてスキャンし、Huffman や
動的テーブルのデコードを実装していません。ARM が使用する gRPC の C コアは、これらのトレーラーを
非圧縮で送信します。将来のビルドがそれらを Huffman 符号化した場合、ツールは partial-response を
伴う INCONCLUSIVE を報告します。PATCHED はこの CVE にのみ適用され、他の ARM 脆弱性のステータスを示すものでは
ありません。VULNERABLE は、プローブが到達したパスに修正が存在しないことを示します。ツールは
ターゲット上でコードを実行せず、事前の侵害も検出しません。上記の NetworkConnectionEntry
インジケーターを参照してください。ARM 2026.2.1 リリースノート に従い、ARM 2026.2.1.7 以降にアップグレードしてください。 ビルド 2026.2.0.42 以前が影響を受けます。
追加の注意:
このコードは MIT ライセンス の下で配布されています。
事前の相互同意なくターゲットを攻撃するためにこのツールを使用することは違法です。適用される すべての地方、州、および連邦法を遵守するのはエンドユーザーの責任です。開発者は一切の責任を 負わず、このプログラムによって引き起こされたいかなる誤用や損害についても責任を負いません。
| フラグ | 説明 |
|---|
TARGET | 1つ以上の HOST[:PORT] ターゲット。ポートのデフォルトは 55555 |
-f, --targets-file FILE | ファイルからターゲットを読み取る (1行に1つ、# コメント可) |
-p, --port PORT | ターゲットがポートを省略した場合のデフォルトポート (デフォルト: 55555) |
--timeout SECS | プローブごとのタイムアウト (デフォルト: 12) |
--workers N | 同時実行するターゲット数 (デフォルト: 16)。出力は入力順のまま |
-b, --brief | ターゲットごとに1行の整列出力。多数のホストのスキャン向け |
--json | 構造化 JSON を出力。ターゲットごとの両方のプローブを含む |
--no-color | 色付き出力を無効化 (NO_COLOR および非 TTY も尊重) |
Remoting/Send | GetCertificate | ビルド | 判定 |
|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | any | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | either | INCONCLUSIVE |
| 判定 | reason タグ | 意味 |
|---|
VULNERABLE | unavailable-on-server-role | GetCertificate にサーバーロールとして応答するリスナーで Remoting/Send が 14 を返した。修正が存在しない。 |
PATCHED | unauthenticated-gate | Remoting/Send が 16 を返した。これは修正によって導入されたステータス。この CVE にのみ適用される。 |
INCONCLUSIVE | external-requests-refused | 両方のプローブが 14 を返した。コレクターロールと制限されたサーバーは、どちらのビルドでもここで同じように動作する。ARM サーバー自体をスキャンすること。 |
INCONCLUSIVE | partial-response | 1つのプローブのみがステータスを返したため、ペアを比較できない。再試行すること。 |
INCONCLUSIVE | unrecognized-status | エンドポイントが認識済みセット外のステータスの組み合わせで応答した。 |
UNAFFECTED | not-arm | ここでは ARM gRPC サービスが応答しなかった。 |
ERROR | unreachable | TCP 接続、TLS ハンドシェイク、または HTTP/2 交換が失敗した。 |
0VULNERABLE のターゲットがなかった |
1 | 少なくとも1つのターゲットが VULNERABLE |
2 | 使用エラー (不正な引数、無効なターゲット、または読み取り不能なターゲットファイル) |
130 | 中断 (Ctrl-C) |