
Detector comportamental de estado de patch para Citrix NetScaler CVE-2026-8452. Envia solicitações SAML elaboradas para determinar se a verificação de tamanho do PrefixList está presente, sem explorar ou corromper memória.
Uma verificação segura e não destrutiva do estado do patch para CVE-2026-8452, o heap overflow de pré-autenticação
no canonicalizador de assinatura SAML do Citrix NetScaler ADC / NetScaler Gateway
(CTX696604,
CVSS 8.8). Um PrefixList superdimensionado de canonicalização exclusiva estoura um buffer de tamanho fixo durante
a canonicalização, que o NetScaler executa antes de validar a assinatura que o transporta — portanto, todo o
caminho é acessível sem credenciais, sem sessão e sem assinatura válida. Relatado por Michael
Tucker da equipe XOR do JPMorgan Chase; análise de causa raiz e de exploração creditada a
watchTowr Labs.
Este script não explora a vulnerabilidade e não corrompe a memória. Ele responde a uma pergunta por alvo: a correção está presente neste appliance? — determinado comportamentalmente, observando o patch em vez de adivinhar a versão.
Sim. Ele foi projetado para uso em produção e em avaliações:
PrefixList de . Esse limite foi localizado byte a byte,
é idêntico nos dois ramos suportados e não muda com a configuração do appliance nem
com a forma da mensagem SAML ao redor — confirmado ao testar ambas as rotas, que envolvem o
valor em quantidades substancialmente diferentes de XML, e ao verificar que elas mudam de comportamento no mesmo
byte. 575 supera o limite em 63 bytes, portanto o veredito não depende de como um alvo acontece de
estar configurado.PrefixList. Inflar outros campos além dele — URLs de assertion consumer service,
nomes de issuer, identificadores de algoritmo, valores de digest e assinatura — não muda nada em uma versão corrigida, portanto
aplicar a correção não deve fazer uma configuração SAML funcional começar a falhar.Se você modificar a sonda, não altere
PROBE_PREFIXESe não varra comprimentos. 575 bytes é essencial. Outros comprimentos dePrefixListpodem desestabilizar um appliance, em pelo menos um caso em uma versão que contém esta correção; portanto, uma varredura de comprimentos não é uma forma segura de explorar este bug, e mais curto não é mais seguro.
Versões corrigidas rejeitam um PrefixList superdimensionado de forma limpa, com uma mensagem distintiva. Versões não corrigidas
passam pelo parser e retornam um erro interno genérico. Uma solicitação idêntica, duas respostas
diferentes:
PrefixList de 575 bytes | Resposta |
|---|---|
| Não corrigido | 500 Internal Server Error 43549 |
| Corrigido | 200 Malformed Assertion sent to Netscaler |
Duas rotas são testadas, IdP primeiro, parando assim que uma der uma resposta. Qualquer uma é suficiente sozinha, e juntas elas cobrem os dois papéis SAML:
| Rota | Solicitação | Exige |
|---|---|---|
| 1 (primeira) | POST /saml/login — AuthnRequest assinado, PrefixList em ds:SignedInfo | uma política de IdP SAML vinculada ao vserver alvo |
| 2 (fallback) | POST /cgi/samlauth — SAMLResponse, PrefixList na assinatura da asserção | um serviço de consumidor de asserção de SP SAML no vserver alvo |
A rota IdP vem primeiro porque é a mais robusta das duas. Ela é insensível ao valor de Issuer,
à AssertionConsumerServiceURL e ao skew de relógio — um IssueInstant bem fora da
tolerância de skew do appliance ainda discrimina corretamente, porque a canonicalização precede a verificação
de tempo, assim como a verificação de assinatura.
O
AuthnRequestda rota 1 deve ser assinado. Um não assinado retorna200 Malformed Assertion sent to Netscalerem versões corrigidas e não corrigidas, o que é byte-idêntico ao sinal de corrigido; portanto, uma sonda que omite o bloco de assinatura relata todos os appliances como corrigidos. A assinatura não precisa ser válida, e a desta ferramenta não é; ela só precisa estar presente, porque é o seuSignedInfoque carrega oPrefixListpara o canonicalizador.
Ambos os ramos suportados mudam de comportamento exatamente na versão corrigida, em ambas as rotas:
| Versão | Veredito | |
|---|---|---|
13.1-63.16 | última 13.1 vulnerável | VULNERABLE |
13.1-63.18 | primeira 13.1 corrigida | PATCHED |
14.1-66.59 | 14.1 vulnerável | VULNERABLE |
14.1-72.61 | primeira 14.1 corrigida | PATCHED |
13.1-63.16 e 63.18 são versões consecutivas, portanto a mudança é atribuível ao próprio patch,
em vez de alterações entre versões intermediárias.
Essas são as versões em que esta correção apareceu pela primeira vez, e a sonda detecta exatamente essa transição.
Elas não são mais as versões para as quais atualizar: boletins posteriores as substituíram, então 13.1-63.18 e
14.1-72.61 respondem PATCHED aqui, mas permanecem expostos a problemas mais novos. Veja
Remediação para as versões corrigidas atuais.
Porque é impossível que funcione para esta vulnerabilidade, nem em princípio. 13.1-63.16 e 13.1-63.18, as versões
imediatamente em ambos os lados da correção, servem byte-idênticos tmindex.html, base.css e
resources.js — a correção não toca nenhum ativo web. Hashes de ativos estáticos também colidem entre ramos, portanto,
uma abordagem baseada em hash pode mapear um appliance vulnerável para uma versão corrigida e reportá-lo como limpo,
que é o pior modo de falha que uma ferramenta de detecção pode ter. Portanto, o fingerprinting de versão é deliberadamente
não implementado. O estado do patch vem da sonda, ou do comando show ns version quando você tem
credenciais.
./cve_2026_8452_check.py https://gateway.example.com
./cve_2026_8452_check.py https://gateway.example.com:9443
./cve_2026_8452_check.py -f targets.txt --brief
./cve_2026_8452_check.py -f targets.txt --json > results.json
Aponte a ferramenta para o **servidor virtual Gateway ou AAA**, não para a interface de gerenciamento. A
pré-condição é por servidor virtual, portanto um appliance com vários VIPs precisa que cada um seja testado.
### Opções
| Opção | Descrição |
| --- | --- |
| `URL` | Um ou mais alvos `https://HOST[:PORT]` |
| `-f, --targets-file FILE` | Ler alvos de um arquivo (um por linha; comentários `#`) |
| `-b, --brief` | Uma única linha alinhada por alvo — veredito, alvo, tag de motivo — para escanear muitos hosts |
| `--json` | Emitir resultados JSON estruturados |
| `--no-color` | Desativar saída colorida (também respeita `NO_COLOR` e não-TTY) |
| `--timeout SECS` | Timeout por requisição (padrão: 15) |
### Exemplos
**Um appliance sem patch,** respondeu na rota IdP e foi confirmado contra o controle:```console
$ ./cve_2026_8452_check.py https://gateway.example.com:9443
====================================================================
CVE-2026-8452 - NetScaler SAML PrefixList patch-state check
https://gateway.example.com:9443
====================================================================
>> Identifying the appliance
[ OK ] NetScaler indicators: 5 (CSP contains citrixng://)
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 500 / 43549: no size check present
idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check
[FAIL] Size check absent (via the IDP route)
====================================================================
RESULT: VULNERABLE
====================================================================
https://gateway.example.com:9443 via IDP [size-check-absent]
The size check is absent. This appliance is unpatched for
CVE-2026-8452. Upgrade to 13.1-63.21+ / 14.1-73.32+ (12.1 and
13.0 are EOL and never fixed).
====================================================================
A linha de controle vale a pena ser lida com atenção: a solicitação de 35 bytes passa na verificação de tamanho e é então rejeitada por seu IssueInstant obsoleto, enquanto a sonda de 575 bytes nunca chegou tão longe. Essa é a ordem em que todo o método se apoia — a canonicalização é executada antes da verificação de tempo, assim como é executada antes da verificação de assinatura.
Um appliance corrigido, a mesma solicitação contra o build com a correção. Apenas a linha da sonda difere — o PrefixList superdimensionado é rejeitado pelo nome, em vez de cair no erro interno:```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check [ OK ] Size check present (via the IDP route)
RESULT: PATCHED
https://vpn.example.com via IDP [size-check-present]
**Recorrendo à rota SP.** Aqui o endpoint do IdP está acessível, mas nenhuma política do IdP está associada a
este servidor virtual, portanto a rota 1 se abstém e a rota 2 responde. Quando *nenhuma* rota corresponde a uma política,
o veredito é `INCONCLUSIVE` marcado como `no-policy-match` — nunca `PATCHED`, que é exatamente o motivo pelo qual
esse veredito existe:```console
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 200 "Matching policy not found": parser not reached
sp /cgi/samlauth HTTP 500 / 43549: no size check present
sp /cgi/samlauth 35-byte control: HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
[FAIL] Size check absent (via the SP route)
RESULT: VULNERABLE
O controle detectou isso. Aqui o endpoint respondeu com a mensagem corrigida em ambos os comprimentos, então a verificação de tamanho nunca foi exercitada e a resposta de aparência decisiva é retirada. Este é o disparo do guarda de falso-positivo, e o motivo do disparo é declarado em vez de ficar subentendido:```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "Malformed Assertion": size check rejected the probe [WARN] Probe and control answered alike, so the size check was never exercised
RESULT: INCONCLUSIVE
https://sp-strict.example.com via IDP [flat-response]
The 575-byte probe and the 35-byte control got the same answer, so this endpoint replies the same way whatever it is sent and the size check was never exercised. Unknown, not patched.
**Scanning uma frota** (`--brief`), uma linha alinhada por alvo terminando com a tag de motivo. O status de saída é
`1` se qualquer alvo estiver `VULNERABLE`:````console
$ ./cve_2026_8452_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 size-check-absent
VULNERABLE https://gateway.example.com:9444 size-check-absent
PATCHED https://vpn.example.com size-check-present
INCONCLUSIVE https://sp-strict.example.com flat-response
INCONCLUSIVE https://gw-nopolicy.example.com no-policy-match
UNAFFECTED https://mgmt.example.com no-saml-endpoint
ERROR https://offline.example.com not-identified
exit: 1
Saída legível por máquina (--json), que registra cada rota tentada. verdict, reason e
detail são os campos autoritativos; attempts é a evidência bruta, portanto uma tentativa individual pode
indicar patched em um alvo cujo veredito é INCONCLUSIVE:```console
$ ./cve_2026_8452_check.py https://vpn.example.com --json
[
{
"target": "https://vpn.example.com",
"verdict": "PATCHED",
"reason": "size-check-present",
"route": "idp",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe",
"attempts": [
{
"route": "idp",
"path": "/saml/login",
"state": "patched",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe"
},
{
"route": "idp",
"path": "/saml/login",
"state": "control:known-error",
"detail": "35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check"
}
],
"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)"
]
}
]
## Vereditos
Cada veredito carrega uma tag `reason` curta que nomeia a condição por trás dele. `--brief` imprime a tag como
sua terceira coluna, e `--json` a carrega como `reason`.
| Veredito | Tag de razão | Significado |
| --- | --- | --- |
| `VULNERABLE` | `size-check-absent` | A verificação de tamanho está ausente. Este appliance não tem patch — aplique o patch. |
| `PATCHED` | `size-check-present` | A verificação de tamanho disparou no caminho de código que a sonda alcançou. **Escopo deste CVE:** isso não significa que o appliance está em uma build atual. |
| `UNAFFECTED` | `no-saml-endpoint` | Nenhum endpoint SAML respondeu neste servidor virtual, então o caminho vulnerável não é alcançável aqui. **Por vserver, não por appliance:** o SAML pode estar configurado em outro vserver ou VIP na mesma máquina. |
| `INCONCLUSIVE` | `flat-response` | O endpoint respondeu à sonda de 575 bytes e ao controle de 35 bytes de forma idêntica, então a verificação de tamanho nunca foi exercitada. A resposta de aparência decisiva é retirada — é o guarda de falso positivo disparando. |
| `INCONCLUSIVE` | `no-policy-match` | Um endpoint SAML respondeu, mas nenhuma política vinculada correspondeu à sonda, então nenhuma das rotas alcançou o canonicalizador. |
| `INCONCLUSIVE` | `other-saml-error` | Uma condição SAML reconhecida, mas não diagnóstica, rejeitou a sonda antes da verificação de tamanho — um limite de comprimento diferente, uma política de assinatura, um timestamp. |
| `INCONCLUSIVE` | `unrecognized-reply` | Uma superfície SAML respondeu com algo fora do conjunto reconhecido. |
| `ERROR` | `not-identified` | Não identificado como um NetScaler, ou inalcançável. |
Todos os quatro motivos `INCONCLUSIVE` significam a mesma coisa para a tomada de decisão — **desconhecido, não corrigido.**
Confirme com `show ns version`. A tag existe para dizer ao operador *qual* condição corrigir antes de
re-executar: aponte a sonda para um servidor virtual diferente, ou vincule uma política correspondente.
`INCONCLUSIVE` existe como um veredito distinto, com seu próprio código de saída, porque um appliance vulnerável pode
recusar-se a responder à sonda. Se a política SAML vinculada a um servidor virtual não corresponder à
solicitação da sonda, o appliance entra em curto-circuito antes do canonicalizador e não retorna nada de diagnóstico. Um
scanner que apenas falha em corresponder fica em silêncio nesse host, e o silêncio é lido como "corrigido". Esta
ferramenta o relata como desconhecido em vez disso.
### Cada veredito é confirmado contra um controle
`PATCHED` e `VULNERABLE` ambos se baseiam em uma *única* resposta distintiva, então a ferramenta verifica se a
resposta realmente depende do que foi enviado. Após uma resposta decisiva, ela repete a solicitação com uma
`PrefixList` curta de 35 bytes — abaixo de qualquer verificação de tamanho — e o veredito só se mantém se as duas respostas diferirem.
Se corresponderem, o endpoint responde da mesma forma independentemente do que recebe, a verificação de tamanho nunca foi exercitada,
e o resultado é `INCONCLUSIVE`.
Isso não é hipotético. Um provedor de serviços configurado com `samlRejectUnsignedAssertion STRICT` rejeita
a sonda por uma assinatura ausente *antes* da canonicalização, e responde com a mensagem corrigida em todos os
comprimentos. Sem o controle, tal appliance relata `PATCHED` com exit 0 — observado em uma build genuinamente
vulnerável. Agora ele relata `INCONCLUSIVE` com a tag de razão `flat-response`, e a execução afirma
explicitamente que a verificação de tamanho nunca foi exercitada. O mesmo controle captura o caso espelhado,
em que um endpoint retorna o erro interno genérico para solicitações que nunca analisou.
### Respostas reconhecidas não diagnósticas
Um endpoint SAML do NetScaler tem um grande conjunto de respostas possíveis, e apenas duas delas estabelecem o estado de patch.
A ferramenta reconhece 20 das demais e nomeia a condição na linha por rota em vez de
ecoar o corpo da resposta, por exemplo:```text
idp /saml/login HTTP 200 "post body over the appliance's maximum": a different length limit rejected the probe first
sp /cgi/samlauth HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
Três delas são limites de tamanho — um corpo de postagem superdimensionado, um RelayState superdimensionado, um nome de usuário extraído excessivamente longo. Essas são as que mais importam, porque significam que a requisição foi rejeitada por uma verificação de tamanho diferente antes de chegar à que distingue o estado do patch. Isso geralmente significa que a sonda precisa ser apontada para um servidor virtual diferente, não que o appliance esteja ok.
Uma das 20 aparece em todas as execuções da rota IdP: o message timestamp outside the appliance's skew tolerance do controle. A sonda carrega um IssueInstant fixo, então um controle que passa na verificação de tamanho é então rejeitado pela idade — uma propriedade da sonda, não do appliance. Uma resposta fora do conjunto das 20 é impressa como sua própria primeira linha, em vez de uma condição nomeada.
Todas ainda produzem INCONCLUSIVE, marcadas como other-saml-error. Reconhecer uma resposta nunca a eleva para PATCHED: apenas uma resposta explícita de build corrigido faz isso, e toda resposta não reconhecida também cai em INCONCLUSIVE — como unrecognized-reply. O reconhecimento existe para dizer a um operador por que um alvo não pôde ser classificado, não para classificá-lo.
| Código | Significado |
|---|---|
0 | Corrigido, ou não afetado no servidor virtual alvo |
1 | Pelo menos um alvo está VULNERABLE |
2 | Erro de uso (argumentos inválidos / arquivo de alvos ilegível) |
3 | Pelo menos um alvo está INCONCLUSIVE, nenhum vulnerável |
4 | Pelo menos um alvo apresentou erro, nenhum vulnerável ou inconclusivo |
2 é o código de saída próprio do argparse para uma invocação inválida, então os códigos de veredito o pulam. Um script wrapper pode, portanto, distinguir "este appliance não pôde ser classificado" (3) de "eu chamei a ferramenta de forma errada" (2), o que um esquema que sobrecarregasse o 2 não conseguiria.
Em uma varredura com vários alvos, o código é escolhido por prioridade, não pelo pior status: VULNERABLE > INCONCLUSIVE > ERROR > limpo. Portanto, um host inacessível nunca mascara um achado de vulnerabilidade no código de saída.
PATCHED e o código de saída 0 significam que a verificação de tamanho do CVE-2026-8452 está presente no caminho alcançado pela sonda. Eles não dizem nada sobre qualquer outra vulnerabilidade do NetScaler, incluindo aquelas divulgadas depois deste bug e corrigidas em builds posteriores. Não interprete o código de saída 0 desta ferramenta como um atestado de saúde para um appliance.UNAFFECTED significa "não alcançável aqui", não "este appliance está seguro"./saml/login abrange todo o appliance, mas em um servidor virtual sem nenhuma política IdP vinculada, ele responde Matching policy not found e interrompe o processamento antes do canonicalizador. Um IdP cuja política usa uma regra de expressão que a requisição da sonda não satisfaz cairá em INCONCLUSIVE em vez de dar uma resposta.INCONCLUSIVE não é um atestado de saúde. Ele é deliberadamente distinguido de PATCHED com um código de saída separado, para que o silêncio nunca seja confundido com um resultado de aprovação.INCONCLUSIVE. Quando a política SAML está em um rótulo de política alcançado por nextFactor, em vez de vinculada diretamente ao servidor virtual, uma asserção não solicitada não encontra política correspondente e interrompe o processamento antes do canonicalizador. Verificado em um build vulnerável, que reportou INCONCLUSIVE. Como SAML atrás de um primeiro fator de postura de dispositivo ou schema de login é um padrão comum, trate INCONCLUSIVE em um gateway nFactor como "provavelmente alcançável, confirme com show ns version", não como uma curiosidade.VULNERABLE confirma a ausência da verificação de tamanho, que é o estado do patch. Ele não mede até onde um atacante poderia levar a corrupção no seu build.Atualize para 13.1-63.21 ou posterior, ou 14.1-73.32 ou posterior (FIPS e NDcPP: 14.1-73.32 FIPS, ou 13.1-37.277 para 13.1-FIPS e 13.1-NDcPP).
A correção para o CVE-2026-8452 em si foi distribuída pela primeira vez em 13.1-63.18 / 14.1-72.61, conforme CTX696604, e é essa transição que esta ferramenta detecta. Esses builds foram desde então substituídos por CTX696939 (2026-08-19), que adiciona CVE-2026-19489 e CVE-2026-19490, sendo o último um bypass de autenticação pré-autenticação com CVSS 9.3. A pré-condição dele em builds a partir de 14.1-43.56 / 13.1-61.28 é uma ação SAML configurada — então um appliance no escopo do bug que esta ferramenta verifica provavelmente também está no escopo daquele, e um veredito PATCHED aqui não é motivo para adiar a atualização. Ambos os boletins são resolvidos pelos builds acima.
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 tratados como permanentemente vulneráveis e migrados para um ramo suportado.
Duas notas adicionais:
Aplique o patch em ambos os nós de um par HA. Um secundário sem patch é um appliance totalmente exposto no momento em que assume.
Escope seu inventário pela configuração SAML, não pelo tipo de servidor virtual. A redação do aviso do fornecedor (servidor virtual Gateway ou AAA) é mais ampla do que a condição de gatilho. Verifique a configuração em execução para add authentication samlAction e add authentication samlIdPProfile juntamente com add authentication vserver e add vpn vserver.
Isto é testado, não inferido. Em um appliance confirmadamente vulnerável, removemos todos os objetos SAML, vinculamos um fator de autenticação não-SAML em seu lugar e deixamos os servidores virtuais AAA ativos e atendendo: os endpoints SAML então retornaram 404 para todas as requisições. Eles não são meramente bloqueados por política sem configuração SAML — eles não existem. Então, um servidor virtual sem SAML está genuinamente fora do escopo deste bug, e UNAFFECTED em tal alvo é uma resposta real, não um ponto cego. A ressalva que ainda se aplica é a do escopo acima: ela é por servidor virtual, então confirme cada VIP em vez de concluir qualquer coisa sobre o appliance.
O CVE-2026-8452 foi lançado junto com cinco irmãos no mesmo boletim. O que vale a pena acompanhar ao lado dele é o CVE-2026-8451, um overread de memória pré-autenticação no caminho SAML IdP que tem sido ativamente explorado no mundo real. Os dois compartilham a mesma superfície de ataque, então a mesma auditoria de configuração cobre 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.