
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 つだけ使用し、スイープは決して行いません — これについては 実行しても安全か? で説明します。
| フラグ | 説明 |
|---|---|
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 も尊重) |
脆弱なアプライアンス (デフォルトの 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 つ目:
| # | リクエスト | 目的 |
|---|---|---|
| 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 | コントロール、未パッチのシグナルの後にのみ送信 |
リダイレクトは追跡されず、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 判定を検証できません。真にパッチ済みのアプライアンスはプローブとコントロールに同一に応答するからです — ここでパッチ済みであるとはそういう意味です。制限事項 を参照してください。