
Detecte com segurança o bypass de autenticação SAML do Citrix NetScaler CVE-2026-19490
Uma verificação de vulnerabilidade segura e não autenticada para CVE-2026-19490, o bypass de
autenticação pré-autenticação no caminho do provedor de serviços SAML do Citrix NetScaler ADC / NetScaler Gateway
(CTX696939,
publicado em 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). Reportado por Samarth Vashisht da
equipe de testes de intrusão do JPMorgan Chase.
O appliance decodifica em base64 o parâmetro RelayState de uma resposta SAML e, quando o texto simples
começa com ctx=, entrega o restante ao desserializador de contexto nFactor. Em uma build não corrigida, uma
falha de desserialização propaga o comprimento do RelayState decodificado como o código de disposição interno
da requisição em vez de um erro, de modo que um atacante não autenticado escolhe qual ramo interno
o appliance segue em seguida simplesmente escolhendo o comprimento do RelayState. Alguns ramos geram uma sessão
real do Gateway; outros derrubam o packet engine e reiniciam o appliance. Este script não faz nenhum dos dois
— ele envia o único comprimento validado para não criar sessão e deixar o packet engine em paz, e
responde a uma única pergunta por alvo: Um resultado diferente de
.
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+, apenas biblioteca padrão — sem pacotes de terceiros.
Aponte a ferramenta para o Gateway ou virtual server AAA, não para a interface de gerenciamento. A exposição é
por virtual server, então um appliance com vários VIPs precisa que cada um seja testado. A sonda carrega um
envelope de segurança rígido — um comprimento de RelayState validado, nunca varrido — que
É Seguro Executar? detalha.
| Flag | Descrição |
|---|---|
TARGET | Um ou mais alvos [https://]HOST[:PORT]; o esquema padrão é https:// |
-f, --targets-file FILE | Lê alvos de um arquivo (um por linha; comentários com #) |
--timeout SECS | Timeout por requisição (padrão: 15) |
--workers N | Alvos concorrentes (padrão: 16); a saída permanece na ordem de entrada |
-b, --brief | Uma linha alinhada por alvo — ideal para escanear muitos hosts |
--json | Emite JSON estruturado, incluindo cada requisição enviada por alvo |
--no-color | Desativa saída colorida (também respeita NO_COLOR e não-TTY) |
Um appliance vulnerável (a saída padrão de duas linhas). O marcador [!] e VULNERABLE são renderizados
em vermelho em um 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
Um appliance corrigido:
$ ./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)
A proteção contra falsos positivos disparando. A sonda e o controle de mesmo comprimento retornaram o sinal não corrigido, então a resposta não depende do que foi enviado e a resposta aparentemente decisiva é retirada:
$ ./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
Varrendo um parque (--brief). As duas linhas gateway.example.com são os virtual servers SP e somente IdP
no mesmo appliance — ambos respondem, o que uma verificação de pré-condição de configuração não conseguiria:
$ ./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
Saída legível por máquina (--json). Cada requisição é incluída, então um achado pode ser re-derivado
a partir da evidência em vez de ser confiado. O controle é registrado por sua relação com a sonda em vez
de como um veredito próprio, porque um controle que se lê como uma build corrigida é o resultado esperado em
toda build:
$ ./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"
}
]
}
]
Sim. Ele foi projetado para uso em produção e em avaliações:
RelayState da sonda decodifica para exatamente 20 bytes e a
ferramenta não envia nenhum outro comprimento sob nenhuma flag. Em um appliance não corrigido, o comprimento decodificado é
o seletor de ramo, e os ramos incluem alguns que criam uma sessão e alguns que causam SIGSEGV no
packet engine — reiniciando todo o NetScaler e derrubando o tráfego por cerca de 45 segundos. Vinte
bytes caem em um ramo limpo de erro interno que não cria sessão, e o construtor de payload
se recusa a construir qualquer outro comprimento, então uma edição em outro lugar não pode silenciosamente ampliar a sonda.13.1-63.18 não corrigido com zero cores gerados e nenhum reinício do packet engine. A verificação em si
já foi executada contra ambos os ramos de manutenção, em ambos os lados da correção, e contra
virtual servers de provedor de serviços e somente IdP igualmente, sem reinício do packet engine em nenhum
deles.SAMLResponse
precisa estar presente para que o ramo RelayState seja alcançado, mas seu conteúdo é
irrelevante, então são quatro bytes de lixo. O ramo não emite cookie e não escreve configuração.VULNERABLE nunca é reportado com base em uma única resposta — veja
o controle.Se você modificar a sonda, não altere o comprimento decodificado do
RelayStatee não varra comprimentos. Vinte bytes é essencial, e mais curto não é mais seguro. Outros comprimentos não são testados no melhor caso e uma indisponibilidade do appliance no pior caso.
Três requisições HTTP por alvo, e uma quarta apenas quando o appliance retorna o sinal não corrigido:
| # | Requisição | Propósito |
|---|---|---|
| 1 | GET /logon/LogonPoint/tmindex.html | Identificação do NetScaler (tokens de Content-Security-Policy) |
| 2 | GET /vpn/js/rdx/core/rdx.js | Identificação do NetScaler |
| 3 | POST /cgi/samlauth | a sonda |
| 4 | POST /cgi/samlauth | o controle, enviado apenas após um sinal não corrigido |
Redirecionamentos não são seguidos e nenhum cookie é jamais enviado de volta — o NetScaler serve um NSC_DLGE
expirado que um cliente que o reproduzisse carregaria para a requisição 4, transformando o POST em uma página de logon.
O nsppe registra o POST SAML em /var/log/ns.log no nível informativo, sem necessidade de nenhum botão de debug, como
uma falha de desserialização de contexto nFactor incluindo a linha
nFactor: ctx deserialize aaa_info, base64 decode failed. Não há linhas de criação de sessão ou login
neste comprimento, o que distingue uma varredura de uma tentativa de bypass.
A correção é uma instrução. Em uma desserialização de contexto com falha, a build vulnerável carrega o
comprimento decodificado do RelayState adiante como o status da requisição; a build corrigida carrega o erro
constante 0xe0005, renderizado como Malformed Assertion. Então um único POST os separa:
| Resposta à sonda | Build | Veredito |
|---|---|---|
500 Internal Server Error 43524 | não corrigida | VULNERABLE |
200 Malformed Assertion sent to Netscaler | corrigida | PATCHED |
A build corrigida responde Malformed Assertion em todos os comprimentos, o que faz da resposta não corrigida
um oráculo em vez de uma coincidência.
VULNERABLE baseia-se em uma única resposta distintiva, então a ferramenta verifica se a resposta realmente
depende do que foi enviado. Após o sinal não corrigido, ela repete a requisição com um RelayState de controle
com o mesmo comprimento decodificado de 20 bytes, diferindo apenas nos 16 bytes após ctx=. Esse
conteúdo é rejeitado por uma proteção antes do desserializador, então toda build testada — corrigida e
não corrigida — responde a ele 200 Malformed Assertion. O veredito se mantém apenas se as duas respostas diferirem.
Se elas coincidirem, algo está retornando 500/43524 independentemente da entrada, e o resultado é
INCONCLUSIVE marcado como flat-response em vez de um falso VULNERABLE. Manter o controle no
mesmo comprimento decodificado o mantém dentro do envelope de segurança validado e isola o conteúdo como a única variável.
A proteção é unidirecional. Ela não pode validar um veredito PATCHED, porque um appliance genuinamente
corrigido responde à sonda e ao controle de forma idêntica — é isso que significa estar corrigido aqui. Veja
Limitações.
A desserialização do RelayState é executada antes da correspondência de política SAML, então um virtual server que carrega apenas uma
samlIdPPolicy responde à sonda, não apenas um com um samlAction de provedor de serviços
vinculado. Isso é uma cobertura mais ampla do que uma verificação de pré-condição de configuração pode oferecer.
A resposta vulnerável é a única autocorroborante: ela carrega um marcador específico, e o
controle prova que a resposta depende do que foi enviado. PATCHED também é evidência positiva — o
erro corrigido do próprio appliance no caminho que a sonda alcançou — mas está limitado a este CVE e a
esse caminho. INCONCLUSIVE e ERROR não são evidência alguma: a sonda nunca alcançou o
desserializador, então o estado da correção é desconhecido, e é por isso que INCONCLUSIVE é um veredito separado
em vez de ser agrupado em PATCHED. Reportar silêncio como aprovação erra na direção perigosa.
Onde você precisa de certeza, confirme com show ns version contra as builds em
Remediação.
Todo veredito carrega uma tag curta de reason. --brief a imprime como a terceira coluna e --json
a carrega como reason.
| Veredito | Tag de reason | Significado |
|---|---|---|
VULNERABLE | internal-error-43524 | O appliance propagou o comprimento decodificado do RelayState como seu erro interno, e um controle de mesmo comprimento provou que a resposta depende do que foi enviado. A correção do CTX696939 está ausente — corrija. |
PATCHED | fixed-error-returned | O appliance retornou o erro corrigido no caminho que a sonda alcançou. Limitado a este CVE, e veja Limitações sobre dispositivos interpostos. |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth retornou 404, então o consumidor de assertion SAML não é servido aqui. Por vserver, não por appliance, e veja a ressalva de builds mais antigas em Limitações. |
INCONCLUSIVE | flat-response | Sonda e controle de mesmo comprimento responderam de forma idêntica, então a resposta não depende do que foi enviado. A proteção contra falsos positivos disparando. |
INCONCLUSIVE | generic-internal-error | O endpoint retornou 43549, seu erro interno genérico, que tanto builds corrigidas quanto não corrigidas retornam. Não é o discriminador — e observe que é o marcador não corrigido para o CVE-2026-8452 não relacionado, então parece um acerto e não é. |
INCONCLUSIVE | unrecognized-reply | O endpoint respondeu com algo fora do conjunto reconhecido. |
INCONCLUSIVE | no-probe-response | O host respondeu à identificação mas não à sonda — um timeout, um reset, ou um middlebox descartando o POST. Tente novamente. |
ERROR | not-identified | Não identificado como um NetScaler, ou inacessível. |
Todas as quatro razões de INCONCLUSIVE significam a mesma coisa para a tomada de decisão — desconhecido, não corrigido.
A tag nomeia qual condição corrigir antes de reexecutar.
| Código | Significado |
|---|---|
0 | Nenhum alvo era VULNERABLE |
1 | Pelo menos um alvo é VULNERABLE |
2 | Erro de uso (argumentos inválidos / arquivo de alvos ilegível) |
Saída 0 não é um atestado de saúde. PATCHED, UNAFFECTED e todas as quatro razões de INCONCLUSIVE
colapsam nela, e apenas uma delas é um resultado positivo. Um wrapper que precisa distinguir
"corrigido" de "não foi possível classificar" deve ler o veredito — primeira coluna de --brief, ou o
campo verdict de --json — não o código de saída.
PATCHED. Malformed Assertion é o que o
appliance retorna sempre que a sonda não consegue alcançar o desserializador de contexto, e estar corrigido é
apenas uma razão para isso acontecer. Um dispositivo interposto que remove, corrompe ou curto-circuita o
parâmetro RelayState portanto produz PATCHED em um appliance vulnerável, e nenhuma requisição adicional
que a ferramenta pudesse enviar separaria os dois. Onde algo pode estar na frente do
appliance, confirme com show ns version.UNAFFECTED está limitado a builds atuais. Em 14.1-43.55 / 13.1-61.27 e anteriores o
aviso afirma que a exposição não requer uma ação SAML alguma — qualquer virtual server Gateway ou AAA
é afetado — então um 404 em tal build não coloca o appliance fora de escopo. Esse trecho
é retirado do aviso em vez de medido.PATCHED não diz nada sobre qualquer outra
vulnerabilidade do NetScaler, incluindo CVE-2026-19489 no mesmo boletim.VULNERABLE estabelece que a correção
está ausente no caminho que a sonda alcançou. Ele não mede até onde um atacante poderia levar o
bypass na sua configuração, e não lhe dirá se alguém já o fez — procure isso em
/var/log/ns.log separadamente.Atualize para 13.1-63.21 ou posterior, ou 14.1-73.32 ou posterior (FIPS e NDcPP: 13.1-37.277 para 13.1-FIPS e 13.1-NDcPP, 14.1-73.32 FIPS para 14.1-FIPS), conforme CTX696939. Appliances em 12.1 ou 13.0 não têm correção e não receberão uma — esses ramos estão em fim de vida e devem ser migrados para um ramo suportado.
Três notas adicionais:
defaultAuthorizationAction de qualquer vpn sessionAction vinculado ao vserver — ela recorre ao
set vpn parameter -defaultAuthorizationAction global. O padrão de fábrica é DENY; ALLOW
é comum no campo porque é o que os administradores usam em vez de escrever políticas de
autorização por usuário, e ALLOW é o que permite que a sessão não autenticada alcance recursos
internos. É global, não por vserver. Isso limita o impacto; não é uma correção.add authentication samlAction e add authentication samlIdPProfile juntamente com
add authentication vserver e add vpn vserver — sujeito à ressalva de builds mais antigas acima.O CTX696939 também corrige CVE-2026-19489, e esta superfície se sobrepõe fortemente com CVE-2026-8452 do boletim anterior: em builds a partir de 14.1-43.56 / 13.1-61.28 em diante, ambos os problemas são condicionados a uma ação SAML configurada, então uma auditoria de configuração delimita ambos.
Este código é distribuído sob uma licença MIT.
O uso desta ferramenta para atacar alvos sem consentimento mútuo prévio é ilegal. É responsabilidade do usuário final obedecer a todas as leis locais, estaduais e federais aplicáveis. Os desenvolvedores não assumem nenhuma responsabilidade e não são responsáveis por qualquer uso indevido ou dano causado por este programa.