发现网络上的每一个 SSH 服务,判断其是否符合你的标准,并在发生变化时收到通知。
sshfinder 是一个无第三方依赖的单一 Python 文件。将其指向一个 CIDR 网段,它就能发现 SSH 实际监听的任何位置——不仅仅是 22 端口——确认每个服务确实在运行 SSH 协议,评估其加密安全态势,并在有服务不符合你的策略时返回非零退出码。
大多数团队无法回答关于自身 SSH 资产状况的三个问题:
现有工具各自只解决了部分问题便止步于此:
| 工具 | 发现 SSH | 评估 | 覆盖整个资产 |
|---|---|---|---|
nmap | 是 | 浅层,通过 NSE 脚本 | 是 |
ssh-audit | 否——你需要提供单个主机 | 深入 | 否 |
masscan / zmap | 互联网规模 | 否 | 是 |
sshfinder | 是 | 是 | 是 |
这一空白——在一个工具中同时实现发现、评估和判定——正是本工具存在的意义。如果你只需要审计一个已知的主机,请使用 ssh-audit;它在单个服务上的分析深度超过本工具。
渗透测试人员会发现审计和 SOCKS 代理功能很有用,但该工具的设计初衷是针对你拥有的资产反复运行相同的扫描,而非用于一次性测试项目。
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
无需安装,无依赖。需要 Python 3.9+。
它做的三件事,用三条命令展示:
# 1. 资产盘点——网络上存在哪些 SSH 服务?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. 合规判定——是否符合我们的标准?(不符合则退出码为 3)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. 漂移检测——自昨晚以来发生了什么变化?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
默认扫描全部 65535 个端口,因为位于非标准端口上的 SSH 服务恰恰是没有人记录在案的那一个。每个开放端口都会被标记,因此开放端口永远不会被静默地计为 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)
确认过程是真实的 RFC 4253 标识交换,而非仅查看线路上的前几个字节。先打印合法横幅的服务器、等待客户端自我标识的服务器,以及横幅跨 TCP 分段到达的服务器,都能被正确识别——在朴素的实现中,这些情况每一种都会导致漏报。
添加 --audit 参数可查看每个服务的完整信息:
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, …
共享 SSH 主机密钥(可能存在共享/克隆主机):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
最后一块值得关注:跨机器复用的主机密钥通常意味着克隆的虚拟机或共享镜像,而且这意味着攻陷一台主机就等于攻陷了所有主机的身份。
OpenSSH 10.0 将 mlkem768x25519-sha256 设为默认密钥交换算法,10.1 版本警告经典会话面临现在存储、以后解密的捕获风险。--pq-report 直接回答资产层面的问题,仅使用 KEXINIT——因此无需第三方库:
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+
pre-standard(前标准)类别是最容易让人上当的。一个通告 [email protected] 或 Kyber 草案的服务器在算法转储中看起来是后量子的,但 OpenSSH 在 2020 年就放弃了这套已撤回的参数集——因此当前客户端找不到共同方法,会回退到经典加密。如果将其计为就绪,那比完全不检查更糟糕。
报告描述问题。策略断言问题,并可使构建失败:
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
内置三个策略——baseline、strict 和 pq——以其强制执行的成果命名,而非以发行版命名。规则带有 fail 或 warn 严重级别,--fail-on 决定哪个级别触发退出码,因此团队可以先以警告级别采用更严格的标准,之后无需编辑任何内容即可升级。
以 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 字段的 forbid / require。
任何其他内容都会在策略加载时、扫描开始前产生硬错误。 一个静默跳过其无法理解的规则的检查门比没有检查门更糟糕:运行显示绿色通过,而没有人知道该检查从未执行。
$ 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)
每晚针对昨天的报告运行,只查看变化的内容:
# 每晚,在 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
主机密钥发生变化是这里最需要关注的信号——仅在重建或密钥轮换后才会预期发生,其他任何时候都值得关注。
只有 alert 级别会触发 --fail-on-drift。退役主机属于正常变动,如果每晚任务因此失败,会让所有人习惯性地忽略结果。
比较过程谨慎地避免凭空捏造变化。两次扫描都未测量的字段永远不会被报告为已变化,仅比较两次扫描中都存在的主机,且基线中包含主机密钥指纹会使本次扫描也运行深度探测——因此浅层重新扫描永远不会被误读为所有密钥都已消失。
--format text|json|sarif|csv,可选通过 -o 写入文件。
csv — 每个已确认的 SSH 服务一行。这是资产清单实际进行排序和筛选的形态。json — 原生报告格式,也是 --baseline 的输入格式。sarif — SARIF 2.1.0,已针对 OASIS 模式验证。发现项锚定到 host:port 逻辑位置并携带稳定指纹,因此消费者可以跨每晚运行跟踪同一发现项,而非每次都打开新警报。--stream — 换行分隔的 JSON 事件,在每个端口打开和每个服务确认时即时刷新,使管道可以在扫描仍在运行时对第一个结果采取行动: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"}
关于 SARIF 和 GitHub 代码扫描。 SARIF 结果必须携带非空的工件位置,否则
upload-sarif会拒绝该文件,因此会随逻辑位置一起输出一个合成的ssh://host:portURI。它不会解析为你仓库中的文件,因此警报不会带有代码锚点。请将其视为适用于安全工具的一般性 SARIF——VS Code SARIF 查看器、Azure DevOps、归档——而非用于注释差异的方式。
这是策略和漂移功能的核心意义所在,因此值得精确说明:
| 代码 | 含义 |
|---|---|
0 | 成功。未发现任何内容仍算成功——空资产不是错误。 |
1 | 硬错误:所有目标均扫描失败,或无法写入输出文件。 |
2 | 调用错误(未知标志、无效端口规范、格式错误的策略或代理)。 |
3 | 达到或超过 --fail-on 级别的策略违规。仅在 --policy 下出现。 |
4 | 基线漂移警报。仅在 --baseline --fail-on-drift 下出现。 |
130 | 通过 Ctrl+C 中断。 |
硬错误的优先级高于策略判定,策略判定的优先级高于漂移。如果没有任何目标可达,则扫描也无法证明任何合规性结论,因此你会得到 1 而非误导性的通过或失败;而未能达到既定标准比"有变化发生"是更具体的发现。
核心扫描、横幅验证以及 --audit 中无依赖的部分(算法清单、弱加密标志、Terrapin、后量子就绪状态)仅需 Python 3.9+。
# 推荐:解锁 --audit 中的主机密钥指纹、认证方法枚举和共享密钥关联,以及 --validate paramiko。
pip install -r requirements.txt
# 可选,仅用于半开 SYN 扫描(需要 root 权限):
pip install scapy>=2.5