
Herramienta de descubrimiento de servicios SSH en paralelo y auditor de seguridad que escanea cualquier puerto, valida banners SSH y audita métodos de autenticación, criptografía débil, vulnerabilidad Terrapin y claves de host reutilizadas entre hosts y rangos CIDR.
Encuentra cada servicio SSH en tu red, determina si cumple con tu estándar, y recibe una notificación cuando eso cambie.
sshfinder es un único archivo Python sin dependencias requeridas. Apúntalo a
un rango CIDR y descubre SSH dondequiera que esté realmente escuchando — no solo
en el puerto 22 — confirma que cada uno realmente habla SSH, evalúa su postura
criptográfica, y devuelve un código de salida distinto de cero cuando algo
falla en tu política.
La mayoría de los equipos no pueden responder tres preguntas sobre su propio parque SSH:
Las herramientas existentes responden cada una a parte de esto y se detienen:
Esa brecha — descubrimiento y evaluación y un veredicto, en un solo artefacto — es
lo que esta herramienta existe para llenar. Si solo necesitas auditar un host que ya
conoces, usa ssh-audit; profundiza
más en un solo servicio que esto.
Los testers de penetración encontrarán útiles la auditoría y el pivote SOCKS, pero la herramienta está diseñada en torno a ejecutar el mismo escaneo repetidamente contra un parque que posees, no en torno a un compromiso puntual.
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
Sin instalación, sin dependencias. Requiere Python 3.9+.
Las tres cosas que hace, en tres comandos:
# 1. INVENTARIO — ¿qué SSH hay ahí fuera?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. VEREDICTO — ¿cumple con nuestro estándar? (sale con 3 si no)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. DERIVA — ¿qué cambió desde anoche?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
Escanear los 65535 puertos es el valor predeterminado, porque un servicio SSH en un puerto no estándar es precisamente el que nadie ha documentado. Cada puerto abierto está etiquetado, por lo que un puerto abierto nunca se cuenta silenciosamente como uno 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 confirmación es un intercambio de identificación RFC 4253 real, no un vistazo a los primeros bytes en el cable. Los servidores que imprimen un banner legal primero, que esperan a que el cliente se identifique, o cuyo banner llega dividido entre segmentos TCP se reconocen correctamente — cada uno de esos es un falso negativo en una implementación ingenua.
Añade --audit para la imagen completa de cada servicio:
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, …
Claves de host SSH compartidas (posibles hosts compartidos/clonados):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
Ese último bloque vale la pena conocerlo: una clave de host reutilizada entre máquinas normalmente significa VMs clonadas o una imagen compartida, y significa que comprometer un host compromete la identidad de todos ellos.
OpenSSH 10.0 hizo de mlkem768x25519-sha256 el intercambio de claves predeterminado, y 10.1
advierte que las sesiones clásicas están abiertas a la captura de almacena ahora, descifra después.
--pq-report responde la pregunta a nivel de flota directamente, usando solo el
KEXINIT — por lo que no necesita ninguna biblioteca de terceros:
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 categoría pre-standard es la que atrapa a la gente. Un servidor
que anuncia [email protected] o un borrador de Kyber parece
post-cuántico en un volcado de algoritmos, pero OpenSSH eliminó ese conjunto de
parámetros retirado en 2020 — por lo que un cliente actual no encuentra ningún método común y
vuelve a la criptografía clásica. Contado como listo, sería peor que no mirar
en absoluto.
Un informe describe un problema. Una política afirma uno, y puede hacer fallar una compilación:
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
Tres políticas vienen integradas — baseline, strict y pq — nombradas por el
resultado que imponen en lugar de por una distribución. Las reglas llevan una severidad
fail o warn y --fail-on decide qué compuertas, por lo que un equipo puede adoptar
un estándar más estricto como advertencia primero y promoverlo después sin editar nada.
Escribe las tuyas propias como 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"}
]
}
Comprobaciones: password_auth, terrapin, weak_algorithms, post_quantum (con
require: ready, legacy o absent), y forbid / require sobre un
field de kex_algorithms, host_key_algorithms, ciphers o macs.
Cualquier otra cosa es un error grave cuando la política se carga, antes de que comience el escaneo. Una compuerta que omite silenciosamente una regla que no entiende es peor que ninguna compuerta: la ejecución se pone en verde y nadie se entera de que la comprobación nunca se ejecutó.
$ 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)
Ejecútalo cada noche contra el informe de ayer y ve solo lo que se movió:
# Nightly, 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 clave de host que cambió es la señal que más importa aquí — esperada solo después de una reconstrucción o una rotación de claves, y digna de revisión en cualquier otro momento.
Solo alert controla --fail-on-drift. Un host dado de baja es un cambio
ordinario, y hacer fallar un trabajo nocturno por ello entrenaría a todos a ignorar el
resultado.
La comparación tiene cuidado de no inventar cambios. Un campo que ningún escaneo midió nunca se informa como cambiado, solo se comparan los hosts presentes en ambos escaneos, y una línea base que contiene huellas de claves de host hace que este escaneo también ejecute la sonda profunda — por lo que un reescaneo superficial nunca se lee como si cada clave hubiera desaparecido.
--format text|json|sarif|csv, opcionalmente escrito a un archivo con -o.
csv — una fila por servicio SSH confirmado. La forma en que un inventario
de activos realmente se ordena y filtra.json — el informe nativo, y el formato de entrada para --baseline.sarif — SARIF 2.1.0, validado contra el esquema OASIS. Los hallazgos están
anclados a ubicaciones lógicas host:port y llevan huellas estables, por lo que
un consumidor rastrea el mismo hallazgo entre ejecuciones nocturnas en lugar de abrir
una alerta nueva cada vez.--stream — eventos JSON delimitados por nueva línea vaciados a medida que cada puerto se abre
y cada servicio se confirma, para que una canalización pueda actuar sobre el primer resultado
mientras el escaneo aún se está ejecutando: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"}
Sobre SARIF y el escaneo de código de GitHub. Los resultados SARIF deben llevar una ubicación de artefacto no vacía o
upload-sarifrechaza el archivo, por lo que se emite un URI sintéticossh://host:portjunto con la ubicación lógica. No se resuelve a un archivo en tu repositorio, por lo que las alertas aparecen sin un ancla de código. Trata esto como SARIF para herramientas de seguridad en general — el visor SARIF de VS Code, Azure DevOps, archivado — no como una forma de anotar un diff.
Todo el punto de las funciones de política y deriva, por lo que vale la pena declararlos con precisión:
Un error grave supera a un veredicto de política, y un veredicto de política supera a la deriva.
Si nada era alcanzable, el escaneo no demostró nada sobre el cumplimiento en
ningún sentido, por lo que obtienes 1 en lugar de un aprobado o fallo engañoso; y fallar
un estándar declarado es un hallazgo más específico que "algo cambió".
El escaneo central, la validación de banner y las partes sin dependencias de --audit
(inventario de algoritmos, banderas de criptografía débil, Terrapin, preparación post-cuántica)
no necesitan nada más que Python 3.9+.
# Recomendado: desbloquea huellas de claves de host, enumeración de métodos de autenticación y
# correlación de claves compartidas en --audit, además de --validate paramiko.
pip install -r requirements.txt
# Opcional, solo para escaneos SYN de medio abierto (necesita root):
pip install scapy>=2.5
--scan-method auto selecciona el escaneo SYN cuando se ejecuta como root con Scapy
instalado, y de lo contrario recurre al escaneo de conexión sin privilegios.
SOCKS no se puede combinar con un escaneo SYN — SOCKS5 transporta flujos TCP, no
paquetes crudos.
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
El descubrimiento, el intercambio de banner y la auditoría atraviesan el pivote, por lo que los resultados nunca están medio tunelizados. Un proxy que es inalcanzable se informa como un error de escaneo, nunca como "no se encontró SSH".
--max-rate limita las sondas por segundo en todo el escaneo. La concurrencia
limita cuántas conexiones están abiertas a la vez; esto limita qué tan rápido comienzan
nuevas, que es el tope que necesitas poder prometer antes de escanear cualquier cosa
bajo reglas de compromiso.
Notas de implementación, para cuando el comportamiento anterior necesite explicación.
El motor de escaneo. Cada conexión en vuelo se impulsa desde un solo hilo mediante
un bucle de eventos del SO (epoll/kqueue/select), por lo que la concurrencia cuesta un descriptor
de archivo en lugar de un hilo del SO, y cada host se resuelve una vez en lugar de
una vez por puerto. Un barrido completo de 1–65535 se ejecuta aproximadamente 6× más rápido que un
diseño de grupo de hilos.
Puertos SSH primero. El puñado de puertos donde SSH realmente vive (22, 2222, 22222, …) se sondean al inicio de cada barrido. En un barrido completo, el primer servicio SSH confirmado aparece en aproximadamente 0.2 segundos en lugar de 16.
Un apretón de manos por servicio. El socket que descubrió un puerto abierto se entrega directamente al intercambio de banner, por lo que un servicio SSH confirmado cuesta un apretón de manos TCP en lugar de dos.
Tiempo de espera adaptativo. Las sondas esperan tanto como el camino lo justifica, usando
el estimador de ida y vuelta suavizado de RFC 6298 — el que el propio TCP usa — alimentado
por cada sonda respondida y compartido en todo el escaneo. --timeout se convierte en
un tope en lugar de un costo fijo: en un host vivo pero mayormente filtrado eso vale
aproximadamente 5× con hallazgos idénticos. Solo una respuesta definitiva le enseña
algo; un tiempo de espera agotado no dice nada sobre el camino y nunca se retroalimenta.
Dos lugares mantienen deliberadamente el tope completo. El intercambio de banner y la auditoría nunca se adaptan, porque qué tan rápido un host completa un apretón de manos TCP no dice nada sobre qué tan rápido su demonio SSH compone un saludo. Tampoco lo hace la re-sonda final de los puertos habituales de SSH, ya que un SYN caído allí es la única pérdida que realmente le cuesta a esta herramienta un hallazgo.
Salida temprana. Un host que no responde nada en absoluto en sus primeras
pocas cientos de sondas se informa como no receptivo en lugar de consumir un tiempo de espera
por puerto restante. Debido a que los puertos de SSH se barren primero, un servicio vivo
siempre se ve antes de que esto pueda dispararse; --no-early-exit fuerza el rango completo.
Acotado por diseño. Un presupuesto de sockets a nivel de proceso derivado del
límite de descriptores de archivo evita que un escaneo grande agote los descriptores y
informe erróneamente servicios vivos como filtrados. La expansión de objetivos comprueba el tamaño de una red
antes de materializarla, por lo que un /8 perdido se rechaza en milisegundos
en lugar de consumir un gigabyte de memoria.
Juicios de algoritmos curados. Cada algoritmo marcado proviene de una
tabla explícita con una severidad y una razón declarada, no de una cadena de pruebas
de subcadenas. Los nombres se normalizan primero, por lo que un sufijo de proveedor no puede pasar
una comprobación — [email protected] es CBC sin importar quién lo envíe — y
los marcadores de negociación como [email protected] nunca se evalúan como
algoritmos. Esta tabla es en lo que finalmente se basan la compuerta de política y la comparación de deriva.
Ctrl+C robusto. Se respeta incluso en Windows, donde una espera de hilo ilimitada normalmente lo traga: la primera pulsación se detiene con elegancia y devuelve resultados parciales, una segunda fuerza una salida inmediata.
OpenSSH_9.6p1 en Ubuntu 24.04 está parcheado
contra la mayoría de lo que las bases de datos públicas atribuyen a 9.6p1. Eso es una
máquina de falsos positivos — es por eso que ssh-audit eliminó su propia detección de CVE
basada en versiones, y por qué Tenable envía un plugin cuyo único trabajo es detectar
el backporting que lo rompe. Solo lo que un servidor realmente anuncia se
evalúa.nmap en escaneo de puertos general, o con masscan y
zmap a escala de internet. Esos problemas están resueltos.La suite de pruebas es solo de biblioteca estándar, por lo que se ejecuta en un intérprete desnudo:
python -m unittest discover -s tests
Instala las dependencias de ejecución para también ejercitar las pruebas de auditoría respaldadas por Paramiko, que se omiten a sí mismas cuando Paramiko falta:
pip install -r requirements-dev.txt
python -m unittest discover -s tests
Solo escanea sistemas que posees o para los que estás explícitamente autorizado a probar. El escaneo no autorizado puede ser ilegal en tu jurisdicción.
| Herramienta | Descubre SSH | Lo evalúa | En toda una flota |
|---|
nmap | sí | superficial, mediante scripts NSE | sí |
ssh-audit | no — le das un solo host | en profundidad | no |
masscan / zmap | a escala de internet | no | sí |
sshfinder | sí | sí | sí |
| Código | Significado |
|---|
0 | Éxito. Nada encontrado sigue siendo éxito — un parque vacío no es un error. |
1 | Error grave: todos los objetivos fallaron al escanearse, o el archivo de salida no pudo escribirse. |
2 | Invocación incorrecta (bandera desconocida, especificación de puerto inválida, política o proxy malformado). |
3 | Violación de política en o por encima de --fail-on. Solo con --policy. |
4 | Alerta de deriva de línea base. Solo con --baseline --fail-on-drift. |
130 | Interrumpido con Ctrl+C. |
| Opción | Descripción |
|---|
targets | Una o más IPs, nombres de host o redes CIDR. |
-iL, --target-file FILE | Lee objetivos de un archivo (uno por línea, se permiten comentarios #). |
-p, --ports SPEC | Puertos a escanear, p. ej. 22,80,1000-2000 (predeterminado: 1-65535). |
--audit | Audita cada servicio SSH: algoritmos, clave de host, métodos de autenticación, Terrapin, preparación post-cuántica, correlación de claves compartidas. |
--pq-report | Informa la preparación post-cuántica en todo el parque. No necesita ninguna biblioteca de terceros. |
--policy NAME_OR_PATH | Comprueba cada servicio contra baseline, strict, pq, o un archivo de política JSON. Sale con 3 en violación. |
--fail-on {fail,warn,never} | Qué severidad de política controla el código de salida (predeterminado: fail). |
--baseline FILE | Compara contra un informe --json anterior y lista qué cambió. |
--fail-on-drift | Sale con 4 cuando la comparación genera una alerta. |
--format {text,json,sarif,csv} | Formato de salida (predeterminado: text). |
--json | Abreviatura de --format json. |
--stream | Emite eventos JSON delimitados por nueva línea a medida que se encuentran resultados. |
-o, --output FILE | Escribe resultados a un archivo en lugar de stdout. |
--validate {banner,paramiko,none} | Estrategia de validación SSH (predeterminado: banner). |
--scan-method {auto,connect,syn} | Back-end de escaneo (predeterminado: auto). |
--socks [user:pass@]host:port | Alcanza cada objetivo a través de un proxy SOCKS5. |
--max-rate N | Limita sondas por segundo en todo el escaneo (predeterminado: sin límite). |
-t, --timeout SECONDS | Máximo que una sonda puede esperar (predeterminado: 2.0). |
--min-timeout SECONDS | Mínimo para el tiempo de espera adaptativo de la sonda (predeterminado: 0.1). |
--no-adaptive-timeout | Espera el --timeout completo en cada sonda. |
-w, --workers N | Conexiones en vuelo por host (predeterminado: 512). |
--max-sockets N | Tope de sockets de sonda abiertos a la vez (predeterminado: del límite de descriptores de archivo). |
--host-concurrency N | Hosts escaneados en paralelo (predeterminado: 16). |
-r, --retries N | Reintentos para sondas con tiempo de espera agotado (predeterminado: 0). |
--max-targets N | Rechaza listas de objetivos más grandes que esto (predeterminado: 65536). |
--no-early-exit | Barre cada puerto incluso en hosts que no responden nada en absoluto. |
--no-progress | Desactiva el indicador de progreso en vivo. |
-v, --verbose | Registro detallado (-vv también desilencia Paramiko). |
-q, --quiet | Suprime el progreso y el registro informativo. |
--version | Imprime la versión y sale. |