
Параллельный инструмент обнаружения SSH-сервисов и аудитор безопасности, который сканирует любые порты, проверяет SSH-баннеры и проводит аудит методов аутентификации, слабой криптографии, уязвимости Terrapin и повторного использования ключей хостов на хостах и в CIDR-диапазонах.
Найдите каждый SSH-сервис в вашей сети, оцените, соответствует ли он вашим стандартам, и узнайте, когда это изменится.
sshfinder — это один Python-файл без обязательных зависимостей. Укажите ему
диапазон CIDR, и он обнаружит SSH везде, где он реально прослушивается — не только
на порту 22 — подтвердит, что каждый из них действительно говорит по SSH, оценит его
криптографическую позицию и вернёт ненулевой код выхода, когда что-то не соответствует вашей политике.
Большинство команд не могут ответить на три вопроса о собственном SSH-хозяйстве:
Существующие инструменты отвечают на часть этих вопросов и останавливаются:
Этот пробел — обнаружение и оценка и вердикт в одном артефакте — именно то,
для заполнения чего существует этот инструмент. Если вам нужно проверить только один хост, который
вы уже знаете, используйте 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), а также forbid / require по
field из kex_algorithms, host_key_algorithms, ciphers или macs.
Всё остальное — это жёсткая ошибка при загрузке политики, до начала сканирования. Шлюз, который молча пропускает правило, которое он не понимает, хуже, чем отсутствие шлюза: запуск проходит зелёным, и никто не узнаёт, что проверка никогда не выполнялась.
$ 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отклоняет файл, поэтому синтетический URIssh://host:portвыводится вместе с логическим расположением. Он не разрешается в файл в вашем репозитории, поэтому оповещения появляются без привязки к коду. Воспринимайте это как SARIF для инструментов безопасности в целом — просмотрщик SARIF VS Code, Azure DevOps, архивирование — а не как способ аннотировать diff.
В этом весь смысл функций политики и дрейфа, поэтому их стоит указать точно:
Жёсткая ошибка имеет приоритет над вердиктом политики, а вердикт политики имеет приоритет над дрейфом.
Если ничего не было доступно, сканирование ничего не доказало о соответствии в любом
случае, поэтому вы получаете 1, а не вводящий в заблуждение проход или провал; и провал
заявленной планки — это более конкретная находка, чем «что-то изменилось».
Основное сканирование, проверка баннера и не зависящие от зависимостей части --audit
(инвентаризация алгоритмов, флаги слабой криптографии, Terrapin, постквантовая готовность)
требуют только Python 3.9+.
# Рекомендуется: разблокирует отпечатки ключей хостов, перечисление методов аутентификации и
# корреляцию общих ключей в --audit, а также --validate paramiko.
pip install -r requirements.txt
# Опционально, только для полуоткрытых SYN-сканирований (требует root):
pip install scapy>=2.5
--scan-method auto выбирает SYN-сканирование при запуске от root с установленным Scapy,
а в противном случае возвращается к connect-сканированию без привилегий.
SOCKS нельзя комбинировать с SYN-сканированием — SOCKS5 переносит TCP-потоки, а не
сырые пакеты.
python sshfinder.py 10.0.0.0/24 -p 22 --socks user:[email protected]:1080
Обнаружение, обмен баннерами и аудит — всё проходит через прокси, поэтому результаты никогда не бывают наполовину туннелированными. Недоступный прокси сообщается как ошибка сканирования, а никогда как «SSH не найден».
--max-rate ограничивает зонды в секунду по всему сканированию. Параллелизм
ограничивает, сколько соединений открыто одновременно; это ограничивает, как быстро начинаются
новые, — это потолок, который вам нужно уметь обещать перед сканированием чего-либо
в рамках правил взаимодействия.
Заметки по реализации для случаев, когда поведение выше требует объяснения.
Движок сканирования. Каждым соединением в полёте управляет один поток через
цикл событий ОС (epoll/kqueue/select), поэтому параллелизм стоит файлового
дескриптора, а не потока ОС, и каждый хост разрешается один раз, а не
один раз на порт. Полный проход 1–65535 выполняется примерно в 6 раз быстрее, чем
дизайн с пулом потоков.
Сначала SSH-порты. Небольшой набор портов, на которых SSH реально живёт (22, 2222, 22222, …), зондируется в начале каждого прохода. При полном проходе первый подтверждённый SSH-сервис появляется примерно через 0,2 секунды вместо 16.
Одно рукопожатие на сервис. Сокет, обнаруживший открытый порт, передаётся напрямую в обмен баннерами, поэтому подтверждённый SSH-сервис стоит одного TCP-рукопожатия, а не двух.
Адаптивный таймаут. Зонды ждут столько, сколько оправдывает путь, используя
сглаженную оценку времени кругового обхода из RFC 6298 — ту, что использует сам TCP — питаемую
каждым отвеченным зондом и разделяемую по всему сканированию. --timeout становится
потолком, а не фиксированной стоимостью: на живом, но в основном фильтруемом хосте это даёт
примерно 5× при идентичных результатах. Только определённый ответ чему-то учит;
таймаут ничего не говорит о пути и никогда не передаётся обратно.
Два места намеренно сохраняют полный потолок. Обмен баннерами и аудит никогда не адаптируются, потому что скорость завершения TCP-рукопожатия хостом ничего не говорит о том, как быстро его SSH-демон составляет приветствие. То же касается финального повторного зондирования обычных портов SSH, поскольку потерянный SYN там — это та единственная потеря, которая на самом деле стоит этому инструменту находки.
Ранний выход. Хост, который вообще ничего не отвечает в первых нескольких
сотнях зондов, сообщается как не отвечающий, а не потребляет один таймаут на
каждый оставшийся порт. Поскольку порты SSH проходятся первыми, живой сервис всегда
виден до того, как это может сработать; --no-early-exit принудительно проходит весь диапазон.
Ограничен по дизайну. Бюджет сокетов на уровне процесса, полученный из
лимита файловых дескрипторов, не даёт большому сканированию исчерпать дескрипторы и
ошибочно сообщить о живых сервисах как о фильтруемых. Развёртывание целей проверяет
размер сети перед его материализацией, поэтому случайный /8 отклоняется за миллисекунды,
а не потребляет гигабайт памяти.
Курируемые суждения об алгоритмах. Каждый помеченный алгоритм происходит из
явной таблицы с серьёзностью и указанной причиной, а не из цепочки проверок подстрок.
Имена сначала нормализуются, поэтому суффикс вендора не может проскочить мимо
проверки — [email protected] — это CBC, кто бы его ни поставлял — а
маркеры согласования, такие как [email protected], никогда не оцениваются как
алгоритмы. Эта таблица — то, на чём в конечном счёте основываются шлюз политики и сравнение дрейфа.
Надёжный Ctrl+C. Обрабатывается даже в Windows, где неограниченное ожидание потока обычно его проглатывает: первое нажатие останавливается корректно и возвращает частичные результаты, второе принудительно немедленно завершает работу.
OpenSSH_9.6p1 на Ubuntu 24.04 исправлен
против большинства того, что публичные базы данных приписывают 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
Сканируйте только системы, которыми вы владеете или на тестирование которых у вас есть явное разрешение. Несанкционированное сканирование может быть незаконным в вашей юрисдикции.
| Инструмент | Обнаруживает SSH | Оценивает его | По всей инфраструктуре |
|---|
nmap | да | поверхностно, через NSE-скрипты | да |
ssh-audit | нет — вы даёте ему один хост | глубоко | нет |
masscan / zmap | в масштабе интернета | нет | да |
sshfinder | да | да | да |
| Код | Значение |
|---|
0 | Успех. Ничего не найдено — это всё равно успех: пустая инфраструктура — не ошибка. |
1 | Жёсткая ошибка: все цели не удалось просканировать или не удалось записать выходной файл. |
2 | Некорректный вызов (неизвестный флаг, недопустимая спецификация порта, некорректная политика или прокси). |
3 | Нарушение политики на уровне --fail-on или выше. Только с --policy. |
4 | Оповещение о дрейфе базового отчёта. Только с --baseline --fail-on-drift. |
130 | Прервано с помощью Ctrl+C. |
| Параметр | Описание |
|---|
targets | Один или несколько IP-адресов, имён хостов или сетей CIDR. |
-iL, --target-file FILE | Читать цели из файла (по одной в строке, комментарии # разрешены). |
-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 | Ограничение зондов в секунду по всему сканированию (по умолчанию: без ограничения). |
-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 | Вывод версии и выход. |