
Descoberta paralela de serviços SSH e auditor de segurança que varre qualquer porta, valida banners SSH e audita métodos de autenticação, criptografia fraca, vulnerabilidade Terrapin e chaves de host reutilizadas entre hosts e intervalos CIDR.
Encontre todos os serviços SSH na sua rede, avalie se eles atendem ao seu padrão e seja notificado quando isso mudar.
sshfinder é um único arquivo Python sem dependências obrigatórias. Aponte-o
para um intervalo CIDR e ele descobre SSH onde quer que esteja realmente escutando — não
apenas na porta 22 — confirma que cada um realmente fala SSH, avalia sua postura
criptográfica e retorna um código de saída diferente de zero quando algo falha na sua política.
A maioria das equipes não consegue responder a três perguntas sobre seu próprio parque SSH:
As ferramentas existentes respondem a cada parte disso e param:
| Ferramenta | Descobre SSH | Avalia | Em toda a frota |
|---|---|---|---|
nmap | sim | superficial, via scripts NSE | sim |
ssh-audit | não — você fornece um host | profundamente | não |
masscan / zmap | em escala de internet | não | sim |
sshfinder | sim | sim | sim |
Essa lacuna — descoberta e avaliação e um veredito, em um único artefato — é
o que esta ferramenta existe para preencher. Se você só precisa auditar um host que já
conhece, use ssh-audit; ele vai mais
fundo em um único serviço do que esta ferramenta.
Testadores de penetração acharão a auditoria e o pivô SOCKS úteis, mas a ferramenta é moldada em torno de executar a mesma varredura repetidamente contra um parque que você possui, não em torno de um engajamento único.
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
Sem instalação, sem dependências. Requer Python 3.9+.
As três coisas que ele faz, em três comandos:
# 1. INVENTÁRIO — que SSH existe por aí?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. VEREDITO — atende ao nosso padrão? (sai com 3 se não)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. DERIVA — o que mudou desde ontem à noite?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
Escanear todas as 65535 portas é o padrão, porque um serviço SSH em uma porta não padrão é exatamente aquele que ninguém registrou. Cada porta aberta é rotulada, então uma porta aberta nunca é silenciosamente contada como uma 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)
A confirmação é uma troca de identificação real RFC 4253, não uma olhada nos primeiros bytes no fio. Servidores que imprimem um banner legal primeiro, que esperam o cliente se identificar, ou cujo banner chega dividido entre segmentos TCP são todos reconhecidos corretamente — cada um desses é um falso negativo em uma implementação ingênua.
Adicione --audit para a visão completa de cada serviço:
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, …
Chaves de host SSH compartilhadas (possíveis hosts compartilhados/clonados):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
Esse último bloco vale a pena conhecer: uma chave de host reutilizada entre máquinas geralmente significa VMs clonadas ou uma imagem compartilhada, e significa que comprometer um host compromete a identidade de todos eles.
O OpenSSH 10.0 tornou mlkem768x25519-sha256 a troca de chaves padrão, e o 10.1
avisa que sessões clássicas estão abertas à captura armazene agora, descriptografe depois.
--pq-report responde à pergunta no nível da frota diretamente, usando apenas o
KEXINIT — então não precisa de biblioteca de terceiros:
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+
A categoria pre-standard é a que pega as pessoas de surpresa. Um servidor
anunciando [email protected] ou um rascunho Kyber parece
pós-quântico em um despejo de algoritmos, mas o OpenSSH descartou esse conjunto de
parâmetros retirado em 2020 — então um cliente atual não encontra método comum e cai
de volta para criptografia clássica. Contado como pronto, seria pior do que não
olhar nada.
Um relatório descreve um problema. Uma política afirma um, e pode falhar um 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
Três políticas vêm embutidas — baseline, strict e pq — nomeadas pelo
resultado que impõem, não por uma distribuição. Regras carregam uma severidade fail ou
warn e --fail-on decide quais portões, então uma equipe pode adotar um
padrão mais rígido como aviso primeiro e promovê-lo depois sem editar nada.
Escreva a sua própria 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"}
]
}
Verificações: password_auth, terrapin, weak_algorithms, post_quantum (com
require: ready, legacy ou absent), e forbid / require sobre um
field de kex_algorithms, host_key_algorithms, ciphers ou macs.
Qualquer outra coisa é um erro grave quando a política carrega, antes da varredura começar. Um portão que silenciosamente pula uma regra que não entende é pior do que nenhum portão: a execução fica verde e ninguém aprende que a verificação nunca executou.
$ 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)
Execute-o todas as noites contra o relatório de ontem e veja apenas o que mudou:
# 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
Uma chave de host que mudou é o sinal que mais importa aqui — esperado apenas após um rebuild ou uma rotação de chave, e vale uma olhada em qualquer outro momento.
Apenas alert controla --fail-on-drift. Um host descomissionado é rotatividade
comum, e falhar um trabalho noturno por causa dele treinaria todos a ignorar o
resultado.
A comparação é cuidadosa para não inventar mudanças. Um campo que nenhuma varredura mediu nunca é relatado como tendo mudado, apenas hosts presentes em ambas as varreduras são comparados, e uma linha de base contendo impressões digitais de chave de host faz esta varredura executar a sonda profunda também — então uma revarredura superficial nunca lê como se cada chave tivesse desaparecido.
--format text|json|sarif|csv, opcionalmente escrito em um arquivo com -o.
csv — uma linha por serviço SSH confirmado. A forma que um inventário
de ativos realmente é classificado e filtrado.json — o relatório nativo, e o formato de entrada para --baseline.sarif — SARIF 2.1.0, validado contra o esquema OASIS. Descobertas são
ancoradas a locais lógicos host:port e carregam impressões digitais estáveis, então
um consumidor rastreia a mesma descoberta em execuções noturnas em vez de abrir
um alerta novo a cada vez.--stream — eventos JSON delimitados por nova linha liberados conforme cada porta abre
e cada serviço é confirmado, então um pipeline pode agir no primeiro resultado
enquanto a varredura ainda está em execução: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 e varredura de código GitHub. Resultados SARIF devem carregar um local de artefato não vazio ou
upload-sarifrejeita o arquivo, então um URI sintéticossh://host:porté emitido junto com o local lógico. Ele não resolve para um arquivo no seu repositório, então alertas aparecem sem uma âncora de código. Trate isso como SARIF para ferramentas de segurança em geral — o visualizador SARIF do VS Code, Azure DevOps, arquivamento — não como uma forma de anotar um diff.
Todo o ponto dos recursos de política e deriva, então vale a pena declará-los precisamente:
| Código | Significado |
|---|---|
0 | Sucesso. Nada encontrado ainda é sucesso — um parque vazio não é um erro. |
1 | Erro grave: todos os alvos falharam ao escanear, ou o arquivo de saída não pôde ser escrito. |
2 | Invocação ruim (flag desconhecida, especificação de porta inválida, política ou proxy malformado). |
3 | Violação de política em ou acima de --fail-on. Apenas com --policy. |
4 | Alerta de deriva de linha de base. Apenas com --baseline --fail-on-drift. |
130 | Interrompido com Ctrl+C. |
Um erro grave supera um veredito de política, e um veredito de política supera deriva.
Se nada estava acessível, a varredura não provou nada sobre conformidade de qualquer
forma, então você recebe 1 em vez de um passe ou falha enganoso; e falhar um
padrão declarado é uma descoberta mais específica do que "algo mudou".
A varredura principal, validação de banner e as partes sem dependências de --audit
(inventário de algoritmos, flags de criptografia fraca, Terrapin, prontidão pós-quântica)
precisam apenas de Python 3.9+.
# Recomendado: desbloqueia impressões digitais de chave de host, enumeração de métodos de autenticação e
# correlação de chaves compartilhadas em --audit, além de --validate paramiko.
pip install -r requirements.txt
# Opcional, apenas para varreduras SYN de meia abertura (requer root):
pip install scapy>=2.5
| Opção | Descrição |
|---|---|
targets | Um ou mais IPs, nomes de host ou redes CIDR. |
-iL, --target-file FILE | Ler alvos de um arquivo (um por linha, comentários # permitidos). |
-p, --ports SPEC | Portas para escanear, ex. 22,80,1000-2000 (padrão: 1-65535). |
--audit | Auditar cada serviço SSH: algoritmos, chave de host, métodos de autenticação, Terrapin, prontidão pós-quântica, correlação de chaves compartilhadas. |
--pq-report | Relatar prontidão pós-quântica em todo o parque. Não precisa de biblioteca de terceiros. |
--policy NAME_OR_PATH | Verificar cada serviço contra baseline, strict, pq, ou um arquivo de política JSON. Sai com 3 em violação. |
--fail-on {fail,warn,never} | Qual severidade de política controla o código de saída (padrão: fail). |
--baseline FILE | Comparar contra um relatório --json anterior e listar o que mudou. |
--fail-on-drift | Sair com 4 quando a comparação levanta um alerta. |
--format {text,json,sarif,csv} | Formato de saída (padrão: text). |
--json | Atalho para --format json. |
--stream | Emitir eventos JSON delimitados por nova linha conforme resultados são encontrados. |
-o, --output FILE | Escrever resultados em um arquivo em vez de stdout. |
--validate {banner,paramiko,none} | Estratégia de validação SSH (padrão: banner). |
--scan-method {auto,connect,syn} | Back-end de varredura (padrão: auto). |
--socks [user:pass@]host:port | Alcançar cada alvo através de um proxy SOCKS5. |
--max-rate N | Limitar sondas por segundo em toda a varredura (padrão: sem limite). |
-t, --timeout SECONDS | Máximo que uma sonda pode esperar (padrão: 2.0). |
--min-timeout SECONDS | Piso para o tempo limite adaptativo da sonda (padrão: 0.1). |
--no-adaptive-timeout | Esperar o --timeout completo em cada sonda. |
-w, --workers N | Conexões em voo por host (padrão: 512). |
--max-sockets N | Teto de soquetes de sonda abertos de uma vez (padrão: do limite de descritores de arquivo). |
--host-concurrency N | Hosts escaneados em paralelo (padrão: 16). |
-r, --retries N | Tentativas para sondas com tempo esgotado (padrão: 0). |
--max-targets N | Recusar listas de alvos maiores que isso (padrão: 65536). |
--no-early-exit | Varrer cada porta mesmo em hosts que não respondem nada. |
--no-progress | Desabilitar o indicador de progresso ao vivo. |
-v, --verbose | Registro detalhado (-vv também reativa o som do Paramiko). |
-q, --quiet | Suprimir progresso e registro informativo. |
--version | Imprimir a versão e sair. |
--scan-method auto seleciona a varredura SYN quando executado como root com Scapy
instalado, e caso contrário cai de volta para a varredura de conexão sem privilégios.
SOCKS não pode ser combinado com uma varredura SYN — SOCKS5 carrega fluxos TCP, não
pacotes brutos.
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
Descoberta, a troca de banner e a auditoria atravessam o pivô, então resultados nunca são meio-tunelados. Um proxy inacessível é relatado como um erro de varredura, nunca como "nenhum SSH encontrado".
--max-rate limita sondas por segundo em toda a varredura. Concorrência
limita quantas conexões estão abertas de uma vez; isso limita quão rápido novas
começam, que é o teto que você precisa poder prometer antes de escanear qualquer
coisa sob regras de engajamento.
Notas de implementação, para quando o comportamento acima precisar de explicação.
O motor de varredura. Cada conexão em voo é dirigida de uma única thread por um
loop de eventos do SO (epoll/kqueue/select), então concorrência custa um descritor
de arquivo em vez de uma thread do SO, e cada host é resolvido uma vez em vez de
uma vez por porta. Uma varredura completa de 1–65535 roda cerca de 6× mais rápido que um
design de pool de threads.
Portas SSH primeiro. As poucas portas onde SSH realmente vive (22, 2222, 22222, …) são sondadas no início de cada varredura. Em uma varredura completa, o primeiro serviço SSH confirmado aparece em cerca de 0,2 segundos em vez de 16.
Um handshake por serviço. O soquete que descobriu uma porta aberta é entregue diretamente à troca de banner, então um serviço SSH confirmado custa um handshake TCP em vez de dois.
Tempo limite adaptativo. Sondas esperam tanto quanto o caminho justifica, usando o
estimador de ida e volta suavizado do RFC 6298 — o mesmo que o próprio TCP usa — alimentado
por cada sonda respondida e compartilhado em toda a varredura. --timeout se torna
um teto em vez de um custo fixo: em um host vivo mas majoritariamente filtrado, isso
vale cerca de 5× com descobertas idênticas. Apenas uma resposta definitiva ensina algo
a ele; um tempo esgotado não diz nada sobre o caminho e nunca é alimentado de volta.
Dois lugares deliberadamente mantêm o teto completo. A troca de banner e a auditoria nunca se adaptam, porque quão rápido um host completa um handshake TCP não diz nada sobre quão rápido seu daemon SSH compõe uma saudação. Nem a re-sonda final das portas usuais do SSH, já que um SYN descartado lá é a única perda que realmente custa a esta ferramenta uma descoberta.
Saída antecipada. Um host que não responde nada em suas primeiras poucas
centenas de sondas é relatado como sem resposta em vez de consumir um tempo limite
por porta restante. Como as portas SSH são varridas primeiro, um serviço vivo é
sempre visto antes que isso possa disparar; --no-early-exit força o intervalo completo.
Limitado por design. Um orçamento de soquetes em todo o processo derivado do
limite de descritores de arquivo impede que uma varredura grande esgote descritores e
relate erroneamente serviços vivos como filtrados. A expansão de alvos verifica o
tamanho de uma rede antes de materializá-la, então um /8 perdido é recusado em milissegundos
em vez de consumir um gigabyte de memória.
Julgamentos de algoritmos curados. Cada algoritmo sinalizado vem de uma
tabela explícita com uma severidade e uma razão declarada, não uma cadeia de testes
de substring. Nomes são normalizados primeiro, então um sufixo de fornecedor não pode passar
por uma verificação — [email protected] é CBC não importa quem o fornece — e
marcadores de negociação como [email protected] nunca são avaliados como
algoritmos. Esta tabela é o que o portão de política e a comparação de deriva
em última análise se apoiam.
Ctrl+C robusto. Honrado mesmo no Windows, onde uma espera de thread ilimitada normalmente o engole: o primeiro pressionamento para graciosamente e retorna resultados parciais, um segundo força uma saída imediata.
OpenSSH_9.6p1 no Ubuntu 24.04 é corrigido
contra a maioria do que bancos de dados públicos atribuem ao 9.6p1. Isso é uma
máquina de falsos positivos — é por isso que ssh-audit removeu sua própria detecção
de CVE baseada em versão, e por que a Tenable fornece um plugin cujo único trabalho é detectar
o backporting que a quebra. Apenas o que um servidor realmente anuncia é
avaliado.nmap em varredura de portas geral, ou com masscan e
zmap em escala de internet. Esses problemas estão resolvidos.A suíte de testes é apenas biblioteca padrão, então roda em um interpretador puro:
python -m unittest discover -s tests
Instale as dependências de runtime para também exercitar os testes de auditoria baseados em Paramiko, que se pulam quando Paramiko está ausente:
pip install -r requirements-dev.txt
python -m unittest discover -s tests
Apenas escaneie sistemas que você possui ou para os quais está explicitamente autorizado a testar. Varredura não autorizada pode ser ilegal na sua jurisdição.