
Comprobador no destructivo del estado de parcheo para SolarWinds ARM CVE-2026-28326 que sondea el listener gRPC en TCP 55555 para informar VULNERABLE, PATCHED o INCONCLUSIVE.
Una comprobación del estado de parcheo sin autenticación para CVE-2026-28326, la vulnerabilidad de
ejecución remota de código sin autenticación en SolarWinds Access Rights Manager (ARM), corregida en
ARM 2026.2.1
el 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 a SolarWinds por Kai Huang de Armadin.
El AV:A en ese vector asume que TCP 55555 es accesible solo desde una red adyacente, que es la
postura para la que están diseñadas las propias herramientas de firewall de ARM; donde el puerto se
ha abierto de forma más amplia, la puntuación subestima la exposición para ese entorno.
El listener de remoting gRPC de ARM en TCP 55555 autentica a los pares con un certificado de cliente.
Las compilaciones sin parchear también aceptan una credencial de respaldo derivada de una clave
hardcodeada que es idéntica en todas las instalaciones, lo que omite esa autenticación y expone una
ruta de deserialización insegura. El resultado es la ejecución de código como la cuenta de servicio
de ARM, que se ejecuta como LocalSystem de forma predeterminada.
El script informa si la corrección está presente en cada objetivo. Lee los códigos de estado
devueltos por el interceptor de autenticación y no utiliza la clave, no se autentica ni envía datos a
la ruta de deserialización. Un resultado distinto de VULNERABLE no indica por sí mismo que un
objetivo esté parcheado; 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+, solo biblioteca estándar — sin paquetes de terceros.
Ejecute la herramienta contra el servidor ARM en lugar de contra un colector. Ambos roles escuchan
en 55555, pero un colector rechaza las solicitudes externas de la misma manera antes y después de la
corrección, por lo que la herramienta lo reporta como INCONCLUSIVE.
| Flag | Descripción |
|---|---|
TARGET | Uno o más objetivos HOST[:PORT]; el puerto predeterminado es 55555 |
-f, --targets-file FILE | Lee los objetivos desde un archivo (uno por línea; comentarios con #) |
-p, --port PORT | Puerto predeterminado cuando un objetivo omite uno (predeterminado: 55555) |
--timeout SECS | Tiempo de espera por sonda (predeterminado: 12) |
--workers N | Objetivos concurrentes (predeterminado: 16); la salida mantiene el orden de entrada |
-b, --brief | Una sola línea alineada por objetivo, para escanear muchos hosts |
--json | Emite JSON estructurado, incluyendo ambas sondas por objetivo |
--no-color | Desactiva la salida con color (también respeta NO_COLOR y entornos no TTY) |
Servidor vulnerable (salida predeterminada; el marcador [!] y VULNERABLE se muestran en rojo en
una 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 parcheado. La compilación corregida devuelve un grpc-message, que la herramienta imprime
junto al veredicto:
$ ./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 no se puede clasificar. Un colector, o un servidor que rechaza solicitudes externas, responde a ambas sondas de la misma manera en cualquiera de las compilaciones:
$ ./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últiples objetivos con --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
Salida JSON con --json. Se incluyen ambos resultados de sonda para que el veredicto pueda
contrastarse con las respuestas sin procesar:
$ ./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": ""
}
]
}
]
La comprobación está pensada para usarse contra sistemas de producción:
Remoting/Send. Ambas sondas envían un mensaje gRPC con un
cuerpo de longitud cero, y el interceptor rechaza la llamada antes de que se despache ningún cuerpo
de mensaje.GetCertificate envía un código de inscripción vacío, que el servidor rechaza
antes de buscar o eliminar cualquier código pendiente, por lo que la sonda no consume un código de
inscripción de un cliente que esté en pleno registro.| # | Llamada | Propósito |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | Confirma que el endpoint es ARM y está en el rol de servidor |
| 2 | Remoting/Send | La sonda del estado de parcheo |
En un servidor sin parchear, una solicitud anónima rechazada se registra a nivel de depuración
(Unable to validate remote request token) y una línea informativa que nombra al par. Las
compilaciones parcheadas registran un rechazo que nombra al par y la sugerencia de registro citada
en el grpc-message anterior. Ninguna de las compilaciones registra una sesión.
Para buscar explotación en lugar de exposición, revise los registros NetworkConnectionEntry de ARM.
Una entrada con IsAuthenticated=false y un ClientCertThumbPrint poblado es consistente con el
bypass de autenticación y no ocurre en el tráfico normal.
La corrección cambió qué estado devuelve el interceptor de autenticación a un par que no presenta
certificado de cliente. Antes del parche, todas las rutas de rechazo en la comprobación del
certificado de cliente establecían Unavailable. Después del parche, la comprobación de
localhost-más-token establece Unauthenticated con un mensaje distintivo, y nada más en esa ruta
devuelve 16. Dos llamadas anónimas distinguen las compilaciones:
Remoting/Send | GetCertificate | Compilación | Veredicto |
|---|---|---|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | cualquiera | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | cualquiera | INCONCLUSIVE |
El veredicto se lee de Remoting/Send porque solo los manejadores de flujo dúplex y unario
propagan el estado del interceptor; los manejadores de flujo de servidor y de cliente lo sobrescriben
en ambas compilaciones.
La comprobación observa el comportamiento de la ruta de código parcheada y no se basa en una cadena de versión. No informa qué compilación está ejecutando un servidor parcheado.
El listener de ARM restablece las conexiones TLS que no negocian ALPN h2, por lo que la herramienta
lo establece explícitamente. Un escáner que se conecta por TLS sin ALPN no recibe datos del puerto.
VULNERABLE y PATCHED se basan cada uno en una respuesta observada del servidor. INCONCLUSIVE y
ERROR significan que las sondas no clasificaron el objetivo, por lo que su estado de parcheo es
desconocido. Por este motivo, INCONCLUSIVE se informa por separado de PATCHED.
Cada veredicto lleva una etiqueta reason corta. --brief la imprime como tercera columna y
--json la incluye como reason.
| Veredicto | Etiqueta de motivo | Significado |
|---|---|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send devolvió 14 en un listener que responde a GetCertificate como el rol de servidor. La corrección no está presente. |
PATCHED | unauthenticated-gate | Remoting/Send devolvió 16, el estado introducido por la corrección. Aplica solo a este CVE. |
INCONCLUSIVE | external-requests-refused | Ambas sondas devolvieron 14. Un rol de colector y un servidor restringido se comportan de forma idéntica aquí en ambas compilaciones. Escanee el propio servidor ARM. |
INCONCLUSIVE | partial-response | Solo una sonda devolvió un estado, por lo que el par no se puede comparar. Reintente. |
INCONCLUSIVE | unrecognized-status | El endpoint respondió con una combinación de estados fuera del conjunto reconocido. |
UNAFFECTED | not-arm | Ningún servicio gRPC de ARM respondió aquí. |
ERROR | unreachable | Falló la conexión TCP, el handshake TLS o el intercambio HTTP/2. |
Para los tres motivos de INCONCLUSIVE el estado de parcheo es desconocido. La etiqueta de motivo
indica qué hay que resolver antes de ejecutar la comprobación de nuevo.
| Código | Significado |
|---|---|
0 | Ningún objetivo era VULNERABLE |
1 | Al menos un objetivo es VULNERABLE |
2 | Error de uso (argumentos incorrectos, objetivo no válido o archivo de objetivos ilegible) |
130 | Interrumpido (Ctrl-C) |
El código de salida 0 abarca PATCHED, UNAFFECTED y los tres motivos de INCONCLUSIVE. Para
distinguir un objetivo parcheado de uno que no se pudo clasificar, lea el veredicto (la primera
columna de --brief o el campo verdict de --json) en lugar del código de salida.
INCONCLUSIVE. Este caso se probó contra un mock que reproduce la respuesta, no
contra un colector desplegado.grpc-status como un campo HPACK literal y no
implementa decodificación Huffman ni de tabla dinámica. El núcleo C de gRPC, que ARM utiliza, envía
estos trailers sin comprimir. Si una compilación futura los codifica con Huffman, la herramienta
informa INCONCLUSIVE con partial-response.PATCHED aplica solo a este CVE y no indica el estado de otras vulnerabilidades de ARM.VULNERABLE indica que la corrección no está presente en la ruta que
alcanzó la sonda. La herramienta no ejecuta código en el objetivo ni detecta un compromiso previo;
consulte el indicador NetworkConnectionEntry anterior.Actualice a ARM 2026.2.1.7 o posterior, según las notas de la versión ARM 2026.2.1. Las compilaciones 2026.2.0.42 y anteriores están afectadas.
Notas adicionales:
Este código se distribuye bajo una licencia MIT.
El uso de esta herramienta para atacar objetivos sin consentimiento mutuo previo es ilegal. Es responsabilidad del usuario final cumplir con todas las leyes locales, estatales y federales aplicables. Los desarrolladores no asumen ninguna responsabilidad y no son responsables de ningún mal uso o daño causado por este programa.