
Paralleler SSH-Dienst-Erkennungs- und Sicherheitsauditor, der jeden Port scannt, SSH-Banner validiert und Authentifizierungsmethoden, schwache Kryptografie, die Terrapin-Schwachstelle sowie wiederverwendete Host-Keys über Hosts und CIDR-Bereiche hinweg prüft.
Finde jeden SSH-Dienst in deinem Netzwerk, beurteile, ob er deinem Standard entspricht, und erfahre, wenn sich das ändert.
sshfinder ist eine einzelne Python-Datei ohne erforderliche Abhängigkeiten. Richte es auf
einen CIDR-Bereich und es entdeckt SSH überall dort, wo es tatsächlich lauscht – nicht nur
auf Port 22 – bestätigt, dass jeder wirklich SSH spricht, bewertet seine kryptografische
Haltung und gibt einen Nicht-Null-Exit-Code zurück, wenn etwas deine Richtlinie verletzt.
Die meisten Teams können drei Fragen zu ihrem eigenen SSH-Bestand nicht beantworten:
Die vorhandenen Tools beantworten jeweils einen Teil davon und hören dann auf:
Diese Lücke – Entdeckung und Bewertung und ein Urteil, in einem Artefakt – ist
das, was dieses Tool füllen soll. Wenn du nur einen Host prüfen musst, den du bereits
kennst, verwende ssh-audit; es geht
bei einem einzelnen Dienst tiefer als dieses Tool.
Penetrationstester werden die Prüfung und den SOCKS-Pivot nützlich finden, aber das Tool ist darauf ausgerichtet, denselben Scan wiederholt gegen einen Bestand auszuführen, den du besitzt, nicht auf ein einmaliges Engagement.
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
Keine Installation, keine Abhängigkeiten. Erfordert Python 3.9+.
Die drei Dinge, die es tut, in drei Befehlen:
# 1. INVENTAR – welches SSH ist da draußen?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. URTEIL – entspricht es unserem Standard? (beendet mit 3, wenn nicht)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. ABWEICHUNG – was hat sich seit letzter Nacht geändert?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
Das Scannen aller 65535 Ports ist die Standardeinstellung, weil ein SSH-Dienst auf einem nicht standardmäßigen Port genau der ist, den niemand aufgeschrieben hat. Jeder offene Port wird beschriftet, sodass ein offener Port nie stillschweigend als SSH-Port gezählt wird:
=== 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)
Die Bestätigung ist ein echter RFC-4253-Identifikationsaustausch, kein Blick auf die ersten Bytes auf der Leitung. Server, die zuerst einen rechtlichen Banner drucken, die darauf warten, dass sich der Client identifiziert, oder deren Banner über mehrere TCP-Segmente verteilt ankommt, werden alle korrekt erkannt – jeder davon ist eine falsch-negative Erkennung in einer naiven Implementierung.
Füge --audit hinzu, um das vollständige Bild jedes Dienstes zu erhalten:
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) und, …
Geteilte SSH-Host-Keys (mögliche geteilte/geklonte Hosts):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
Dieser letzte Block ist wissenswert: Ein Host-Key, das über Maschinen wiederverwendet wird, bedeutet normalerweise geklonte VMs oder ein geteiltes Image, und es bedeutet, dass die Kompromittierung eines Hosts die Identität aller kompromittiert.
OpenSSH 10.0 machte mlkem768x25519-sha256 zum Standard-Schlüsselaustausch, und 10.1
warnt, dass klassische Sitzungen für store now, decrypt later-Erfassung offen sind.
--pq-report beantwortet die Frage auf Flottenebene direkt, nur mit dem
KEXINIT – es benötigt also keine Drittanbieter-Bibliothek:
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+
Die Kategorie pre-standard ist die, die Leute überrascht. Ein Server,
der [email protected] oder einen Kyber-Entwurf bewirbt, sieht
in einem Algorithmus-Dump post-quantum aus, aber OpenSSH hat diesen zurückgezogenen
Parametersatz 2020 entfernt – ein aktueller Client findet also keine gemeinsame Methode und fällt
auf klassische Krypto zurück. Als bereit gezählt, wäre es schlimmer als gar nicht hinzusehen.
Ein Bericht beschreibt ein Problem. Eine Richtlinie behauptet eines und kann einen Build zum Scheitern bringen:
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
Drei Richtlinien sind eingebaut – baseline, strict und pq – benannt nach dem
Ergebnis, das sie durchsetzen, nicht nach einer Distribution. Regeln tragen eine fail- oder
warn-Schwere und --fail-on entscheidet, welche Tore greifen, sodass ein Team eine
strengere Messlatte zuerst als Warnung übernehmen und später ohne Bearbeitung befördern kann.
Schreibe deine eigene als 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"}
]
}
Prüfungen: password_auth, terrapin, weak_algorithms, post_quantum (mit
require: ready, legacy oder absent) und forbid / require über ein
field von kex_algorithms, host_key_algorithms, ciphers oder macs.
Alles andere ist ein harter Fehler, wenn die Richtlinie geladen wird, bevor der Scan beginnt. Ein Tor, das eine Regel, die es nicht versteht, stillschweigend überspringt, ist schlimmer als kein Tor: Der Lauf wird grün und niemand erfährt, dass die Prüfung nie ausgeführt wurde.
$ 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)
Führe es nächtlich gegen den gestrigen Bericht aus und sieh nur, was sich bewegt hat:
# Nächtlich, 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
Ein Host-Key, das sich geändert hat, ist das Signal, das hier am wichtigsten ist – erwartet nur nach einem Rebuild oder einer Schlüsselrotation und jeden anderen Mal einen Blick wert.
Nur alert steuert --fail-on-drift. Ein stillgelegter Host ist gewöhnlicher
Churn, und ein nächtlicher Job, der daran scheitert, würde alle trainieren, das Ergebnis zu ignorieren.
Der Vergleich ist darauf bedacht, keine Änderungen zu erfinden. Ein Feld, das kein Scan gemessen hat, wird nie als geändert gemeldet, nur Hosts, die in beiden Scans vorhanden sind, werden verglichen, und eine Baseline mit Host-Key-Fingerabdrücken lässt diesen Scan auch die tiefe Prüfung ausführen – ein oberflächlicher Rescan liest sich also nie so, als wären alle Keys verschwunden.
--format text|json|sarif|csv, optional mit -o in eine Datei geschrieben.
csv – eine Zeile pro bestätigtem SSH-Dienst. Die Form, in der ein Asset-
Inventar tatsächlich sortiert und gefiltert wird.json – der native Bericht und das Eingabeformat für --baseline.sarif – SARIF 2.1.0, validiert gegen das OASIS-Schema. Befunde sind
an host:port-logische Orte verankert und tragen stabile Fingerabdrücke, sodass
ein Konsument denselben Befund über nächtliche Läufe verfolgt, anstatt jedes Mal eine
neue Warnung zu öffnen.--stream – zeilengetrennte JSON-Ereignisse, die ausgegeben werden, während sich jeder Port öffnet
und jeder Dienst bestätigt wird, sodass eine Pipeline auf das erste Ergebnis reagieren kann,
während der Scan noch läuft: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"}
Zu SARIF und GitHub-Code-Scanning. SARIF-Ergebnisse müssen einen nicht leeren Artefaktort tragen, sonst lehnt
upload-sarifdie Datei ab, also wird eine synthetischessh://host:port-URI neben dem logischen Ort ausgegeben. Sie löst keine Datei in deinem Repository auf, also erscheinen Warnungen ohne Code-Anker. Behandle dies als SARIF für Sicherheitstools im Allgemeinen – den VS-Code-SARIF-Viewer, Azure DevOps, Archivierung – nicht als eine Möglichkeit, einen Diff zu kommentieren.
Der ganze Sinn der Richtlinien- und Abweichungsfunktionen, daher lohnt es sich, sie präzise zu benennen:
Ein harter Fehler hat Vorrang vor einem Richtlinienurteil, und ein Richtlinienurteil hat Vorrang vor Abweichung.
Wenn nichts erreichbar war, hat der Scan nichts über Compliance bewiesen,
also erhältst du 1 statt eines irreführenden Bestehens oder Scheiterns; und das Scheitern an einer
angegebenen Messlatte ist ein spezifischerer Befund als „etwas hat sich geändert“.
Der Kern-Scan, die Banner-Validierung und die abhängigkeitsfreien Teile von --audit
(Algorithmus-Inventar, Schwach-Krypto-Flags, Terrapin, Post-Quanten-Bereitschaft)
benötigen nichts außer Python 3.9+.
# Empfohlen: entsperrt Host-Key-Fingerabdrücke, Auth-Methoden-Enumeration und
# Shared-Key-Korrelation in --audit, plus --validate paramiko.
pip install -r requirements.txt
# Optional, nur für halboffene SYN-Scans (erfordert root):
pip install scapy>=2.5
--scan-method auto wählt den SYN-Scan, wenn als root mit installiertem Scapy
ausgeführt, und fällt sonst auf den privilegienfreien Connect-Scan zurück.
SOCKS kann nicht mit einem SYN-Scan kombiniert werden – SOCKS5 transportiert TCP-Streams, keine
rohen Pakete.
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
Entdeckung, der Banner-Austausch und die Prüfung durchlaufen alle den Pivot, sodass Ergebnisse nie halb getunnelt sind. Ein Proxy, der nicht erreichbar ist, wird als Scan-Fehler gemeldet, nie als „kein SSH gefunden“.
--max-rate begrenzt Sonden pro Sekunde über den gesamten Scan. Nebenläufigkeit
begrenzt, wie viele Verbindungen gleichzeitig offen sind; dies begrenzt, wie schnell neue
starten, was die Obergrenze ist, die du versprechen können musst, bevor du etwas unter
Einsatzregeln scannst.
Implementierungsnotizen, für wenn das obige Verhalten erklärt werden muss.
Die Scan-Engine. Jede Verbindung in Flug wird von einem Thread über eine
OS-Ereignisschleife (epoll/kqueue/select) gesteuert, also kostet Nebenläufigkeit einen
Dateideskriptor statt eines OS-Threads, und jeder Host wird einmal aufgelöst statt einmal
pro Port. Ein voller 1–65535-Sweep läuft etwa 6× schneller als ein
Thread-Pool-Design.
SSH-Ports zuerst. Die Handvoll Ports, auf denen SSH tatsächlich lebt (22, 2222, 22222, …), werden am Anfang jedes Sweeps abgetastet. Bei einem vollen Sweep erscheint der erste bestätigte SSH-Dienst in etwa 0,2 Sekunden statt 16.
Ein Handshake pro Dienst. Der Socket, der einen offenen Port entdeckt hat, wird direkt an den Banner-Austausch übergeben, also kostet ein bestätigter SSH-Dienst einen TCP-Handshake statt zwei.
Adaptives Timeout. Sonden warten so lange, wie der Pfad es rechtfertigt, mit dem
geglätteten Round-Trip-Schätzer aus RFC 6298 – dem, den TCP selbst verwendet – gespeist
von jeder beantworteten Sonde und über den Scan geteilt. --timeout wird zu einer
Obergrenze statt einer festen Kosten: Auf einem lebenden, aber meist gefilterten Host ist das
etwa 5× wert bei identischen Befunden. Nur eine eindeutige Antwort lehrt es etwas;
ein Timeout sagt nichts über den Pfad aus und wird nie zurückgespeist.
Zwei Stellen behalten bewusst die volle Obergrenze. Der Banner-Austausch und die Prüfung passen sich nie an, weil die Geschwindigkeit, mit der ein Host einen TCP-Handshake abschließt, nichts darüber sagt, wie schnell sein SSH-Daemon eine Begrüßung verfasst. Auch das endgültige erneute Abtasten der üblichen SSH-Ports nicht, da ein verlorenes SYN dort der eine Verlust ist, der diesem Tool tatsächlich einen Befund kostet.
Früher Ausstieg. Ein Host, der über seine ersten paar hundert Sonden gar nichts
beantwortet, wird als nicht reagierend gemeldet, statt ein Timeout pro verbleibendem Port zu verbrauchen.
Da SSH-Ports zuerst abgetastet werden, wird ein lebender Dienst immer gesehen, bevor dies auslösen kann;
--no-early-exit erzwingt den vollen Bereich.
Von Natur aus begrenzt. Ein prozessweites Socket-Budget, das aus dem
Dateideskriptor-Limit abgeleitet wird, verhindert, dass ein großer Scan Deskriptoren erschöpft und
lebende Dienste als gefiltert meldet. Die Zielerweiterung prüft die Größe eines
Netzwerks, bevor es materialisiert wird, also wird ein verirrtes /8 in Millisekunden abgelehnt,
statt ein Gigabyte Speicher zu verbrauchen.
Kuratierte Algorithmus-Bewertungen. Jeder markierte Algorithmus stammt aus einer
expliziten Tabelle mit einer Schwere und einem angegebenen Grund, nicht aus einer Kette von Teilstring-
Tests. Namen werden zuerst normalisiert, sodass ein Anbieter-Suffix nicht an einer Prüfung vorbeischlüpfen kann –
[email protected] ist CBC, egal wer es liefert – und
Verhandlungsmarker wie [email protected] werden nie als
Algorithmen bewertet. Diese Tabelle ist es, auf der das Richtlinien-Tor und der Abweichungsvergleich
letztlich beruhen.
Robustes Strg+C. Wird sogar unter Windows berücksichtigt, wo eine unbegrenzte Thread-Wartezeit es normalerweise verschluckt: Der erste Druck stoppt sauber und gibt partielle Ergebnisse zurück, ein zweiter erzwingt einen sofortigen Ausstieg.
OpenSSH_9.6p1 auf Ubuntu 24.04 gegen die meisten
Dinge gepatcht, die öffentliche Datenbanken 9.6p1 zuschreiben. Das ist eine
Falsch-Positiv-Maschine – es ist der Grund, warum ssh-audit seine eigene versionsbasierte
CVE-Erkennung entfernt hat, und warum Tenable ein Plugin ausliefert, dessen ganze Aufgabe darin besteht, das
Zurückportieren zu erkennen, das es bricht. Nur was ein Server tatsächlich bewirbt, wird
bewertet.nmap beim allgemeinen Port-Scannen konkurrieren oder mit masscan und
zmap im Internet-Maßstab. Diese Probleme sind gelöst.Die Testsuite verwendet nur die Standardbibliothek, also läuft sie auf einem nackten Interpreter:
python -m unittest discover -s tests
Installiere die Laufzeitabhängigkeiten, um auch die Paramiko-gestützten Audit- Tests auszuführen, die sich selbst überspringen, wenn Paramiko fehlt:
pip install -r requirements-dev.txt
python -m unittest discover -s tests
Scanne nur Systeme, die du besitzt oder für die du ausdrücklich autorisiert bist, sie zu testen. Unautorisiertes Scannen kann in deiner Rechtsordnung illegal sein.
| Tool | Entdeckt SSH | Bewertet es | Über eine Flotte |
|---|
nmap | ja | oberflächlich, über NSE-Skripte | ja |
ssh-audit | nein – du gibst ihm einen Host | tiefgehend | nein |
masscan / zmap | im Internet-Maßstab | nein | ja |
sshfinder | ja | ja | ja |
| Code | Bedeutung |
|---|
0 | Erfolg. Nichts gefunden ist immer noch Erfolg – ein leerer Bestand ist kein Fehler. |
1 | Harter Fehler: Jedes Ziel konnte nicht gescannt werden, oder die Ausgabedatei konnte nicht geschrieben werden. |
2 | Ungültiger Aufruf (unbekanntes Flag, ungültige Port-Spezifikation, fehlerhafte Richtlinie oder Proxy). |
3 | Richtlinienverletzung bei oder über --fail-on. Nur mit --policy. |
4 | Baseline-Abweichungswarnung. Nur mit --baseline --fail-on-drift. |
130 | Mit Strg+C unterbrochen. |
| Option | Beschreibung |
|---|
targets | Eine oder mehrere IPs, Hostnamen oder CIDR-Netzwerke. |
-iL, --target-file DATEI | Ziele aus einer Datei lesen (eine pro Zeile, #-Kommentare erlaubt). |
-p, --ports SPEZ | Zu scannende Ports, z. B. 22,80,1000-2000 (Standard: 1-65535). |
--audit | Jeden SSH-Dienst prüfen: Algorithmen, Host-Key, Auth-Methoden, Terrapin, Post-Quanten-Bereitschaft, Shared-Key-Korrelation. |
--pq-report | Post-Quanten-Bereitschaft über den Bestand melden. Benötigt keine Drittanbieter-Bibliothek. |
--policy NAME_ODER_PFAD | Jeden Dienst gegen baseline, strict, pq oder eine JSON-Richtliniendatei prüfen. Beendet mit 3 bei Verletzung. |
--fail-on {fail,warn,never} | Welche Richtlinienschwere den Exit-Code steuert (Standard: fail). |
--baseline DATEI | Gegen einen vorherigen --json-Bericht vergleichen und auflisten, was sich geändert hat. |
--fail-on-drift | Beendet mit 4, wenn der Vergleich eine Warnung auslöst. |
--format {text,json,sarif,csv} | Ausgabeformat (Standard: text). |
--json | Kurzform für --format json. |
--stream | Zeilengetrennte JSON-Ereignisse ausgeben, sobald Ergebnisse gefunden werden. |
-o, --output DATEI | Ergebnisse in eine Datei statt auf stdout schreiben. |
--validate {banner,paramiko,none} | SSH-Validierungsstrategie (Standard: banner). |
--scan-method {auto,connect,syn} | Scan-Backend (Standard: auto). |
--socks [user:pass@]host:port | Jedes Ziel über einen SOCKS5-Proxy erreichen. |
--max-rate N | Sonden pro Sekunde über den gesamten Scan begrenzen (Standard: keine Begrenzung). |
-t, --timeout SEKUNDEN | Längste Wartezeit einer Sonde (Standard: 2.0). |
--min-timeout SEKUNDEN | Untergrenze für das adaptive Sonden-Timeout (Standard: 0.1). |
--no-adaptive-timeout | Bei jeder Sonde das volle --timeout abwarten. |
-w, --workers N | Verbindungen in Flug pro Host (Standard: 512). |
--max-sockets N | Obergrenze für gleichzeitig offene Sonden-Sockets (Standard: aus dem Dateideskriptor-Limit). |
--host-concurrency N | Parallel gescannte Hosts (Standard: 16). |
-r, --retries N | Wiederholungen für abgelaufene Sonden (Standard: 0). |
--max-targets N | Ziellisten größer als dies ablehnen (Standard: 65536). |
--no-early-exit | Jeden Port abtasten, auch auf Hosts, die gar nichts beantworten. |
--no-progress | Den Live-Fortschrittsindikator deaktivieren. |
-v, --verbose | Ausführliche Protokollierung (-vv entsilenziert auch Paramiko). |
-q, --quiet | Fortschritts- und Informationsprotokollierung unterdrücken. |
--version | Version ausgeben und beenden. |