
Verificador não destrutivo de estado de patch para SolarWinds ARM CVE-2026-28326 que sonda o listener gRPC na porta TCP 55555 para reportar VULNERABLE, PATCHED ou INCONCLUSIVE.
Uma verificação de estado de patch não autenticada para CVE-2026-28326, a vulnerabilidade de execução
remota de código não autenticada no SolarWinds Access Rights Manager (ARM), corrigida no
ARM 2026.2.1
em 2026-09-17. CWE-798, CVSS 8.8 Alto
(CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). Reportado à SolarWinds por Kai Huang da Armadin.
O AV:A nesse vetor assume que a TCP 55555 é acessível apenas a partir de uma rede adjacente, que é a
postura para a qual as próprias ferramentas de firewall do ARM foram construídas; onde a porta foi aberta de forma mais ampla, a
pontuação subestima a exposição para esse ambiente.
O listener de remoting gRPC do ARM na TCP 55555 autentica pares com um certificado de cliente. Builds
não corrigidos também aceitam uma credencial de fallback derivada de uma chave hardcoded que é idêntica em todas
as instalações, o que contorna essa autenticação e expõe um caminho de desserialização inseguro. O
resultado é execução de código como a conta de serviço do ARM, que é executada como LocalSystem por padrão.
O script informa se a correção está presente em cada alvo. Ele lê os códigos de status retornados pelo
interceptor de autenticação e não usa a chave, não autentica nem envia dados para o
caminho de desserialização. Um resultado diferente de VULNERABLE não indica por si só que um alvo está
corrigido; consulte .
# single target (port defaults to 55555)
./cve_2026_28326_check.py arm.example.com
# explicit port
./cve_2026_28326_check.py arm.example.com:55555
# several targets
./cve_2026_28326_check.py arm-a.example.com arm-b.example.com
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_28326_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_28326_check.py -f targets.txt --json > results.json
Python 3.8+, apenas biblioteca padrão — sem pacotes de terceiros.
Execute a ferramenta contra o servidor ARM em vez de um coletor. Ambos os papéis escutam na 55555, mas um
coletor recusa requisições externas da mesma forma antes e depois da correção, então a ferramenta o reporta como
INCONCLUSIVE.
| Flag | Descrição |
|---|---|
TARGET | Um ou mais alvos HOST[:PORT]; a porta padrão é 55555 |
-f, --targets-file FILE | Lê alvos de um arquivo (um por linha; comentários com #) |
-p, --port PORT | Porta padrão quando um alvo omite uma (padrão: 55555) |
--timeout SECS | Timeout por sonda (padrão: 12) |
--workers N | Alvos concorrentes (padrão: 16); a saída permanece na ordem de entrada |
-b, --brief | Uma linha alinhada por alvo, para escanear muitos hosts |
--json | Emite JSON estruturado, incluindo ambas as sondas por alvo |
--no-color | Desativa saída colorida (também respeita NO_COLOR e não-TTY) |
Servidor vulnerável (saída padrão; o marcador [!] e VULNERABLE são exibidos em vermelho em um TTY):
$ ./cve_2026_28326_check.py arm.example.com
[!] arm.example.com:55555: VULNERABLE [unavailable-on-server-role]
Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)
Servidor corrigido. O build corrigido retorna um grpc-message, que a ferramenta imprime junto com o
veredito:
$ ./cve_2026_28326_check.py arm-dr.example.com
[+] arm-dr.example.com:55555: PATCHED [unauthenticated-gate]
Remoting/Send returned 16 UNAUTHENTICATED, the localhost and per-process-token gate introduced by the fix (>= 2026.2.1.7)
grpc-message: Client certificate not found. Please register your application first.
Listener que não pode ser classificado. Um coletor, ou um servidor que recusa requisições externas, responde ambas as sondas da mesma forma em qualquer build:
$ ./cve_2026_28326_check.py collector.example.com
[?] collector.example.com:55555: INCONCLUSIVE [external-requests-refused]
the listener refuses external requests, which a collector role and a restricted server both do identically on either build; scan the ARM server itself
Múltiplos alvos com --brief:
$ ./cve_2026_28326_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE arm.example.com:55555 unavailable-on-server-role
PATCHED arm-dr.example.com:55555 unauthenticated-gate
INCONCLUSIVE collector.example.com:55555 external-requests-refused
UNAFFECTED db.example.com:55555 not-arm
ERROR offline.example.com:55555 unreachable
exit: 1
Saída JSON com --json. Ambos os resultados das sondas são incluídos para que o veredito possa ser verificado contra
as respostas brutas:
$ ./cve_2026_28326_check.py arm.example.com --json
[
{
"target": "arm.example.com:55555",
"verdict": "VULNERABLE",
"reason": "unavailable-on-server-role",
"detail": "Remoting/Send returned 14 UNAVAILABLE on a listener answering as the server role, the pre-patch rejection path (<= 2026.2.0.42)",
"grpc_message": "",
"probes": [
{
"path": "/SolarWinds_ARM_GRPC_ConnectionAuthrization.ConnectionAuthorization/GetCertificate",
"status": 0,
"message": "",
"alpn": "h2",
"note": ""
},
{
"path": "/SolarWinds_ARM_Remoting.Remoting/Send",
"status": 14,
"message": "",
"alpn": "h2",
"note": ""
}
]
}
]
A verificação destina-se ao uso contra sistemas de produção:
Remoting/Send. Ambas as sondas enviam uma mensagem gRPC com corpo de comprimento zero,
e o interceptor rejeita a chamada antes que qualquer corpo de mensagem seja despachado.GetCertificate envia um código de inscrição vazio, que o servidor rejeita antes de procurar
ou remover quaisquer códigos pendentes, então a sonda não consome um código de inscrição para um cliente que está
em meio ao registro.| # | Chamada | Propósito |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | Confirma que o endpoint é ARM e está no papel de servidor |
| 2 | Remoting/Send | A sonda de estado de patch |
Em um servidor não corrigido, uma requisição anônima rejeitada é registrada em nível de debug
(Unable to validate remote request token) e uma linha informativa nomeando o par. Builds corrigidos
registram uma rejeição nomeando o par e a dica de registro citada no grpc-message acima.
Nenhum dos builds registra uma sessão.
Para procurar exploração em vez de exposição, revise os registros NetworkConnectionEntry do ARM. Uma
entrada com IsAuthenticated=false e um ClientCertThumbPrint preenchido é consistente com a
contorno de autenticação e não ocorre em tráfego normal.
A correção alterou qual status o interceptor de autenticação retorna a um par que não apresenta
certificado de cliente. Antes da correção, todo caminho de rejeição na verificação de certificado de cliente definia Unavailable.
Após a correção, a verificação de localhost mais token define Unauthenticated com uma mensagem distintiva, e
nada mais nesse caminho retorna 16. Duas chamadas anônimas distinguem os builds:
Remoting/Send | GetCertificate | Build | Veredito |
|---|---|---|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | qualquer | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | qualquer | INCONCLUSIVE |
O veredito é lido de Remoting/Send porque apenas os handlers de duplex-streaming e unary
propagam o status do interceptor; os handlers de server- e client-streaming o sobrescrevem em ambos
os builds.
A verificação observa o comportamento do caminho de código corrigido e não depende de uma string de versão. Ela não reporta qual build um servidor corrigido está executando.
O listener do ARM reinicia conexões TLS que não negociam ALPN h2, então a ferramenta o define
explicitamente. Um scanner que se conecta via TLS sem ALPN não recebe dados da porta.
VULNERABLE e PATCHED baseiam-se cada um em uma resposta observada do servidor. INCONCLUSIVE e
ERROR significam que as sondas não classificaram o alvo, então seu estado de patch é desconhecido. Por esse motivo
INCONCLUSIVE é reportado separadamente de PATCHED.
Cada veredito carrega uma tag reason curta. --brief a imprime como a terceira coluna e --json
a carrega como reason.
| Veredito | Tag de motivo | Significado |
|---|---|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send retornou 14 em um listener que responde GetCertificate como o papel de servidor. A correção não está presente. |
PATCHED | unauthenticated-gate | Remoting/Send retornou 16, o status introduzido pela correção. Aplica-se apenas a este CVE. |
INCONCLUSIVE | external-requests-refused | Ambas as sondas retornaram 14. Um papel de coletor e um servidor restrito se comportam de forma idêntica aqui em ambos os builds. Escaneie o próprio servidor ARM. |
INCONCLUSIVE | partial-response | Apenas uma sonda retornou um status, então o par não pode ser comparado. Tente novamente. |
INCONCLUSIVE | unrecognized-status | O endpoint respondeu com uma combinação de status fora do conjunto reconhecido. |
UNAFFECTED | not-arm | Nenhum serviço gRPC do ARM respondeu aqui. |
ERROR | unreachable | A conexão TCP, o handshake TLS ou a troca HTTP/2 falhou. |
Para todos os três motivos de INCONCLUSIVE o estado de patch é desconhecido. A tag de motivo indica o que
abordar antes de executar a verificação novamente.
| Código | Significado |
|---|---|
0 | Nenhum alvo era VULNERABLE |
1 | Pelo menos um alvo é VULNERABLE |
2 | Erro de uso (argumentos inválidos, alvo inválido ou arquivo de alvos ilegível) |
130 | Interrompido (Ctrl-C) |
O código de saída 0 cobre PATCHED, UNAFFECTED e todos os três motivos de INCONCLUSIVE. Para distinguir
um alvo corrigido de um que não pôde ser classificado, leia o veredito (a primeira coluna de
--brief ou o campo verdict de --json) em vez do código de saída.
INCONCLUSIVE. Este caso foi testado contra um mock que reproduz a resposta, não contra um
coletor implantado.grpc-status como um campo HPACK literal e não implementa
decodificação Huffman ou de tabela dinâmica. O núcleo C do gRPC, que o ARM usa, envia esses trailers
descomprimidos. Se um build futuro os codificar em Huffman, a ferramenta reporta INCONCLUSIVE com
partial-response.PATCHED aplica-se apenas a este CVE e não indica o status de outras vulnerabilidades
do ARM.VULNERABLE indica que a correção não está presente no caminho que a
sonda alcançou. A ferramenta não executa código no alvo e não detecta comprometimento prévio;
consulte o indicador NetworkConnectionEntry acima.Atualize para o ARM 2026.2.1.7 ou posterior, conforme as notas de versão do ARM 2026.2.1. Os builds 2026.2.0.42 e anteriores são afetados.
Notas adicionais:
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 qualquer responsabilidade e não são responsáveis por qualquer uso indevido ou dano causado por este programa.