
任意のポートをスキャンし、SSHバナーを検証し、認証方式、脆弱な暗号化、Terrapin脆弱性、およびホスト間・CIDR範囲にわたるホストキーの再利用を監査する、並列SSHサービス検出・セキュリティ監査ツール。
ネットワーク上のすべてのSSHサービスを検出し、基準を満たしているかを判定し、変更があった場合に通知します。
sshfinder は、依存関係のない単一のPythonファイルです。CIDRレンジを指定すると、ポート22だけでなく、実際にリッスンしている場所のSSHを検出し、それぞれが本当にSSHを話していることを確認し、暗号化の状態を評価し、ポリシーに違反するものがある場合は非ゼロの終了コードを返します。
ほとんどのチームは、自社のSSH資産に関する3つの質問に答えられません。
既存のツールはそれぞれ一部に答え、そこで止まります。
| ツール | SSHを検出 | 評価 | フリート全体 |
|---|---|---|---|
nmap | はい | 浅い(NSEスクリプト経由) | はい |
ssh-audit | いいえ — 1台のホストを指定する | 深い | いいえ |
masscan / zmap | インターネット規模 | いいえ | はい |
sshfinder | はい | はい | はい |
そのギャップ — 検出と評価と判定を1つの成果物で実現すること — が、このツールが存在する目的です。すでに知っている1台のホストだけを監査する必要がある場合は、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+ が必要です。
3つのコマンドで実行できる3つの機能:
# 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
最後のブロックは知っておく価値があります。マシン間で再利用されるホストキーは、通常、クローンされたVMまたは共有イメージを意味し、1つのホストが侵害されると、すべてのホストのIDが侵害されることを意味します。
OpenSSH 10.0 は mlkem768x25519-sha256 をデフォルトの鍵交換にしました。10.1 は、従来のセッションが store now, decrypt later キャプチャに対して開かれていると警告しています。--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
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 の field に対する 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)
昨夜のレポートに対して毎晩実行し、移動したものだけを確認します:
# 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
変更されたホストキー は、ここで最も重要なシグナルです — 再構築またはキーローテーション後にのみ期待され、それ以外の場合は調査する価値があります。
--fail-on-drift をゲートするのは alert のみです。廃止されたホストは通常のチャーンであり、それで毎晩のジョブを失敗させると、誰もが結果を無視するように訓練されてしまいます。
比較は、変更を発明しないように注意しています。どちらのスキャンも測定しなかったフィールドが変更されたとして報告されることはなく、両方のスキャンに存在するホストのみが比較され、ホストキーフィンガープリントを保持するベースラインにより、このスキャンもディーププローブを実行します — そのため、浅い再スキャンがすべてのキーが消えたと読まれることはありません。
--format text|json|sarif|csv、オプションで -o を使用してファイルに書き込めます。
csv — 確認されたSSHサービスごとに1行。資産インベントリが実際にソートおよびフィルタリングされる形式です。json — ネイティブレポートであり、--baseline の入力形式です。sarif — OASISスキーマに対して検証されたSARIF 2.1.0。検出結果は 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、アーカイブ — 差分に注釈を付ける方法としては扱わないでください。
ポリシーとドリフト機能の要点なので、正確に述べる価値があります:
ハードエラーはポリシー判定より優先され、ポリシー判定はドリフトより優先されます。何も到達できなかった場合、スキャンはコンプライアンスについて何も証明していないため、誤解を招く合格または不合格ではなく 1 が返されます。また、明示された基準に失敗することは、「何かが変わった」よりも具体的な発見です。
コアスキャン、バナー検証、および --audit の依存関係のない部分(アルゴリズムインベントリ、弱暗号フラグ、Terrapin、ポスト量子対応)には、Python 3.9+ のみが必要です。
# Recommended: unlocks host key fingerprints, auth-method enumeration and
# shared-key correlation in --audit, plus --validate paramiko.
pip install -r requirements.txt
# Optional, only for half-open SYN scans (needs root):
pip install scapy>=2.5
--scan-method auto は、rootでScapyがインストールされている場合にSYNスキャンを選択し、それ以外の場合は特権不要のコネクトスキャンにフォールバックします。SOCKSはSYNスキャンと組み合わせることはできません — SOCKS5はTCPストリームを運び、生のパケットは運びません。
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
検出、バナー交換、監査はすべてピボットを通過するため、結果が半分トンネルされることはありません。到達不能なプロキシは、「SSHが見つかりません」ではなく、スキャンエラーとして報告されます。
--max-rate は、スキャン全体の1秒あたりのプローブ数を制限します。並行性は同時に開いている接続数を制限します。これは、新しい接続が開始される速度を制限し、エンゲージメントルールの下で何かをスキャンする前に約束できる上限です。
上記の動作を説明するための実装メモ。
スキャンエンジン。 進行中のすべての接続は、OSイベントループ(epoll/kqueue/select)によって1つのスレッドから駆動されるため、並行性はOSスレッドではなくファイル記述子を消費し、各ホストはポートごとではなく1回だけ解決されます。完全な1〜65535スイープは、スレッドプール設計よりも約6倍高速に実行されます。
SSHポートを最初に。 SSHが実際に存在する少数のポート(22、2222、22222など)は、すべてのスイープの先頭でプローブされます。完全なスイープでは、最初に確認されたSSHサービスが16秒ではなく約0.2秒で表示されます。
サービスごとに1つのハンドシェイク。 オープンポートを検出したソケットは、そのままバナー交換に渡されるため、確認されたSSHサービスは2つではなく1つのTCPハンドシェイクで済みます。
適応型タイムアウト。 プローブは、パスが保証する限り待機します。RFC 6298の平滑化された往復時間推定器 — TCP自体が使用するもの — を使用し、応答したすべてのプローブによって供給され、スキャン全体で共有されます。--timeout は固定コストではなく上限になります。ライブだがほとんどフィルタリングされたホストでは、同じ結果で約5倍の価値があります。明確な回答だけが何かを教えます。タイムアウトはパスについて何も言わず、決してフィードバックされません。
2つの場所で意図的に完全な上限を維持しています。バナー交換と監査は適応しません。ホストがTCPハンドシェイクを完了する速さは、そのSSHデーモンがグリーティングを構成する速さとは関係ないからです。SSHの通常のポートの最終再プローブも同様です。そこでのSYNドロップは、このツールが発見を失う唯一の損失だからです。
早期終了。 最初の数百プローブでまったく応答しないホストは、残りのポートごとに1つのタイムアウトを消費するのではなく、応答なしとして報告されます。SSHのポートが最初にスイープされるため、ライブサービスはこれが発生する前に常に確認されます。--no-early-exit は完全な範囲を強制します。
設計による制限。 ファイル記述子の制限から導出されたプロセス全体のソケット予算により、大規模なスキャンが記述子を使い果たし、ライブサービスをフィルタリング済みとして誤って報告するのを防ぎます。ターゲットの展開は、ネットワークのサイズを具体化する前にチェックするため、迷いの /8 はギガバイトのメモリを消費するのではなく、ミリ秒単位で拒否されます。
キュレーションされたアルゴリズム判定。 フラグが付けられたすべてのアルゴリズムは、部分文字列テストの連鎖ではなく、重大度と明示された理由を持つ明示的なテーブルから来ています。名前は最初に正規化されるため、ベンダーサフィックスがチェックをすり抜けることはありません — [email protected] は誰が提供してもCBCです — また、[email protected] のようなネゴシエーションマーカーはアルゴリズムとして評価されることはありません。このテーブルは、ポリシーゲートとドリフト比較が最終的に依存するものです。
堅牢なCtrl+C。 通常は無制限のスレッド待機がそれを飲み込むWindowsでも尊重されます。最初の押下でグレースフルに停止し部分的な結果を返し、2回目の押下で即時終了を強制します。
OpenSSH_9.6p1 は、公開データベースが9.6p1に帰属するもののほとんどに対してパッチが適用されています。これは誤検知マシンです — ssh-audit が独自のバージョンベースのCVE検出を削除した理由であり、Tenableがそれを壊すバックポートを検出することだけを仕事とするプラグインを出荷する理由です。サーバーが実際にアドバタイズするものだけが評価されます。nmap と競合する、またはインターネット規模で masscan と zmap と競合する。これらの問題は解決されています。テストスイートは標準ライブラリのみなので、素のインタープリターで実行できます:
python -m unittest discover -s tests
Paramikoバックエンドの監査テストも実行するには、ランタイム依存関係をインストールします。Paramikoがない場合は自動的にスキップされます:
pip install -r requirements-dev.txt
python -m unittest discover -s tests
所有しているシステム、または明示的にテストを許可されたシステムのみをスキャンしてください。許可のないスキャンは、お住まいの地域では違法となる場合があります。
| コード | 意味 |
|---|
0 | 成功。何も見つからないことも成功です — 空の資産はエラーではありません。 |
1 | ハードエラー: すべてのターゲットのスキャンに失敗したか、出力ファイルを書き込めませんでした。 |
2 | 不正な呼び出し(不明なフラグ、無効なポート仕様、不正なポリシーまたはプロキシ)。 |
3 | --fail-on 以上のポリシー違反。--policy でのみ発生します。 |
4 | ベースラインドリフトアラート。--baseline --fail-on-drift でのみ発生します。 |
130 | Ctrl+Cで中断。 |
| オプション | 説明 |
|---|
targets | 1つ以上のIP、ホスト名、またはCIDRネットワーク。 |
-iL, --target-file FILE | ファイルからターゲットを読み取ります(1行に1つ、# コメント許可)。 |
-p, --ports SPEC | スキャンするポート。例: 22,80,1000-2000(デフォルト: 1-65535)。 |
--audit | 各SSHサービスを監査: アルゴリズム、ホストキー、認証方法、Terrapin、ポスト量子対応、共有キー相関。 |
--pq-report | 資産全体のポスト量子対応をレポートします。サードパーティのライブラリは不要です。 |
--policy NAME_OR_PATH | 各サービスを baseline、strict、pq、またはJSONポリシーファイルに対してチェックします。違反時は 3 で終了します。 |
--fail-on {fail,warn,never} | 終了コードをゲートするポリシーの重大度(デフォルト: fail)。 |
--baseline FILE | 以前の --json レポートと比較し、変更点をリストします。 |
--fail-on-drift | 比較がアラートを発生させた場合に 4 で終了します。 |
--format {text,json,sarif,csv} | 出力形式(デフォルト: text)。 |
--json | --format json の省略形。 |
--stream | 結果が見つかるたびに改行区切りのJSONイベントを出力します。 |
-o, --output FILE | 結果をstdoutではなくファイルに書き込みます。 |
--validate {banner,paramiko,none} | SSH検証戦略(デフォルト: banner)。 |
--scan-method {auto,connect,syn} | スキャンバックエンド(デフォルト: auto)。 |
--socks [user:pass@]host:port | SOCKS5プロキシを介してすべてのターゲットに到達します。 |
--max-rate N | スキャン全体の1秒あたりのプローブ数を制限します(デフォルト: 制限なし)。 |
-t, --timeout SECONDS | プローブが待機できる最大時間(デフォルト: 2.0)。 |
--min-timeout SECONDS | 適応型プローブタイムアウトの下限(デフォルト: 0.1)。 |
--no-adaptive-timeout | すべてのプローブで完全な --timeout を待機します。 |
-w, --workers N | ホストごとの進行中の接続数(デフォルト: 512)。 |
--max-sockets N | 同時に開くプローブソケットの上限(デフォルト: ファイル記述子の制限から)。 |
--host-concurrency N | 並行してスキャンするホスト数(デフォルト: 16)。 |
-r, --retries N | タイムアウトしたプローブの再試行回数(デフォルト: 0)。 |
--max-targets N | これより大きいターゲットリストを拒否します(デフォルト: 65536)。 |
--no-early-exit | まったく応答しないホストでもすべてのポートをスイープします。 |
--no-progress | ライブ進行インジケーターを無効にします。 |
-v, --verbose | 詳細ログ(-vv はParamikoの沈黙も解除します)。 |
-q, --quiet | 進行状況と情報ログを抑制します。 |
--version | バージョンを出力して終了します。 |