
Rilevamento parallelo di servizi SSH e strumento di audit di sicurezza che esegue scansioni su qualsiasi porta, valida i banner SSH e verifica i metodi di autenticazione, la crittografia debole, la vulnerabilità Terrapin e le chiavi host riutilizzate tra host e intervalli CIDR.
Trova ogni servizio SSH sulla tua rete, valuta se soddisfa i tuoi standard e ricevi una notifica quando qualcosa cambia.
sshfinder è un singolo file Python senza dipendenze richieste. Puntalo su
un intervallo CIDR e scopre SSH ovunque sia effettivamente in ascolto — non
solo sulla porta 22 — conferma che ciascuno parli davvero SSH, valuta la sua
postura crittografica e restituisce un codice di uscita non zero quando
qualcosa viola la tua policy.
La maggior parte dei team non riesce a rispondere a tre domande sul proprio parco SSH:
Gli strumenti esistenti rispondono ciascuno a parte di questo e si fermano:
Questo divario — scoperta e valutazione e un verdetto, in un unico artefatto — è
ciò che questo strumento esiste per colmare. Se devi solo controllare un singolo host che
già conosci, usa ssh-audit; va
più in profondità su un singolo servizio di quanto faccia questo.
I penetration tester troveranno utili l'audit e il pivot SOCKS, ma lo strumento è modellato attorno all'esecuzione ripetuta della stessa scansione contro un parco che possiedi, non attorno a un impegno una tantum.
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
Nessuna installazione, nessuna dipendenza. Richiede Python 3.9+.
Le tre cose che fa, in tre comandi:
# 1. INVENTARIO — che SSH c'è in giro?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. VERDETTO — soddisfa il nostro standard? (esce con 3 se no)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. DERIVA — cosa è cambiato da ieri sera?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
La scansione di tutte le 65535 porte è l'impostazione predefinita, perché un servizio SSH su una porta non standard è esattamente quello che nessuno ha annotato. Ogni porta aperta viene etichettata, quindi una porta aperta non viene mai contata silenziosamente come SSH:
=== 10.0.0.5 ===
open: 10.0.0.5:22 [SSH], 10.0.0.5:8080 [not ssh]
SSH 10.0.0.5:22 (SSH-2.0-OpenSSH_7.4)
La conferma è un vero scambio di identificazione RFC 4253, non uno sguardo ai primi byte sul filo. I server che stampano prima un banner legale, che aspettano che il client si identifichi, o il cui banner arriva suddiviso tra segmenti TCP vengono tutti riconosciuti correttamente — ciascuno di questi è un falso negativo in un'implementazione ingenua.
Aggiungi --audit per il quadro completo di ogni servizio:
SSH 10.0.0.5:22 (SSH-2.0-OpenSSH_7.4)
host key: ssh-ed25519 SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
auth: publickey, password [!] password auth enabled
[!] Terrapin (CVE-2023-48795): VULNERABLE
[!] weak ciphers: aes128-cbc
aes128-cbc [weak]: CBC mode is vulnerable to the SSH plaintext-recovery attack (CVE-2008-5161) and, …
Chiavi host SSH condivise (possibili host condivisi/clonati):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
Quell'ultimo blocco merita attenzione: una chiave host riutilizzata tra macchine di solito significa VM clonate o un'immagine condivisa, e significa che compromettere un host compromette l'identità di tutti.
OpenSSH 10.0 ha reso mlkem768x25519-sha256 lo scambio di chiavi predefinito, e 10.1
avverte che le sessioni classiche sono aperte alla cattura store now, decrypt later.
--pq-report risponde direttamente alla domanda a livello di parco, usando solo il
KEXINIT — quindi non richiede librerie di terze parti:
python sshfinder.py 10.0.0.0/24 -p 22,2222 --pq-report
Post-quantum readiness:
1/3 service(s) negotiate post-quantum key exchange with a current client
[!] no PQ key exchange offered (1):
10.0.0.2:22
[!] pre-standard PQ only (1) - looks post-quantum but is not:
10.0.0.3:22
2 service(s) exposed to store-now-decrypt-later capture; upgrade to OpenSSH 9.0+
La categoria pre-standard è quella che coglie di sorpresa. Un server
che pubblicizza [email protected] o una bozza Kyber sembra
post-quantistico in un dump di algoritmi, ma OpenSSH ha eliminato quel set di parametri
ritirato nel 2020 — quindi un client attuale non trova alcun metodo comune e ripiega
sulla crittografia classica. Contato come pronto, sarebbe peggio che non guardare
affatto.
Un report descrive un problema. Una policy asserisce un problema e può far fallire una build:
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline; echo "exit $?"
Policy 'baseline':
No password login, no Terrapin exposure, no weak algorithms.
1/3 service(s) pass
[FAIL] 1 service(s):
10.0.0.3:22
- password_auth: password login accepted: publickey, password
- terrapin: vulnerable to Terrapin (CVE-2023-48795)
- post_quantum (warn): post-quantum readiness is absent, ready required
[warn] 1 service(s):
10.0.0.2:22
- post_quantum (warn): post-quantum readiness is absent, ready required
exit 3
Tre policy sono incluse — baseline, strict e pq — denominate per il
risultato che impongono piuttosto che per una distribuzione. Le regole portano una severità
fail o warn e --fail-on decide quali soglie, quindi un team può adottare
uno standard più severo prima come avviso e promuoverlo in seguito senza modificare nulla.
Scrivi le tue come JSON:
{
"name": "house-rules",
"description": "What we expect of every SSH service.",
"rules": [
{"check": "password_auth", "severity": "fail"},
{"check": "terrapin", "severity": "fail"},
{"check": "post_quantum", "require": "ready", "severity": "warn"},
{"check": "forbid", "field": "ciphers",
"algorithms": ["3des-cbc", "arcfour"], "severity": "fail"},
{"check": "require", "field": "kex_algorithms",
"algorithms": ["curve25519-sha256"], "severity": "fail"}
]
}
Controlli: password_auth, terrapin, weak_algorithms, post_quantum (con
require: ready, legacy o absent), e forbid / require su un
field di kex_algorithms, host_key_algorithms, ciphers o macs.
Qualsiasi altra cosa è un errore grave quando la policy viene caricata, prima che la scansione inizi. Una soglia che salta silenziosamente una regola che non comprende è peggio di nessuna soglia: l'esecuzione diventa verde e nessuno scopre che il controllo non è mai stato eseguito.
$ sshfinder 10.0.0.0/24 --policy house.json
sshfinder: error: rule 1: unknown check 'pasword_auth'
(known: forbid, password_auth, post_quantum, require, terrapin, weak_algorithms)
Eseguilo ogni notte contro il report di ieri e vedi solo ciò che si è mosso:
# Notturno, in cron:
python sshfinder.py 10.0.0.0/24 -p 22,2222 --audit --json -o today.json
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
Baseline drift (vs yesterday.json):
[alert] 2 change(s):
10.0.0.5:22 SHA256:T/ZM4jO... -> SHA256:9aKm2Qx...; expected only after a rebuild or key rotation
10.0.0.3:22 password login is now accepted
[added] 1 change(s):
10.0.0.9:2222 new SSH service (SSH-2.0-OpenSSH_9.6)
[improved] 1 change(s):
10.0.0.7:22 post-quantum readiness rose from absent to ready
Una chiave host che è cambiata è il segnale più importante qui — prevista solo dopo una ricostruzione o una rotazione delle chiavi, e merita un'occhiata ogni altra volta.
Solo alert controlla --fail-on-drift. Un host dismesso è normale
avvicendamento, e far fallire un lavoro notturno su questo addestrerebbe tutti a ignorare il
risultato.
Il confronto è attento a non inventare cambiamenti. Un campo che nessuna scansione ha misurato non viene mai segnalato come cambiato, vengono confrontati solo gli host presenti in entrambe le scansioni, e una baseline che contiene le impronte delle chiavi host fa eseguire anche la sonda profonda a questa scansione — quindi una riscansione superficiale non viene mai letta come se ogni chiave fosse sparita.
--format text|json|sarif|csv, opzionalmente scritto su un file con -o.
csv — una riga per ogni servizio SSH confermato. La forma in cui un inventario
degli asset viene effettivamente ordinato e filtrato.json — il report nativo e il formato di input per --baseline.sarif — SARIF 2.1.0, validato contro lo schema OASIS. I risultati sono
ancorati a posizioni logiche host:port e portano impronte stabili, quindi
un consumatore tiene traccia dello stesso risultato tra esecuzioni notturne piuttosto che aprire
un nuovo avviso ogni volta.--stream — eventi JSON delimitati da nuove righe emessi mentre ogni porta si apre
e ogni servizio viene confermato, quindi una pipeline può agire sul primo risultato
mentre la scansione è ancora in esecuzione:python sshfinder.py 10.0.0.0/24 --stream -q | jq -c 'select(.event=="ssh")'
{"event":"ssh","elapsed":0.164,"host":"10.0.0.5","port":22,"banner":"SSH-2.0-OpenSSH_9.6"}
{"event":"ssh","elapsed":0.881,"host":"10.0.0.9","port":2222,"banner":"SSH-2.0-dropbear"}
Su SARIF e GitHub code scanning. I risultati SARIF devono portare una posizione dell'artefatto non vuota o
upload-sarifrifiuta il file, quindi viene emesso un URI sinteticossh://host:portaccanto alla posizione logica. Non risolve a un file nel tuo repository, quindi gli avvisi appaiono senza un'ancora di codice. Trattalo come SARIF per gli strumenti di sicurezza in generale — il visualizzatore SARIF di VS Code, Azure DevOps, archiviazione — non come un modo per annotare una diff.
Il punto centrale delle funzionalità di policy e deriva, quindi vale la pena dichiararli con precisione:
Un errore grave supera un verdetto di policy, e un verdetto di policy supera la deriva.
Se nulla era raggiungibile, la scansione non ha dimostrato nulla sulla conformità in
entrambi i casi, quindi ottieni 1 piuttosto che un pass o fail fuorviante; e fallire
uno standard dichiarato è un risultato più specifico di "qualcosa è cambiato".
La scansione principale, la validazione del banner e le parti senza dipendenze di --audit
(inventario degli algoritmi, flag di crittografia debole, Terrapin, prontezza post-quantistica)
non richiedono altro che Python 3.9+.
# Consigliato: sblocca le impronte delle chiavi host, l'enumerazione dei metodi di autenticazione e
# la correlazione delle chiavi condivise in --audit, oltre a --validate paramiko.
pip install -r requirements.txt
# Opzionale, solo per scansioni SYN a metà apertura (richiede root):
pip install scapy>=2.5
--scan-method auto seleziona la scansione SYN quando viene eseguita come root con Scapy
installato, altrimenti ripiega sulla scansione connect senza privilegi.
SOCKS non può essere combinato con una scansione SYN — SOCKS5 trasporta flussi TCP, non
pacchetti grezzi.
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
La scoperta, lo scambio di banner e l'audit attraversano tutti il pivot, quindi i risultati non sono mai mezzo-tunnelizzati. Un proxy non raggiungibile viene segnalato come errore di scansione, mai come "nessun SSH trovato".
--max-rate limita le sonde al secondo nell'intera scansione. La concorrenza
limita quante connessioni sono aperte contemporaneamente; questo limita la velocità con cui
se ne avviano di nuove, che è il tetto che devi poter promettere prima di scansionare
qualsiasi cosa sotto regole di ingaggio.
Note di implementazione, per quando il comportamento sopra richiede una spiegazione.
Il motore di scansione. Ogni connessione in volo è guidata da un singolo thread tramite un
loop di eventi del sistema operativo (epoll/kqueue/select), quindi la concorrenza costa un
descrittore di file piuttosto che un thread del sistema operativo, e ogni host viene risolto una volta
piuttosto che una volta per porta. Una scansione completa 1–65535 gira circa 6× più veloce di un
design a thread pool.
Porte SSH per prime. Le poche porte su cui SSH vive realmente (22, 2222, 22222, …) vengono sondate all'inizio di ogni scansione. In una scansione completa, il primo servizio SSH confermato appare in circa 0,2 secondi invece di 16.
Una stretta di mano per servizio. Il socket che ha scoperto una porta aperta viene passato direttamente allo scambio di banner, quindi un servizio SSH confermato costa una stretta di mano TCP piuttosto che due.
Timeout adattivo. Le sonde attendono quanto il percorso giustifica, usando lo
stimatore del tempo di andata e ritorno smussato di RFC 6298 — quello che TCP stesso usa — alimentato
da ogni sonda risposta e condiviso nell'intera scansione. --timeout diventa
un tetto piuttosto che un costo fisso: su un host vivo ma per lo più filtrato questo vale
circa 5× con risultati identici. Solo una risposta definitiva gli insegna qualcosa;
un timeout non dice nulla sul percorso e non viene mai reimmesso.
Due punti mantengono deliberatamente l'intero tetto. Lo scambio di banner e l'audit non si adattano mai, perché la velocità con cui un host completa una stretta di mano TCP non dice nulla sulla velocità con cui il suo demone SSH compone un saluto. Nemmeno la ri-sonda finale delle porte usuali di SSH, poiché un SYN perso lì è l'unica perdita che costa davvero a questo strumento un risultato.
Uscita anticipata. Un host che non risponde affatto nelle sue prime poche
centinaia di sonde viene segnalato come non reattivo piuttosto che consumare un timeout per
ogni porta rimanente. Poiché le porte di SSH vengono scansionate per prime, un servizio vivo viene
sempre visto prima che questo possa scattare; --no-early-exit forza l'intero intervallo.
Limitato per progettazione. Un budget di socket a livello di processo derivato dal
limite dei descrittori di file impedisce a una scansione di grandi dimensioni di esaurire i descrittori e
segnalare erroneamente servizi vivi come filtrati. L'espansione dei target controlla la
dimensione di una rete prima di materializzarla, quindi un /8 fuori posto viene rifiutato in millisecondi
piuttosto che consumare un gigabyte di memoria.
Giudizi algoritmici curati. Ogni algoritmo segnalato proviene da una
tabella esplicita con una severità e una motivazione dichiarata, non da una catena di test
di sottostringhe. I nomi vengono normalizzati prima, quindi un suffisso del fornitore non può
sfuggire a un controllo — [email protected] è CBC a prescindere da chi lo
fornisce — e i marcatori di negoziazione come [email protected] non vengono mai valutati come
algoritmi. Questa tabella è ciò su cui poggiano in ultima analisi la soglia di policy e il confronto di deriva.
Ctrl+C robusto. Onorato anche su Windows, dove un'attesa di thread illimitata normalmente lo inghiotte: la prima pressione si ferma con grazia e restituisce risultati parziali, una seconda forza un'uscita immediata.
OpenSSH_9.6p1 su Ubuntu 24.04 è patchato
contro la maggior parte di ciò che i database pubblici attribuiscono a 9.6p1. Questa è una
macchina di falsi positivi — è il motivo per cui ssh-audit ha rimosso il proprio rilevamento CVE
basato sulla versione, e perché Tenable fornisce un plugin il cui intero lavoro è rilevare
il backporting che lo rompe. Viene valutato solo ciò che un server pubblicizza
effettivamente.nmap sulla scansione delle porte in generale, o con masscan e
zmap a scala internet. Quei problemi sono risolti.La suite di test usa solo la libreria standard, quindi gira su un interprete nudo:
python -m unittest discover -s tests
Installa le dipendenze runtime per esercitare anche i test di audit basati su Paramiko, che si saltano da soli quando Paramiko manca:
pip install -r requirements-dev.txt
python -m unittest discover -s tests
Scansiona solo sistemi che possiedi o per cui sei esplicitamente autorizzato a fare test. La scansione non autorizzata potrebbe essere illegale nella tua giurisdizione.
| Strumento | Scopre SSH | Lo valuta | Su un intero parco |
|---|
nmap | sì | superficiale, tramite script NSE | sì |
ssh-audit | no — gli fornisci un singolo host | in profondità | no |
masscan / zmap | a scala internet | no | sì |
sshfinder | sì | sì | sì |
| Codice | Significato |
|---|
0 | Successo. Non trovare nulla è comunque successo — un parco vuoto non è un errore. |
1 | Errore grave: ogni target non è riuscito a essere scansionato, o il file di output non ha potuto essere scritto. |
2 | Invocazione non valida (flag sconosciuto, specifica porta non valida, policy o proxy malformato). |
3 | Violazione della policy a o sopra --fail-on. Solo con --policy. |
4 | Avviso di deriva dalla baseline. Solo con --baseline --fail-on-drift. |
130 | Interrotto con Ctrl+C. |
| Opzione | Descrizione |
|---|
targets | Uno o più IP, hostname o reti CIDR. |
-iL, --target-file FILE | Leggi i target da un file (uno per riga, commenti # consentiti). |
-p, --ports SPEC | Porte da scansionare, es. 22,80,1000-2000 (predefinito: 1-65535). |
--audit | Controlla ogni servizio SSH: algoritmi, chiave host, metodi di autenticazione, Terrapin, prontezza post-quantistica, correlazione delle chiavi condivise. |
--pq-report | Segnala la prontezza post-quantistica nell'intero parco. Non richiede librerie di terze parti. |
--policy NAME_OR_PATH | Controlla ogni servizio contro baseline, strict, pq o un file di policy JSON. Esce con 3 in caso di violazione. |
--fail-on {fail,warn,never} | Quale severità di policy controlla il codice di uscita (predefinito: fail). |
--baseline FILE | Confronta con un report --json precedente ed elenca cosa è cambiato. |
--fail-on-drift | Esce con 4 quando il confronto solleva un avviso. |
--format {text,json,sarif,csv} | Formato di output (predefinito: text). |
--json | Scorciatoia per --format json. |
--stream | Emetti eventi JSON delimitati da nuove righe man mano che i risultati vengono trovati. |
-o, --output FILE | Scrivi i risultati su un file invece che su stdout. |
--validate {banner,paramiko,none} | Strategia di validazione SSH (predefinito: banner). |
--scan-method {auto,connect,syn} | Back-end di scansione (predefinito: auto). |
--socks [user:pass@]host:port | Raggiungi ogni target tramite un proxy SOCKS5. |
--max-rate N | Limita le sonde al secondo nell'intera scansione (predefinito: nessun limite). |
-t, --timeout SECONDS | Tempo massimo di attesa di una sonda (predefinito: 2.0). |
--min-timeout SECONDS | Limite minimo per il timeout adattivo della sonda (predefinito: 0.1). |
--no-adaptive-timeout | Attendi l'intero --timeout su ogni sonda. |
-w, --workers N | Connessioni in volo per host (predefinito: 512). |
--max-sockets N | Tetto massimo di socket di sonda aperti contemporaneamente (predefinito: dal limite dei descrittori di file). |
--host-concurrency N | Host scansionati in parallelo (predefinito: 16). |
-r, --retries N | Nuovi tentativi per sonde in timeout (predefinito: 0). |
--max-targets N | Rifiuta elenchi di target più grandi di questo (predefinito: 65536). |
--no-early-exit | Spazza ogni porta anche su host che non rispondono affatto. |
--no-progress | Disabilita l'indicatore di avanzamento in tempo reale. |
-v, --verbose | Registrazione dettagliata (-vv riattiva anche Paramiko). |
-q, --quiet | Sopprime l'avanzamento e la registrazione informativa. |
--version | Stampa la versione ed esce. |