
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 세션을 생성하고, 다른 분기는 패킷 엔진을 충돌시켜 어플라이언스를 재시작합니다. 이 스크립트는 둘 다 하지 않습니다 — 세션을 생성하지 않고 패킷 엔진에 영향을 주지 않는 것으로 검증된 단 하나의 길이를 전송하며, 대상마다 단 하나의 질문에 답합니다: 이 어플라이언스는 취약한가? VULNERABLE이 아닌 결과는 그 자체로 안전하다는 보증이 아닙니다.
# single target
./cve_2026_19490_check.py https://gateway.example.com
# a specific Gateway or AAA virtual server
./cve_2026_19490_check.py https://gateway.example.com:9443
# several targets; the scheme defaults to https://
./cve_2026_19490_check.py gw-a.example.com gw-b.example.com:9443
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_19490_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_19490_check.py -f targets.txt --json > results.json
Python 3.8+, 표준 라이브러리만 사용 — 서드파티 패키지 없음.
도구를 Gateway 또는 AAA 가상 서버에 지정하세요, 관리 인터페이스가 아닙니다. 노출은 가상 서버별로 이루어지므로, 여러 VIP를 가진 어플라이언스는 각각을 테스트해야 합니다. 프로브는 강력한 안전 범위를 지닙니다 — 검증된 단 하나의 RelayState 길이만 사용하며, 절대 훑지 않습니다 — 이는 실행해도 안전한가?에서 설명합니다.
취약한 어플라이언스 (기본 두 줄 출력). [!] 마커와 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). 두 개의 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회 실행되었으며 코어 생성이 전혀 없고 패킷 엔진 재시작도 없었습니다. 점검 자체는 이후 두 유지보수 브랜치에 대해, 수정 전후 양쪽에서, 그리고 서비스 제공자 및 IdP 전용 가상 서버 모두에 대해 실행되었으며, 어느 경우에도 패킷 엔진 재시작이 없었습니다.RelayState 분기에 도달하려면 SAMLResponse 파라미터가 존재해야 하지만 그 내용은 무관하므로, 4바이트의 쓰레기 데이터입니다. 이 분기는 쿠키를 발급하지 않고 구성을 기록하지 않습니다.VULNERABLE은 단일 응답으로 절대 보고되지 않습니다 — 대조군을 참조하세요.프로브를 수정하는 경우, 디코딩된
RelayState길이를 변경하지 말고 길이를 훑지 마세요. 20바이트는 핵심이며, 더 짧다고 더 안전하지 않습니다. 다른 길이는 최선의 경우 미검증이고 최악의 경우 어플라이언스 중단입니다.
대상당 세 개의 HTTP 요청, 그리고 어플라이언스가 미패치 신호를 반환할 때만 네 번째 요청:
리다이렉트는 따라가지 않으며 쿠키는 절대 되돌려 보내지 않습니다 — NetScaler는 만료된 NSC_DLGE를 제공하는데, 재생 클라이언트가 이를 요청 4로 가져가면 POST가 로그온 페이지로 바뀝니다.
nsppe는 SAML POST를 /var/log/ns.log에 정보 수준으로 기록하며, 디버그 설정이 필요 없고, nFactor 컨텍스트 역직렬화 실패로 다음 줄을 포함합니다
nFactor: ctx deserialize aaa_info, base64 decode failed. 이 길이에서는 세션 생성이나 로그인 줄이 없으며, 이것이 스캔과 우회 시도를 구분합니다.
수정은 명령어 하나입니다. 컨텍스트 역직렬화 실패 시 취약한 빌드는 디코딩된 RelayState 길이를 요청의 상태로 전달하고, 수정된 빌드는 상수 오류 0xe0005를 전달하며, 이는 Malformed Assertion으로 렌더링됩니다. 따라서 하나의 POST로 둘을 구분합니다:
| 프로브에 대한 응답 | 빌드 | 판정 |
|---|---|---|
500 Internal Server Error 43524 | 미패치 | VULNERABLE |
200 Malformed Assertion sent to Netscaler | 패치됨 | PATCHED |
패치된 빌드는 모든 길이에서 Malformed Assertion으로 응답하며, 이것이 미패치 응답을 우연이 아닌 오라클로 만듭니다.
VULNERABLE은 단일 구별 응답에 의존하므로, 도구는 응답이 실제로 전송된 내용에 의존하는지 검증합니다. 미패치 신호 이후 **동일한 20바이트 디코딩 길이의 대조군 RelayState**로 요청을 반복하며, ctx= 뒤의 16바이트만 다릅니다. 그 내용은 역직렬화기 앞의 가드에 의해 거부되므로, 테스트된 모든 빌드 — 패치됨과 미패치 — 가 200 Malformed Assertion으로 응답합니다. 두 응답이 다를 때만 판정이 유지됩니다. 일치하면 무언가가 입력과 무관하게 500/43524를 반환하는 것이며, 결과는 거짓 VULNERABLE이 아니라 flat-response 태그가 붙은 INCONCLUSIVE입니다. 대조군을 동일한 디코딩 길이로 유지하면 검증된 안전 범위 내에 머물고 내용을 유일한 변수로 격리합니다.
가드는 단방향입니다. PATCHED 판정을 검증할 수 없습니다. 진정으로 패치된 어플라이언스는 프로브와 대조군에 동일하게 응답하기 때문입니다 — 여기서 패치되었다는 것의 의미가 바로 그것입니다. 제한 사항을 참조하세요.
RelayState 역직렬화는 SAML 정책 매칭 이전에 실행되므로, samlIdPPolicy만 가진 가상 서버도 프로브에 응답하며, 서비스 제공자 samlAction이 바인딩된 것만이 아닙니다. 이는 구성 사전 조건 점검이 제공할 수 있는 것보다 넓은 범위입니다.
취약한 응답은 유일하게 자기 입증적인 응답입니다: 특정 마커를 가지며, 대조군이 응답이 전송된 내용에 의존함을 증명합니다. PATCHED도 긍정적 증거입니다 — 프로브가 도달한 경로에서 어플라이언스 자체의 수정된 오류 — 하지만 이는 이 CVE와 그 경로로 범위가 한정됩니다. INCONCLUSIVE와 ERROR는 전혀 증거가 아닙니다: 프로브가 역직렬화기에 도달하지 못했으므로 패치 상태는 알 수 없으며, 이것이 INCONCLUSIVE가 PATCHED로 합쳐지지 않고 별도 판정인 이유입니다. 침묵을 통과로 보고하는 것은 위험한 방향으로 잘못 판단하는 것입니다. 확실성이 필요한 경우, 해결의 빌드에 대해 show ns version으로 확인하세요.
모든 판정은 짧은 reason 태그를 가집니다. --brief는 이를 세 번째 열로 출력하고 --json은 reason으로 담습니다.
네 가지 INCONCLUSIVE 이유 모두 의사 결정 측면에서 동일한 의미입니다 — 알 수 없음, 패치되지 않음. 태그는 재실행 전에 어떤 조건을 해결해야 하는지 명시합니다.
| 코드 | 의미 |
|---|---|
0 | VULNERABLE인 대상이 없음 |
1 | 최소 하나의 대상이 VULNERABLE |
2 | 사용 오류 (잘못된 인수 / 읽을 수 없는 대상 파일) |
종료 0은 안전하다는 보증이 아닙니다. PATCHED, UNAFFECTED 및 네 가지 INCONCLUSIVE 이유 모두가 여기에 포함되며, 그중 하나만 긍정적 결과입니다. "패치됨"과 "분류할 수 없음"을 구별해야 하는 래퍼는 종료 코드가 아니라 판정 — --brief의 첫 번째 열, 또는 --json의 verdict 필드 — 을 읽어야 합니다.
PATCHED 판정을 왜곡할 수 있습니다. Malformed Assertion은 프로브가 컨텍스트 역직렬화기에 도달하지 못할 때마다 어플라이언스가 반환하는 것이며, 패치된 것이 그런 일이 발생하는 이유 중 하나일 뿐입니다. 따라서 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의 어플라이언스는 수정이 없으며 제공되지 않을 것입니다 — 해당 브랜치는 수명이 종료되었으며 지원되는 브랜치로 마이그레이션해야 합니다.
세 가지 추가 참고 사항:
vpn sessionAction의 defaultAuthorizationAction도 적용되지 않습니다 — 전역 set vpn parameter -defaultAuthorizationAction으로 폴백합니다. 기본 제공 값은 DENY이며, ALLOW는 사용자별 권한 부여 정책을 작성하는 대신 관리자가 흔히 선택하기 때문에 현장에서 흔하며, ALLOW가 비인증 세션이 내부 리소스에 도달하게 하는 것입니다. 이는 전역이며 vserver별이 아닙니다. 이는 영향을 제한할 뿐, 수정이 아닙니다.add authentication vserver 및 add vpn vserver와 함께 add authentication samlAction 및 add authentication samlIdPProfile을 확인하세요 — 위의 구형 빌드 주의 사항이 적용됩니다.CTX696939는 CVE-2026-19489도 수정하며, 이 표면은 이전 게시판의 CVE-2026-8452와 크게 겹칩니다: 14.1-43.56 / 13.1-61.28 이후 빌드에서는 두 문제 모두 구성된 SAML 액션에 의해 제한되므로, 하나의 구성 감사로 둘 다 범위를 정할 수 있습니다.
이 코드는 MIT 라이선스에 따라 배포됩니다.
사전 상호 동의 없이 대상을 공격하기 위해 이 도구를 사용하는 것은 불법입니다. 모든 관련 지역, 주, 연방 법률을 준수할 책임은 최종 사용자에게 있습니다. 개발자는 이 프로그램으로 인해 발생하는 어떠한 오용이나 손해에 대해서도 책임을 지지 않습니다.
| 플래그 | 설명 |
|---|
TARGET | 하나 이상의 [https://]HOST[:PORT] 대상; 스킴은 기본적으로 https:// |
-f, --targets-file FILE | 파일에서 대상 읽기 (한 줄에 하나; # 주석) |
--timeout SECS | 요청당 타임아웃 (기본값: 15) |
--workers N | 동시 대상 수 (기본값: 16); 출력은 입력 순서 유지 |
-b, --brief | 대상당 정렬된 한 줄 — 많은 호스트 스캔에 이상적 |
--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로 식별되지 않았거나 도달할 수 없습니다. |