Vérificateur non destructif de l'état des correctifs pour SolarWinds ARM CVE-2026-28326 qui sonde l'écouteur gRPC sur TCP 55555 pour signaler VULNERABLE, PATCHED ou INCONCLUSIVE.
Une vérification de l'état du correctif sans authentification pour CVE-2026-28326, la vulnérabilité
d'exécution de code à distance sans authentification dans SolarWinds Access Rights Manager (ARM), corrigée dans
ARM 2026.2.1
le 2026-09-17. CWE-798, CVSS 8.8 Élevé
(CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). Signalée à SolarWinds par Kai Huang d'Armadin.
Le AV:A de ce vecteur suppose que le TCP 55555 n'est accessible que depuis un réseau adjacent, ce qui correspond
à la posture pour laquelle les outils de pare-feu propres à ARM sont conçus ; lorsque le port a été ouvert plus largement, le
score sous-estime l'exposition pour cet environnement.
L'écouteur de remoting gRPC d'ARM sur le TCP 55555 authentifie les pairs avec un certificat client. Les versions
non corrigées acceptent également un identifiant de secours dérivé d'une clé codée en dur identique sur chaque
installation, ce qui contourne cette authentification et expose un chemin de désérialisation non sécurisé. Le
résultat est une exécution de code sous le compte de service ARM, qui s'exécute en tant que LocalSystem par défaut.
Le script indique si le correctif est présent sur chaque cible. Il lit les codes de statut renvoyés par l'intercepteur d'authentification et n'utilise pas la clé, ne s'authentifie pas et n'envoie pas de données vers le chemin de désérialisation. Un résultat autre que n'indique pas à lui seul qu'une cible est corrigée ; voir .
VULNERABLE# 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+, bibliothèque standard uniquement — aucun paquet tiers.
Exécutez l'outil contre le serveur ARM plutôt que contre un collecteur. Les deux rôles écoutent sur le 55555, mais un
collecteur refuse les requêtes externes de la même manière avant et après le correctif, de sorte que l'outil le signale comme
INCONCLUSIVE.
| Flag | Description |
|---|---|
TARGET | Une ou plusieurs cibles HOST[:PORT] ; le port par défaut est 55555 |
-f, --targets-file FILE | Lit les cibles depuis un fichier (une par ligne ; commentaires #) |
-p, --port PORT | Port par défaut lorsqu'une cible n'en précise pas (défaut : 55555) |
--timeout SECS | Délai d'attente par sonde (défaut : 12) |
--workers N | Cibles simultanées (défaut : 16) ; la sortie reste dans l'ordre d'entrée |
-b, --brief | Une seule ligne alignée par cible, pour scanner de nombreux hôtes |
--json | Émet du JSON structuré, incluant les deux sondes par cible |
--no-color | Désactive la sortie colorée (respecte aussi NO_COLOR et les non-TTY) |
Serveur vulnérable (sortie par défaut ; le marqueur [!] et VULNERABLE sont affichés en rouge sur un 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)
Serveur corrigé. La version corrigée renvoie un grpc-message, que l'outil affiche à côté du
verdict :
$ ./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.
Écouteur qui ne peut pas être classifié. Un collecteur, ou un serveur qui refuse les requêtes externes, répond aux deux sondes de la même manière sur l'une ou l'autre version :
$ ./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
Plusieurs cibles avec --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
Sortie JSON avec --json. Les résultats des deux sondes sont inclus afin que le verdict puisse être vérifié par rapport
aux réponses brutes :
$ ./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 vérification est destinée à être utilisée contre des systèmes de production :
Remoting/Send. Les deux sondes envoient un message gRPC avec un corps de longueur nulle,
et l'intercepteur rejette l'appel avant que tout corps de message ne soit distribué.GetCertificate envoie un code d'enrôlement vide, que le serveur rejette avant de rechercher
ou de supprimer tout code en attente, de sorte que la sonde ne consomme pas de code d'enrôlement pour un client en
cours d'enregistrement.| # | Appel | Objectif |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | Confirme que le point de terminaison est ARM et qu'il est dans le rôle serveur |
| 2 | Remoting/Send | La sonde d'état du correctif |
Sur un serveur non corrigé, une requête anonyme rejetée est journalisée au niveau debug
(Unable to validate remote request token) ainsi qu'une ligne informative nommant le pair. Les versions corrigées
journalisent un rejet nommant le pair et l'indice d'enregistrement cité dans le grpc-message ci-dessus.
Aucune des deux versions n'enregistre de session.
Pour rechercher une exploitation plutôt qu'une exposition, examinez les enregistrements NetworkConnectionEntry d'ARM. Une
entrée avec IsAuthenticated=false et un ClientCertThumbPrint renseigné est cohérente avec le
contournement d'authentification et ne se produit pas dans le trafic normal.
Le correctif a modifié le statut que l'intercepteur d'authentification renvoie à un pair ne présentant aucun certificat
client. Avant le correctif, chaque chemin de rejet dans la vérification du certificat client définissait Unavailable.
Après le correctif, la vérification localhost-plus-token définit Unauthenticated avec un message distinctif, et
rien d'autre sur ce chemin ne renvoie 16. Deux appels anonymes distinguent les versions :
Remoting/Send | GetCertificate | Version | Verdict |
|---|---|---|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | any | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | either | INCONCLUSIVE |
Le verdict est lu depuis Remoting/Send car seuls les gestionnaires duplex-streaming et unaires
propagent le statut de l'intercepteur ; les gestionnaires server- et client-streaming l'écrasent sur les deux
versions.
La vérification observe le comportement du chemin de code corrigé et ne s'appuie pas sur une chaîne de version. Elle n'indique pas quelle version un serveur corrigé exécute.
L'écouteur ARM réinitialise les connexions TLS qui ne négocient pas ALPN h2, de sorte que l'outil le définit
explicitement. Un scanner qui se connecte via TLS sans ALPN ne reçoit aucune donnée du port.
VULNERABLE et PATCHED sont chacun basés sur une réponse observée du serveur. INCONCLUSIVE et
ERROR signifient que les sondes n'ont pas classifié la cible, de sorte que son état de correctif est inconnu. Pour cette raison,
INCONCLUSIVE est signalé séparément de PATCHED.
Chaque verdict porte une courte balise reason. --brief l'affiche comme troisième colonne et --json
la porte comme reason.
| Verdict | Balise reason | Signification |
|---|---|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send a renvoyé 14 sur un écouteur répondant à GetCertificate en tant que rôle serveur. Le correctif n'est pas présent. |
PATCHED | unauthenticated-gate | Remoting/Send a renvoyé 16, le statut introduit par le correctif. S'applique à ce CVE uniquement. |
INCONCLUSIVE | external-requests-refused | Les deux sondes ont renvoyé 14. Un rôle collecteur et un serveur restreint se comportent de manière identique ici sur les deux versions. Scannez le serveur ARM lui-même. |
INCONCLUSIVE | partial-response | Une seule sonde a renvoyé un statut, de sorte que la paire ne peut pas être comparée. Réessayez. |
INCONCLUSIVE | unrecognized-status | Le point de terminaison a répondu avec une combinaison de statuts en dehors de l'ensemble reconnu. |
UNAFFECTED | not-arm | Aucun service gRPC ARM n'a répondu ici. |
ERROR | unreachable | La connexion TCP, la poignée de main TLS ou l'échange HTTP/2 a échoué. |
Pour les trois raisons INCONCLUSIVE, l'état du correctif est inconnu. La balise reason indique ce qu'il faut
traiter avant de relancer la vérification.
| Code | Signification |
|---|---|
0 | Aucune cible n'était VULNERABLE |
1 | Au moins une cible est VULNERABLE |
2 | Erreur d'utilisation (arguments invalides, cible invalide ou fichier de cibles illisible) |
130 | Interrompu (Ctrl-C) |
Le code de sortie 0 couvre PATCHED, UNAFFECTED et les trois raisons INCONCLUSIVE. Pour distinguer
une cible corrigée d'une cible qui n'a pas pu être classifiée, lisez le verdict (la première colonne de
--brief ou le champ verdict de --json) plutôt que le code de sortie.
INCONCLUSIVE. Ce cas a été testé contre un mock qui reproduit la réponse, et non contre un
collecteur déployé.grpc-status comme champ HPACK littéral et n'implémente pas
le décodage Huffman ou de table dynamique. Le cœur C de gRPC, qu'ARM utilise, envoie ces trailers
non compressés. Si une future version les encode en Huffman, l'outil signale INCONCLUSIVE avec
partial-response.PATCHED s'applique à ce CVE uniquement et n'indique pas le statut d'autres vulnérabilités
ARM.VULNERABLE indique que le correctif n'est pas présent sur le chemin que
la sonde a atteint. L'outil n'exécute pas de code sur la cible et ne détecte pas une compromission antérieure ;
voir l'indicateur NetworkConnectionEntry ci-dessus.Mettez à niveau vers ARM 2026.2.1.7 ou une version ultérieure, conformément aux notes de version ARM 2026.2.1. Les versions 2026.2.0.42 et antérieures sont affectées.
Notes complémentaires :
Ce code est distribué sous une licence MIT.
L'utilisation de cet outil pour attaquer des cibles sans consentement mutuel préalable est illégale. Il est de la responsabilité de l'utilisateur final de respecter toutes les lois locales, étatiques et fédérales applicables. Les développeurs n'assument aucune responsabilité et ne sont pas responsables de toute mauvaise utilisation ou de tout dommage causé par ce programme.