
Citrix NetScalerのSAML認証バイパスCVE-2026-19490を安全に検出
Citrix NetScaler ADC / NetScaler Gateway の SAML サービスプロバイダーパスにおける認証前の認証バイパスである CVE-2026-19490 に対する、安全で非認証の脆弱性チェック
(CTX696939、
2026-08-19 公開)。CWE-288、CVSS v4.0 9.3
(AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L)。JPMorgan Chase のペネトレーションテストチームの Samarth Vashisht 氏により報告されました。
アプライアンスは SAML レスポンスの RelayState パラメータを base64 デコードし、その平文が ctx= で始まる場合、残りの部分を nFactor コンテキストデシリアライザに渡します。未パッチのビルドでは、デシリアライズの失敗がエラーではなく デコードされた RelayState の長さ をリクエストの内部ディスポジションコードとして伝播するため、非認証の攻撃者は RelayState の長さを選ぶだけで、アプライアンスが次に取る内部ブランチを選択できます。一部のブランチは実際の Gateway セッションを発行し、他のブランチはパケットエンジンをクラッシュさせてアプライアンスを再起動させます。このスクリプトはどちらも行いません — セッションを作成せずパケットエンジンにも影響を与えないことが検証済みの単一の長さを送信し、ターゲットごとに 1 つの問いに答えます: このアプライアンスは脆弱か? VULNERABLE 以外の結果は それ自体が安全のお墨付きではありません。
# 単一ターゲット
./cve_2026_19490_check.py https://gateway.example.com
# 特定の Gateway または AAA 仮想サーバー
./cve_2026_19490_check.py https://gateway.example.com:9443
# 複数ターゲット; スキームはデフォルトで https://
./cve_2026_19490_check.py gw-a.example.com gw-b.example.com:9443
# リストをスキャン、1 行に 1 ターゲット ('#' コメント可)、コンパクト出力
./cve_2026_19490_check.py -f targets.txt --brief
# パイプライン向けの機械可読出力
./cve_2026_19490_check.py -f targets.txt --json > results.json
Python 3.8+、標準ライブラリのみ — サードパーティパッケージは不要です。
ツールは Gateway または AAA 仮想サーバー を指定してください。管理インターフェースではありません。露出は仮想サーバーごとに異なるため、複数の VIP を持つアプライアンスではそれぞれをテストする必要があります。プローブには厳格な安全エンベロープが組み込まれています — 検証済みの RelayState 長を 1 つだけ使用し、スイープは決して行いません — これについては 実行しても安全か? で説明します。
脆弱なアプライアンス (デフォルトの 2 行出力)。[!] マーカーと VULNERABLE は TTY 上で赤く表示されます:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443
[!] https://gateway.example.com:9443: VULNERABLE [internal-error-43524]
HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent
パッチ済みのアプライアンス:
$ ./cve_2026_19490_check.py https://vpn.example.com
[+] https://vpn.example.com: PATCHED [fixed-error-returned]
HTTP 200 "Malformed Assertion": the fixed error was returned on the path this probe reached, so the CTX696939 fix is present (>= 13.1-63.21 / 14.1-73.32)
偽陽性ガードが発動した例。 プローブ と 同じ長さのコントロールの両方が未パッチのシグナルを返したため、応答は送信内容に依存せず、決定的に見える回答は撤回されます:
$ ./cve_2026_19490_check.py https://sp-strict.example.com
[?] https://sp-strict.example.com: INCONCLUSIVE [flat-response]
the probe and the same-length control both answered HTTP 500 / 43524, so the reply does not depend on what was sent and the fix was never exercised; unknown, not patched
資産全体のスイープ (--brief)。2 つの gateway.example.com 行は同じアプライアンス上の SP と IdP 専用の仮想サーバーです — どちらも応答しますが、構成前提条件チェックでは対応できません:
$ ./cve_2026_19490_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 internal-error-43524
VULNERABLE https://gateway.example.com:9444 internal-error-43524
PATCHED https://vpn.example.com fixed-error-returned
INCONCLUSIVE https://sp-strict.example.com flat-response
UNAFFECTED https://lb.example.com no-saml-endpoint
ERROR https://www.example.com not-identified
exit: 1
機械可読出力 (--json)。すべてのリクエストが含まれるため、検出結果は信頼するのではなく証拠から再導出できます。コントロールは独自の判定としてではなく、プローブとの 関係 によって記録されます。パッチ済みビルドのように読めるコントロールは、どのビルドでも期待される結果だからです:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443 --json
[
{
"target": "https://gateway.example.com:9443",
"verdict": "VULNERABLE",
"reason": "internal-error-43524",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent",
"netscaler_indicators": [
"CSP contains citrixng://",
"CSP contains com.citrix.nsgclient://",
"CSP contains nsgcepa://",
"CSP report-uri /nscsp_violation/report_uri",
"/vpn/js/rdx/ present (HTTP 404)"
],
"attempts": [
{
"kind": "probe",
"path": "/cgi/samlauth",
"status": 500,
"state": "unpatched",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error"
},
{
"kind": "control",
"path": "/cgi/samlauth",
"status": 200,
"state": "differs-from-probe",
"detail": "same-length control: HTTP 200 \"Malformed Assertion\": the fixed error was returned"
}
]
}
]
はい。本番環境および評価での使用を想定して設計されています:
RelayState は正確に 20 バイトにデコードされ、ツールはいかなるフラグでも他の長さを送信しません。未パッチのアプライアンスでは、デコードされた長さが ブランチセレクタそのもの であり、ブランチにはセッションを作成するものとパケットエンジンを SIGSEGV させるものが含まれます — これは NetScaler 全体を再起動させ、約 45 秒間トラフィックを落とします。20 バイトはセッションを作成しないクリーンな内部エラーブランチに到達し、ペイロードビルダーは他の長さの構築を拒否するため、他の箇所の編集でプローブが密かに広がることはありません。13.1-63.18 に対して 10 回連続で実行され、コアは 1 つも生成されず、パケットエンジンの再起動もありませんでした。チェック自体はその後、両方のメンテナンスブランチ、修正の両側、およびサービスプロバイダーと IdP 専用の仮想サーバーの両方に対して実行され、いずれもパケットエンジンの再起動はありませんでした。SAMLResponse パラメータは RelayState ブランチに到達するために 存在 する必要がありますが、その内容は無関係であるため、4 バイトのジャンクです。このブランチは Cookie を発行せず、構成も書き込みません。VULNERABLE は単一の応答では決して報告されません — コントロール を参照してください。プローブを変更する場合、デコードされた
RelayStateの長さを変更しないでください。また、長さをスイープしないでください。 20 バイトは重要な意味を持ち、短ければ安全というわけではありません。他の長さは良くても未テストであり、最悪の場合はアプライアンスの停止を招きます。
ターゲットごとに 3 つの HTTP リクエスト、アプライアンスが未パッチのシグナルを返した場合のみ 4 つ目:
リダイレクトは追跡されず、Cookie も一切返送されません — NetScaler は期限切れの NSC_DLGE を提供し、これを再生するクライアントはリクエスト 4 に持ち込み、POST をログオンページに切り替えてしまいます。
nsppe は SAML POST を /var/log/ns.log に情報レベルで記録します。デバッグノブは不要で、nFactor コンテキストのデシリアライズ失敗として、次の行を含みます:
nFactor: ctx deserialize aaa_info, base64 decode failed。この長さではセッション作成やログインの行はなく、これがスキャンとバイパスの試行を区別するものです。
修正は 1 つの命令です。コンテキストのデシリアライズが失敗した際、脆弱なビルドはデコードされた RelayState の長さをリクエストのステータスとして引き継ぎますが、修正済みビルドは定数エラー 0xe0005 を引き継ぎ、Malformed Assertion としてレンダリングされます。したがって 1 回の POST で両者を区別できます:
| プローブへの応答 | ビルド | 判定 |
|---|---|---|
500 Internal Server Error 43524 | 未パッチ | VULNERABLE |
200 Malformed Assertion sent to Netscaler | パッチ済み | PATCHED |
パッチ済みビルドは あらゆる 長さで Malformed Assertion を返します。これにより、未パッチの応答は偶然ではなくオラクルとなるのです。
VULNERABLE は単一の識別応答に依存するため、ツールは応答が実際に送信内容に依存していることを検証します。未パッチのシグナルの後、同じ 20 バイトのデコード長を持つコントロール RelayState でリクエストを繰り返します。異なるのは ctx= の後の 16 バイトのみです。この内容はデシリアライザの前段のガードによって拒否されるため、テストされたすべてのビルド — パッチ済みと未パッチの両方 — が 200 Malformed Assertion を返します。判定は 2 つの応答が異なる場合にのみ成立します。一致する場合、何かが入力に関係なく 500/43524 を返しており、結果は偽の VULNERABLE ではなく flat-response タグ付きの INCONCLUSIVE となります。コントロールを同じデコード長に保つことで、検証済みの安全エンベロープ内に収め、内容のみを唯一の変数として分離します。
ガードは一方向です。 これは PATCHED 判定を検証できません。真にパッチ済みのアプライアンスはプローブとコントロールに同一に応答するからです — ここでパッチ済みであるとはそういう意味です。制限事項 を参照してください。
RelayState のデシリアライズは SAML ポリシーマッチングの 前に 実行されるため、samlIdPPolicy のみを持つ仮想サーバーでもプローブに応答します。サービスプロバイダーの samlAction がバインドされたものだけではありません。これは構成前提条件チェックが提供できるよりも広いカバレッジです。
脆弱な応答だけが自己裏付け的です: 特定のマーカーを持ち、コントロールが応答が送信内容に依存することを証明します。PATCHED もまた肯定的な証拠です — プローブが到達したパス上でアプライアンス自身が返す修正済みエラー — ただしこれはこの CVE とそのパスに限定されます。INCONCLUSIVE と ERROR はまったく証拠ではありません: プローブがデシリアライザに到達しなかったため、パッチ状態は不明であり、だからこそ INCONCLUSIVE は PATCHED に折り込まれず別の判定となっています。沈黙を合格として報告することは、危険な方向に誤ります。確実性が必要な場合は、修復 にあるビルドに対して show ns version で確認してください。
すべての判定には短い reason タグが付きます。--brief はそれを 3 列目として出力し、--json は reason として含めます。
4 つの INCONCLUSIVE 理由はすべて意思決定において同じ意味です — 不明であり、パッチ済みではありません。 タグは再実行前に修正すべき条件を示します。
| コード | 意味 |
|---|---|
0 | VULNERABLE なターゲットがなかった |
1 | 少なくとも 1 つのターゲットが VULNERABLE |
2 | 使用エラー (不正な引数 / 読み取り不能なターゲットファイル) |
終了コード 0 は安全のお墨付きではありません。 PATCHED、UNAFFECTED、および 4 つの INCONCLUSIVE 理由すべてがこれに集約され、そのうち肯定的な結果は 1 つだけです。「パッチ済み」と「分類できなかった」を区別する必要があるラッパーは、終了コードではなく判定 — --brief の最初の列、または --json の verdict フィールド — を読む必要があります。
PATCHED 判定を歪める可能性があります。 Malformed Assertion は、プローブがコンテキストデシリアライザに到達しなかったときにアプライアンスが返すものであり、パッチ済みであることはその理由の 1 つにすぎません。したがって、RelayState パラメータを除去、改変、または短絡させる介在デバイスは、脆弱なアプライアンス上で PATCHED を生成し、ツールが送信できる追加のリクエストでも両者を区別できません。アプライアンスの前に何かがある可能性がある場合は、show ns version で確認してください。UNAFFECTED は現在のビルドに限定されます。 14.1-43.55 / 13.1-61.27 以前 では、アドバイザリは露出に SAML アクションがまったく不要であると述べています — 任意の Gateway または AAA 仮想サーバーが影響を受けます — そのためそのようなビルドでの 404 はアプライアンスをスコープ外にするものではありません。この部分は測定ではなくアドバイザリから引用しています。PATCHED は同じ速報にある CVE-2026-19489 を含め、他の NetScaler 脆弱性について何も述べていません。VULNERABLE はプローブが到達したパス上で修正が欠如していることを立証します。あなたの構成で攻撃者がバイパスをどこまで進められるかは測定せず、誰かがすでに侵害したかどうかも教えてくれません — それは /var/log/ns.log で別途ハントしてください。CTX696939 に従い、13.1-63.21 以降、または 14.1-73.32 以降にアップグレードしてください (FIPS および NDcPP: 13.1-FIPS および 13.1-NDcPP には 13.1-37.277、14.1-FIPS には 14.1-73.32 FIPS)。12.1 または 13.0 のアプライアンスには修正がなく、今後も提供されません — これらのブランチはサポート終了であり、サポート対象ブランチに移行すべきです。
さらに 3 点の注意:
vpn sessionAction の defaultAuthorizationAction を取得しません — グローバル な set vpn parameter -defaultAuthorizationAction にフォールバックします。出荷時のデフォルトは DENY です; ALLOW は、ユーザーごとの認可ポリシーを書く代わりに管理者が手を伸ばすものであるため現場でよく見られ、ALLOW が非認証セッションを内部リソースに到達させる原因です。これはグローバルであり、vserver 単位ではありません。これは影響を限定するものであり、修正ではありません。add authentication samlAction と add authentication samlIdPProfile を、add authentication vserver と add vpn vserver とともに確認してください — 上記の古いビルドに関する注意が適用されます。CTX696939 は CVE-2026-19489 も修正し、このサーフェスは前回の速報の CVE-2026-8452 と大きく重複します: 14.1-43.56 / 13.1-61.28 以降のビルドでは、両方の問題が構成された SAML アクションを条件とするため、1 回の構成監査で両方のスコープを定められます。
このコードは MIT ライセンス の下で配布されています。
事前の相互同意なくターゲットを攻撃するためにこのツールを使用することは違法です。適用されるすべての地方、州、および連邦法を遵守することはエンドユーザーの責任です。開発者は一切の責任を負わず、このプログラムによって引き起こされたいかなる誤用や損害についても責任を負いません。
| フラグ | 説明 |
|---|
TARGET | 1 つ以上の [https://]HOST[:PORT] ターゲット; スキームはデフォルトで https:// |
-f, --targets-file FILE | ファイルからターゲットを読み込む (1 行に 1 つ; # コメント) |
--timeout SECS | リクエストごとのタイムアウト (デフォルト: 15) |
--workers N | 同時ターゲット数 (デフォルト: 16); 出力は入力順を維持 |
-b, --brief | ターゲットごとに整列された 1 行 — 多数のホストのスキャンに最適 |
--json | 構造化 JSON を出力、ターゲットごとに送信されたすべてのリクエストを含む |
--no-color | 色付き出力を無効化 (NO_COLOR および非 TTY も尊重) |
| # | リクエスト | 目的 |
|---|
| 1 | GET /logon/LogonPoint/tmindex.html | NetScaler の識別 (Content-Security-Policy トークン) |
| 2 | GET /vpn/js/rdx/core/rdx.js | NetScaler の識別 |
| 3 | POST /cgi/samlauth | プローブ |
| 4 | POST /cgi/samlauth | コントロール、未パッチのシグナルの後にのみ送信 |
| 判定 | 理由タグ | 意味 |
|---|
VULNERABLE | internal-error-43524 | アプライアンスがデコードされた RelayState の長さを内部エラーとして伝播し、同じ長さのコントロールが応答が送信内容に依存することを証明しました。CTX696939 の修正が欠如しています — パッチを適用してください。 |
PATCHED | fixed-error-returned | アプライアンスがプローブの到達したパス上で修正済みエラーを返しました。この CVE に限定 され、介在デバイスについては 制限事項 を参照してください。 |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth が 404 を返したため、SAML アサーションコンシューマがここでは提供されていません。アプライアンス単位ではなく vserver 単位 であり、制限事項 の古いビルドに関する注意を参照してください。 |
INCONCLUSIVE | flat-response | プローブと同じ長さのコントロールが同一に応答したため、応答は送信内容に依存しません。偽陽性ガードの発動です。 |
INCONCLUSIVE | generic-internal-error | エンドポイントが汎用内部エラーである 43549 を返しました。これはパッチ済みと未パッチの両方のビルドが返します。識別子ではありません — また、これは無関係な CVE-2026-8452 の未パッチマーカー である ため、ヒットに見えますがヒットではありません。 |
INCONCLUSIVE | unrecognized-reply | エンドポイントが認識されたセット外の何かを返しました。 |
INCONCLUSIVE | no-probe-response | ホストは識別には応答しましたがプローブには応答しませんでした — タイムアウト、リセット、またはミドルボックスが POST をドロップしています。再試行してください。 |
ERROR | not-identified | NetScaler として識別されなかった、または到達不能です。 |