
Verificatore non distruttivo dello stato delle patch per SolarWinds ARM CVE-2026-28326 che sonda il listener gRPC su TCP 55555 per segnalare VULNERABLE, PATCHED o INCONCLUSIVE.
Un controllo dello stato della patch non autenticato per CVE-2026-28326, la vulnerabilità di
esecuzione di codice in remoto non autenticata in SolarWinds Access Rights Manager (ARM), corretta in
ARM 2026.2.1
il 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). Segnalata a SolarWinds da Kai Huang di Armadin.
L'AV:A in quel vettore presuppone che TCP 55555 sia raggiungibile solo da una rete adiacente, che è la
postura per cui sono costruiti gli stessi strumenti firewall di ARM; dove la porta è stata aperta in modo più ampio, il
punteggio sottostima l'esposizione per quell'ambiente.
Il listener di remoting gRPC di ARM su TCP 55555 autentica i peer con un certificato client. Le build
non aggiornate accettano anche una credenziale di fallback derivata da una chiave hardcoded identica su ogni
installazione, che aggira tale autenticazione ed espone un percorso di deserializzazione non sicuro. Il
risultato è l'esecuzione di codice come account del servizio ARM, che per impostazione predefinita viene eseguito come LocalSystem.
Lo script segnala se la correzione è presente su ciascun target. Legge i codici di stato restituiti
dall'intercettore di autenticazione e non utilizza la chiave, non si autentica e non invia dati al
percorso di deserializzazione. Un risultato diverso da VULNERABLE non indica di per sé che un target sia
corretto; vedere .
# 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 libreria standard — nessun pacchetto di terze parti.
Eseguire lo strumento contro il server ARM anziché contro un collector. Entrambi i ruoli ascoltano sulla 55555, ma un
collector rifiuta le richieste esterne allo stesso modo prima e dopo la correzione, quindi lo strumento lo segnala come
INCONCLUSIVE.
| Flag | Descrizione |
|---|---|
TARGET | Uno o più target HOST[:PORT]; la porta predefinita è 55555 |
-f, --targets-file FILE | Legge i target da un file (uno per riga; commenti con #) |
-p, --port PORT | Porta predefinita quando un target ne omette una (predefinita: 55555) |
--timeout SECS | Timeout per singola sonda (predefinito: 12) |
--workers N | Target concorrenti (predefinito: 16); l'output mantiene l'ordine di input |
-b, --brief | Una singola riga allineata per target, per scansionare molti host |
--json | Emette JSON strutturato, includendo entrambe le sonde per target |
--no-color | Disabilita l'output colorato (rispetta anche NO_COLOR e i non-TTY) |
Server vulnerabile (output predefinito; il marcatore [!] e VULNERABLE sono mostrati in rosso su 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)
Server corretto. La build corretta restituisce un grpc-message, che lo strumento stampa insieme al
verdetto:
$ ./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 che non può essere classificato. Un collector, o un server che rifiuta le richieste esterne, risponde a entrambe le sonde allo stesso modo su entrambe le 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
Target multipli 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
Output JSON con --json. Sono inclusi i risultati di entrambe le sonde, così il verdetto può essere verificato rispetto
alle risposte grezze:
$ ./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": ""
}
]
}
]
Il controllo è pensato per l'uso su sistemi di produzione:
Remoting/Send. Entrambe le sonde inviano un messaggio gRPC con corpo di lunghezza zero,
e l'intercettore rifiuta la chiamata prima che qualsiasi corpo del messaggio venga inoltrato.GetCertificate invia un codice di registrazione vuoto, che il server rifiuta prima di cercare
o rimuovere qualsiasi codice in sospeso, quindi la sonda non consuma un codice di registrazione per un client che è
in fase di registrazione.| # | Chiamata | Scopo |
|---|---|---|
| 1 | ConnectionAuthorization/GetCertificate | Conferma che l'endpoint è ARM e si trova nel ruolo server |
| 2 | Remoting/Send | La sonda sullo stato della patch |
Su un server non aggiornato una richiesta anonima rifiutata viene registrata a livello debug
(Unable to validate remote request token) e una riga informativa che nomina il peer. Le build corrette
registrano un rifiuto che nomina il peer e il suggerimento di registrazione citato nel grpc-message sopra.
Nessuna delle due build registra una sessione.
Per cercare lo sfruttamento anziché l'esposizione, esaminare i record NetworkConnectionEntry di ARM. Una
voce con IsAuthenticated=false e un ClientCertThumbPrint popolato è coerente con il
bypass dell'autenticazione e non si verifica nel traffico normale.
La correzione ha cambiato quale stato l'intercettore di autenticazione restituisce a un peer che non presenta alcun
certificato client. Prima della patch, ogni percorso di rifiuto nel controllo del certificato client impostava Unavailable.
Dopo la patch, il controllo localhost-più-token imposta Unauthenticated con un messaggio distintivo, e
nient'altro su quel percorso restituisce 16. Due chiamate anonime distinguono le build:
Remoting/Send | GetCertificate | Build | Verdetto |
|---|---|---|---|
14 UNAVAILABLE | 0 OK | <= 2026.2.0.42 | VULNERABLE |
16 UNAUTHENTICATED | any | >= 2026.2.1.7 | PATCHED |
14 UNAVAILABLE | 14 UNAVAILABLE | either | INCONCLUSIVE |
Il verdetto viene letto da Remoting/Send perché solo gli handler duplex-streaming e unary
propagano lo stato dell'intercettore; gli handler server- e client-streaming lo sovrascrivono su entrambe
le build.
Il controllo osserva il comportamento del percorso di codice corretto e non si basa su una stringa di versione. Non segnala quale build stia eseguendo un server corretto.
Il listener ARM reimposta le connessioni TLS che non negoziano ALPN h2, quindi lo strumento lo imposta
esplicitamente. Uno scanner che si connette tramite TLS senza ALPN non riceve dati dalla porta.
VULNERABLE e PATCHED si basano ciascuno su una risposta osservata del server. INCONCLUSIVE e
ERROR significano che le sonde non hanno classificato il target, quindi il suo stato di patch è sconosciuto. Per questo motivo
INCONCLUSIVE viene segnalato separatamente da PATCHED.
Ogni verdetto porta un breve tag reason. --brief lo stampa come terza colonna e --json
lo riporta come reason.
| Verdetto | Tag reason | Significato |
|---|---|---|
VULNERABLE | unavailable-on-server-role | Remoting/Send ha restituito 14 su un listener che risponde a GetCertificate come ruolo server. La correzione non è presente. |
PATCHED | unauthenticated-gate | Remoting/Send ha restituito 16, lo stato introdotto dalla correzione. Si applica solo a questa CVE. |
INCONCLUSIVE | external-requests-refused | Entrambe le sonde hanno restituito 14. Un ruolo collector e un server limitato si comportano in modo identico qui su entrambe le build. Scansionare il server ARM stesso. |
INCONCLUSIVE | partial-response | Solo una sonda ha restituito uno stato, quindi la coppia non può essere confrontata. Riprovare. |
INCONCLUSIVE | unrecognized-status | L'endpoint ha risposto con una combinazione di stati al di fuori dell'insieme riconosciuto. |
UNAFFECTED | not-arm | Qui non ha risposto alcun servizio gRPC ARM. |
ERROR | unreachable | La connessione TCP, l'handshake TLS o lo scambio HTTP/2 sono falliti. |
Per tutti e tre i motivi di INCONCLUSIVE lo stato della patch è sconosciuto. Il tag reason indica cosa
affrontare prima di eseguire nuovamente il controllo.
| Codice | Significato |
|---|---|
0 | Nessun target era VULNERABLE |
1 | Almeno un target è VULNERABLE |
2 | Errore di utilizzo (argomenti errati, target non valido o file dei target non leggibile) |
130 | Interrotto (Ctrl-C) |
Il codice di uscita 0 copre PATCHED, UNAFFECTED e tutti e tre i motivi di INCONCLUSIVE. Per distinguere
un target corretto da uno che non è stato possibile classificare, leggere il verdetto (la prima colonna di
--brief o il campo verdict di --json) anziché il codice di uscita.
INCONCLUSIVE. Questo caso è stato testato contro un mock che riproduce la risposta, non contro un
collector distribuito.grpc-status come campo HPACK letterale e non implementa la decodifica
Huffman o della tabella dinamica. Il core C di gRPC, che ARM utilizza, invia questi trailer
non compressi. Se una build futura li codificasse con Huffman, lo strumento segnalerebbe INCONCLUSIVE con
partial-response.PATCHED si applica solo a questa CVE e non indica lo stato di altre vulnerabilità
di ARM.VULNERABLE indica che la correzione non è presente sul percorso che la
sonda ha raggiunto. Lo strumento non esegue codice sul target e non rileva compromissioni precedenti;
vedere l'indicatore NetworkConnectionEntry sopra.Aggiornare ad ARM 2026.2.1.7 o successivo, secondo le note di rilascio di ARM 2026.2.1. Le build 2026.2.0.42 e precedenti sono interessate.
Note aggiuntive:
Questo codice è distribuito sotto una licenza MIT.
L'uso di questo strumento per attaccare target senza previo consenso reciproco è illegale. È responsabilità dell'utente finale rispettare tutte le leggi locali, statali e federali applicabili. Gli sviluppatori non si assumono alcuna responsabilità e non sono responsabili per qualsiasi uso improprio o danno causato da questo programma.