kabiri-labs/sshfinder
GitHub: kabiri-labs/sshfinder
sshfinder 是一个快速并行的 SSH 服务发现与安全审计工具,支持跨主机和 CIDR 范围扫描任意端口上的 SSH 服务,并标记认证方法、弱加密算法、Terrapin 漏洞及共享主机密钥。
Stars: 2 | Forks: 0
# sshfinder
[](https://github.com/kabiri-labs/sshfinder/actions/workflows/ci.yml)

`sshfinder` 是一个快速、可靠的工具,用于在一个或多个目标中发现开放的 **SSH** 服务。
它会扫描开放的 TCP 端口,然后确认其中哪些端口实际上在使用 SSH —— 这两个阶段会并发执行以提升速度。
## 功能
- **多目标** — 在单次运行中扫描多个 IP、主机名和 CIDR 网络(例如 `10.0.0.0/24`),或者从文件中加载它们。
- **非阻塞扫描引擎** — 每个进行中的连接都通过操作系统事件循环(`epoll`/`kqueue`/`select`)由单个线程驱动,因此并发成本仅相当于一个文件描述符,而非操作系统线程。完整的 1–65535 端口扫描速度比之前的线程池引擎快约 6 倍,并且目标主机只需解析一次,而不是每个端口解析一次。
- **SSH 端口优先** — SSH 实际驻留的少数几个端口(22、2222、22222……)会在每次扫描的最开始进行探测,因此即使扫描覆盖了所有 65535 个端口,通常也能在远低于一秒的时间内确认服务。
- **自适应超时** — 探测会根据网络路径的实际情况等待,而不是固定等待两秒。来自 RFC 6298 的平滑往返时间估算器 —— 也就是 TCP 本身使用的算法 —— 会根据每个被响应的探测进行调整,并在整个扫描过程中共享,因此 `--timeout` 变成了一个上限,而不是固定成本。在存活但大部分端口被过滤的主机上,这在发现结果相同的情况下能带来约 5 倍的速度提升(扫描 8000 个端口从 34 秒降至 6 秒);使用 `--no-adaptive-timeout` 可恢复固定的等待时间。
- **两种扫描后端**:
- `connect` — 便携的 TCP connect 扫描,无需特殊权限(默认)。
- `syn` — 通过 Scapy 进行的半开 SYN 扫描(速度更快,需要 root 权限)。
- **可靠的 SSH 验证** — 完整执行 RFC 4253 的身份识别交换,而不是仅仅查看线路上的前几个字节:无论是先打印法律声明横幅的服务器,还是等待客户端先表明身份的服务器,亦或是横幅被拆分在多个 TCP 段中到达的情况,都能被正确识别。同时也提供可选的完整 Paramiko 握手验证。
- **流式输出 (`--stream`)** — 在 stdout 上输出以换行符分隔的 JSON 事件,在每个端口开放和每个 SSH 服务被确认时即刷新输出,使得流水线可以在扫描仍在运行时就对第一个结果采取行动。
- **SSH 安全审计 (`--audit`)** — 将发现转化为渗透测试人员的攻击面情报:枚举接受的认证方法(并标记密码认证),列出提供的 KEX/加密/MAC/主机密钥算法并标记脆弱/已弃用的算法,检测 **Terrapin (CVE-2023-48795)**,并**关联跨目标的共享主机密钥**,以揭示克隆或负载均衡的基础设施。
- **后量子准备度 (`--pq-report`)** — 为整个资产提供一个指标:有多少 SSH 服务仍然无法协商后量子密钥交换,以及具体是哪些。OpenSSH 10.0 将 `mlkem768x25519-sha256` 设为默认选项,而 10.1 则警告经典会话容易受到*现在截获,以后解密*的威胁。它只读取 KEXINIT,因此不需要第三方库。它还会区分提供**标准化前**混合算法(已撤销的 `sntrup4591761`、Kyber 草案)的服务 —— 这些算法在转储列表中看起来像后量子算法,但在与当前所有客户端协商时使用的却是经典加密。
- **精心维护的算法评估** — 每个被标记的密钥交换、加密、MAC 和主机密钥均来自一个明确的表格,该表格包含严重级别(`critical` 或 `weak`)和具体原因,而不是一连串的子字符串匹配测试。名称会首先进行规范化处理,因此供应商的后缀无法让某种算法逃避检查 —— 无论由谁提供,`rijndael-cbc@lysator.liu.se` 始终是 CBC。诸如 `kex-strict-s-v00@openssh.com` 的协商标记也绝不会被视为算法进行评估。策略门控和基线比较最终都依赖于该表格。
- **策略门控 (`--policy`)** — 根据基线检查每个 SSH 服务,并在**违规时以非零状态退出**,从而使得扫描可以用于拦截 CI 或定时任务。内置了 `baseline`、`strict` 和 `pq`,或者可以使用自定义的 JSON 策略。规则带有 `fail`/`warn` 严重级别,并由 `--fail-on` 决定哪一种级别用于拦截。未识别的检查、字段或严重级别将被视为硬错误,绝不会是静默跳过的规则 —— 错字绝不能让一个未通过检查的资产显示为合格。
- **基线比较 (`--baseline`)** — 每晚运行该命令并与昨天的 `--json` 报告进行对比,仅查看发生变动的部分:**主机密钥发生改变**(最重要的信号 —— 通常仅在系统重建或密钥轮换后预期发生)、密码登录被新启用、加密被削弱、后量子准备度丧失、服务与端口的出现或消失。仅对在*两次*扫描中都存在的主机进行比较,且任何一次扫描都未测量的字段绝不会报告为变更,因此扫描一个机柜不会导致其他所有机柜被视作下线。
- **零必需依赖** — 默认的 connect 扫描和横幅验证仅依靠 Python 标准库即可运行。
- **设计上的边界限制** — 源自文件描述符限制的全局 socket 预算可防止大型扫描耗尽描述符,并避免将存活服务误报为被过滤;同时,目标扩展机制会拒绝实现大于 `--max-targets` 设定范围的扫描。
- **死机主机的提前退出** — 如果某台主机在最初几百次探测中没有任何响应,将被报告为无响应,而无需为每个剩余端口消耗一次超时时间。因为 SSH 的端口会被优先扫描,所以总能最先发现存活服务;`--no-early-exit` 可强制执行全范围扫描。
- **流水线化的身份识别** — 每个开放端口背后的服务会在发现端口的瞬间(与端口扫描的其余部分并发)被识别(并审计)。SSH 服务无需等待整个扫描完成即可被确认,并且每个开放端口都会被标记为 `[SSH]` / `[not ssh]`,因此开放端口绝不会与 SSH 端口混淆。
- **实时的单 socket 发现** — 开放端口和确认的 SSH 服务会在被发现的瞬间以 `host:port` 的格式打印出来,因此在扫描多台主机时,始终可以清楚地分辨哪个结果属于哪个目标。
- **实时进度与稳健的 Ctrl+C** — 实时进度指示器会显示扫描正在进行中。即使在 Windows 上(通常无限制的线程等待会吞掉该信号),Ctrl+C 也能被响应:第一次按下将优雅地停止并返回部分结果,第二次按下则会强制立即退出。
- **清晰的主机状态** — 区分开放、关闭和*被过滤 (filtered)* 的端口,因此被防火墙拦截或不可达的主机会被如实报告,而不是看起来像程序卡死。
- **机读输出** — `--format text|json|sarif|csv`,可选择写入文件。面向安全工具的 **SARIF 2.1.0**(已通过 OASIS 模式验证;发现结果锚定于 `host:port` 逻辑位置,并带有稳定的指纹,使得使用者可以在多次运行中追踪同一个问题)。面向电子表格的 **CSV**:每个确认的 SSH 服务占据一行,这正是资产清册实际进行过滤和排序时所需的格式。
## 安装
```
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
# 安装 paramiko(推荐:启用完整的 --audit 深度检查和
# --validate paramiko):
pip install -r requirements.txt
# 可选,仅用于半开 SYN 扫描(需要 root):
pip install scapy>=2.5
```
核心的 connect 扫描、横幅验证以及 `--audit` 的无依赖部分(算法清单、弱加密标记、Terrapin)无需任何第三方软件包即可运行;而 paramiko 则用于解锁主机密钥指纹、认证方法枚举和共享密钥关联。
要求 **Python 3.9+**。
## 用法
```
python sshfinder.py [targets ...] [options]
```
### 选项
| 选项 | 描述 |
| ------ | ----------- |
| `targets` | 一个或多个 IP、主机名或 CIDR 网络。 |
| `-iL, --target-file FILE` | 从文件中读取目标(每行一个,允许使用 `#` 添加注释)。 |
| `-p, --ports SPEC` | 要扫描的端口,例如 `22,80,1000-2000`(默认:`1-65535`)。 |
| `--scan-method {auto,connect,syn}` | 扫描后端(默认:`auto`)。 |
| `--validate {banner,paramiko,none}` | SSH 验证策略(默认:`banner`)。 |
| `--audit` | 审计每个 SSH 服务(算法、主机密钥、认证方法、Terrapin、后量子准备度、共享密钥关联)。 |
| `--pq-report` | 报告整个资产的后量子密钥交换准备度。仅读取 KEXINIT —— 无需第三方库。 |
| `--policy NAME_OR_PATH` | 根据策略(`baseline`、`strict`、`pq` 或 JSON 文件)检查每个 SSH 服务,并在违规时以 `3` 退出。 |
| `--fail-on {fail,warn,never}` | 哪种策略严重级别用于拦截退出状态码(默认:`fail`)。 |
| `--baseline FILE` | 与之前的 `--json` 报告进行对比,并列出更改的内容。 |
| `--fail-on-drift` | 当对比产生警报时以 `4` 退出。服务的出现或消失不会触发拦截。 |
| `-t, --timeout SECONDS` | 探测的最长等待时间(默认:`2.0`)。 |
| `--min-timeout SECONDS` | 自适应探测超时的下限(默认:`0.1`)。 |
| `--no-adaptive-timeout` | 对每个探测等待完整的 `--timeout` 时间,而不是根据测量的往返时间进行自适应调整。 |
| `-w, --workers N` | 每台主机的并发连接数(默认:`512`)。 |
| `--max-sockets N` | 整个扫描过程中同时打开的探测 socket 数量上限(默认:从文件描述符限制中推导得出)。 |
| `--host-concurrency N` | 并行扫描的主机数量(默认:`16`)。 |
| `-r, --retries N` | 超时探测的重试次数(默认:`0`)。 |
| `--max-targets N` | 拒绝大于此数量的目标列表(默认:`65536`)。 |
| `--no-early-exit` | 即使主机完全没有响应,也扫描所有端口。 |
| `--format {text,json,sarif,csv}` | 输出格式(默认:`text`)。 |
| `--json` | `--format json` 的简写。 |
| `--stream` | 随着结果的发现,在 stdout 上输出以换行符分隔的 JSON 事件。 |
| `-o, --output FILE` | 将结果写入文件而不是 stdout。 |
| `--no-progress` | 禁用实时进度指示器。 |
| `-v` | 详细的(调试)日志。 |
| `-q, --quiet` | 抑制进度和信息类日志。 |
### 退出代码
| 代码 | 含义 |
| ---- | ------- |
| `0` | 成功。什么都没找到也算成功 —— 空资产不是错误。 |
| `1` | 硬错误:所有目标均扫描失败,或无法写入输出文件。 |
| `2` | 错误的调用(未知的标志、无效的端口范围、格式错误的策略)。 |
| `3` | 达到或超过 `--fail-on` 设定的策略违规。仅在使用 `--policy` 时返回。 |
| `4` | 基线漂移警报。仅在使用 `--baseline --fail-on-drift` 时返回。 |
| `130` | 被 Ctrl+C 中断。 |
硬错误的优先级高于策略判定,而策略判定的优先级高于漂移:如果没有任何目标可达,说明扫描未能证明任何关于合规性的问题,因此会返回 `1` 而不是误导性的通过或失败;并且未达到规定的基线要求是比“某些内容发生了变化”更具体的发现。
`auto` 会在以 root 身份运行且安装了 Scapy 时选择 SYN 扫描,否则将回退到不需要权限的 connect 扫描。
## 示例
在常见的 SSH 端口上扫描单个主机:
```
python sshfinder.py 192.168.1.1 -p 22,2222
```
扫描整个子网并输出 JSON:
```
python sshfinder.py 10.0.0.0/24 -p 22,2222 --json -o results.json
```
使用快速的 SYN 扫(以 root 身份)扫描来自文件的多个目标:
```
sudo python sshfinder.py -iL targets.txt --scan-method syn
```
通过完整的握手严格验证 SSH:
```
python sshfinder.py example.com -p 22 --validate paramiko
```
在发现结果时将其流式传输到流水线中,无需等待扫描完成:
```
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"}
```
跨子网审计 SSH 攻击面(认证方法、弱加密、Terrapin、共享主机密钥):
```
python sshfinder.py 10.0.0.0/24 -p 22,2222 --audit
```
检查整个资产的后量子准备度(无需额外依赖):
```
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+ (10.0+ preferred)
```
将 CI 任务或定时扫描交由基线进行拦截 —— 如果任何服务违规,则以 `3` 退出:
```
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
```
```
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
```
编写自己的 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"}
]
}
```
可用的检查项目包括:`password_auth`、`terrapin`、`weak_algorithms`、`post_quantum`(配合 `require`:`ready`、`legacy`、`absent`),以及针对 `kex_algorithms`、`host_key_algorithms`、`ciphers` 或 `macs` 某个 `field` 的 `forbid` / `require`。在策略加载时,其他任何内容都会被拒绝。
将 SSH 清单导出到电子表格,每个服务占据一行:
```
python sshfinder.py 10.0.0.0/24 -p 22,2222 --audit --format csv -o ssh.csv
```
为安全工具输出 SARIF 2.1.0:
```
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline --format sarif \
-o sshfinder.sarif
```
每个发现结果都锚定在一个 `host:port` 的**逻辑位置**上 —— 这是 SARIF 中专为不与源文件绑定的结果设计的部分 —— 并且包含一个稳定的 `partialFingerprints` 条目,使得使用者可以在多次运行中追踪同一个发现,而不是每天晚上都触发一个新的警报。当提供了 `--policy` 时,策略违规*即是*发现的结果;反之,则报告内置的审计发现。无论哪种方式,每个结果只会出现一次。
随着时间推移跟踪资产 —— 先捕获一份报告,然后与之对比:
```
# 每晚在 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
```
比较过程会自动匹配基线的深度:如果保存的基线包含主机密钥指纹,则本次扫描也会执行深度探测,因此浅层的重新扫描绝不会显示为所有的密钥都消失了。
审计输出示例:
```
=== 10.0.0.5 ===
open: 10.0.0.5:22
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, with Encrypt-then-MAC, to Terrapin
Shared SSH host keys (possible shared/cloned hosts):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
```
算法清单、弱加密标记和 Terrapin 检查无需任何依赖即可工作。主机密钥指纹、认证方法枚举和共享密钥关联使用 Paramiko(`pip install paramiko`)。
## 开发
测试套件仅需标准库,因此可以在纯净的解释器上运行:
```
python -m unittest discover -s tests
```
安装运行时依赖还可以运行基于 Paramiko 的审计测试,如果缺少 Paramiko,这些测试会自动跳过:
```
pip install -r requirements-dev.txt
python -m unittest discover -s tests
```
## 法律声明
仅扫描您拥有或被明确授权测试的系统。未经授权的扫描在您所在的司法管辖区可能是违法的。
标签:Python, SSH, 云存储安全, 插件系统, 无后门, 漏洞审计, 网络扫描, 逆向工具