SNMP брутфорс на Python: пишем сканер community string вместо onesixtyone

На HTB-машине с открытым UDP 161 запускаешь onesixtyone с дефолтным словарём — отрабатывает за секунду, ноль результатов. Тот же перебор через Python-скрипт на 40 строк с расширенным списком из SecLists находит нестандартную community string за минуту. Дело не в скорости (onesixtyone быстрее на порядок), а в контроле: какой словарь подсунуть, какой таймаут выставить, что делать с найденной строкой автоматически. Соберём рабочий SNMP-брутфорс на Python — от ручной сборки BER-пакета до многопоточного перебора — и разберём каждую строку кода.
Зачем писать свой SNMP-сканер: Python для пентеста вместо готовых утилит
onesixtyone — быстрый инструмент на C, отправляет SNMP-запросы асинхронно и находит валидные community string за секунды. nmap --script snmp-brute делает то же через NSE-скрипт на Lua: открывает отправляющий сокет и параллельно pcap-сокет для перехвата ответов (согласно документации Nmap). Metasploit предлагает модуль auxiliary/scanner/snmp/snmp_login с поддержкой словарей и автоматическим определением READ/WRITE-доступа (по данным OfSec, модуль проверяет диапазоны IP и валидирует уровень привилегий для каждой найденной строки).
Зачем тогда свой скрипт?
Первое — понимание протокола. Когда руками формируешь SNMP-пакет из байтов, становится очевидно, почему community string уязвима: она лежит в теле запроса как OCTET STRING без какой-либо защиты. Это понимание пригодится в Wireshark, при написании правил детектирования и при работе с нестандартными реализациями агентов.
Второе — гибкость. Готовые инструменты не дают на лету добавить логику: автоматически запустить snmpwalk при находке, записать результат в JSON, протестировать одновременно v1 и v2c или отфильтровать ответы по содержимому OID. Свой скрипт — свои правила.
Третье — доступность. На CTF-площадках и в ограниченных средах (контейнер без пакетного менеджера, минимальный образ) Python почти всегда есть, а onesixtyone — далеко не факт.
В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды — её идентификаторы) перебор community string — это техника Password Guessing (T1110.001, тактика Credential Access). Обнаружение SNMP-сервисов — Network Service Discovery (T1046, тактика Discovery). После нахождения валидной строки и выгрузки конфигурации — SNMP MIB Dump (T1602.001, тактика Collection).
Бизнес-логика атаки: зачем злоумышленнику community string
Валидная community string даёт доступ к MIB-дереву (Management Information Base — иерархическая база параметров устройства). Оттуда вытягиваются:
- Учётные данные. По данным Rapid7, в MIB-дампах обнаруживались email-адреса, хеши паролей и cleartext-пароли — например, в логах неудачных аутентификаций, когда пользователь случайно ввёл пароль в поле логина.
- Сетевая топология. IP-адреса интерфейсов, маршруты, ARP-таблицы, списки запущенных процессов с аргументами командной строки.
- Контроль устройства. Если найдена строка с WRITE-доступом (часто
private), атакующий может менять настройки, сбрасывать пароли, извлекать конфигурации маршрутизаторов.
Последствия выходят за рамки разведки. CVE-2022-20924 в Cisco ASA/FTD (CVSS 7.7 HIGH; вектор: AV:N — атака по сети, AC:L — низкая сложность, PR:L — нужны минимальные привилегии, то есть валидная community string, UI:N — без участия пользователя, S:C — выход за пределы компонента, A:H — высокий импакт на доступность; CWE-20 — недостаточная валидация ввода, CWE-703 — некорректная обработка исключений) позволяет аутентифицированному атакующему вызвать отказ в обслуживании крафтированным SNMP-запросом. А CVE-2022-45315 в MikroTik RouterOS до stable-версии 7.6 (CVSS 6.4 MEDIUM; AV:A/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H; CWE-125 — out-of-bounds read) позволяла выполнить произвольный код через крафтированный SNMP-пакет. Формально — высокая сложность (AC:H) и нужны высокие привилегии (PR:H) из смежной L2-сети (AV:A, а не AV:N). Но по данным CISA Vulnrichment, для этой уязвимости есть публичный PoC и эксплуатация автоматизируема (exploitation=poc, automatable=yes). На бумаге — средний риск, на практике — вполне рабочий вектор.
Перебор community string: как работает аутентификация в SNMP v1/v2c
SNMP (Simple Network Management Protocol) — протокол управления сетевыми устройствами: маршрутизаторами, коммутаторами, серверами, IoT. Версии v1 и v2c используют community string для аутентификации. По сути это текстовая строка-пароль, которая включается в каждый запрос в открытом виде. Если агент (SNMP-демон на устройстве) принимает строку — отвечает. Не принимает — молча отбрасывает пакет.
Протокол работает поверх UDP (транспорт без установления соединения), порт 161 для запросов, 162 для trap-уведомлений. Для перебора это принципиально: UDP не устанавливает соединение. Мы отправляем дейтаграмму и ждём ответа. При неверной строке ответа не будет — только таймаут. Как отмечает HackTricks, таймаут неоднозначен: он может означать неверную строку, потерю пакета, фильтрацию файрволом или неподдерживаемую версию протокола. Поэтому критичный перебор лучше запускать дважды.
По умолчанию SNMP не ограничивает количество попыток — ни rate limit, ни блокировки после N неудачных запросов. public и private — стандартные строки, которые часто остаются дефолтными: первая обычно даёт доступ на чтение (RO), вторая — на запись (RW). SNMPv3 использует другую модель безопасности (username + пароль + опционально шифрование), но v1/v2c до сих пор встречаются повсеместно.
Пишем SNMP-сканер на Python: три компонента скрипта
Скрипт состоит из трёх частей: формирование SNMP-пакета, отправка через UDP-сокет и параллельный перебор. Разберём каждую.
Предпосылки: Python 3.6+ (нужны f-strings и concurrent.futures). Дополнительных библиотек не требуется — работаем с модулем socket из стандартной библиотеки. Root-права для запуска не нужны: мы отправляем пакеты на порт 161 целевого хоста, а не слушаем привилегированные порты.
BER-кодировка SNMP-пакета вручную
SNMP-запрос — это ASN.1-структура в BER-кодировке (Binary Encoding Rules). Каждый элемент кодируется тройкой: тег-длина-значение (TLV). Для GET-запроса структура выглядит так: SEQUENCE { версия, community, GetRequest-PDU { request_id, error_status, error_index, VarBindList } }.
Запрашиваем OID 1.3.6.1.2.1.1.1.0 — sysDescr (текстовое описание системы). Если community string верна, агент ответит строкой вроде «Linux server 5.4.0-42-generic».
def build_snmp_get(community: str) -> bytes:
c = community.encode()
if len(c) > 127:
raise ValueError(f'Community string too long for single-byte BER length: {len(c)}')
# OID 1.3.6.1.2.1.1.1.0 (sysDescr) в BER: тег 0x06, длина 8
oid = b'\x06\x08\x2b\x06\x01\x02\x01\x01\x01\x00'
varbind = b'\x30' + bytes([len(oid) + 2]) + oid + b'\x05\x00'
vbl = b'\x30' + bytes([len(varbind)]) + varbind
req_id = b'\x02\x04\x00\x00\x00\x01'
pdu_body = req_id + b'\x02\x01\x00\x02\x01\x00' + vbl
pdu = b'\xa0' + bytes([len(pdu_body)]) + pdu_body
msg = b'\x02\x01\x01' + b'\x04' + bytes([len(c)]) + c + pdu
return b'\x30' + bytes([len(msg)]) + msg
Построчный разбор:
c = community.encode()— переводим строку в байты."public"станетb'public'(6 байт).oid— OID sysDescr в BER. Первые два компонента (1.3) кодируются формулой1*40 + 3 = 43 = 0x2b, остальные — по байту на компонент.varbind— SEQUENCE (тег0x30) из OID + NULL (0x05 0x00). NULL — заглушка значения в GET-запросе; агент подставит сюда реальные данные в ответе.vbl— VarBindList, SEQUENCE-обёртка для списка привязок.req_id— INTEGER со значением 1. Агент вернёт этот ID в ответе, чтобы мы могли сопоставить запрос и ответ.pdu_body— тело PDU: request_id + error_status(0) + error_index(0) + VarBindList. Нули означают «ошибок нет» — это запрос, не ответ.pdu— GetRequest-PDU с тегом0xa0(контекстно-зависимый конструктивный тег [0], означающий «это GET-запрос»).msg— версия протокола (0x02 0x01 0x01= INTEGER 1, то есть SNMPv2c; для SNMPv1 было бы0x00) + community string как OCTET STRING + PDU.- Финальная обёртка: SEQUENCE (
0x30) с длиной всего сообщения.
Ограничение: bytes([len(...)]) работает для длин до 127 (однобайтовая BER-длина). Community string длиннее 100 символов на практике не встречается, так что хватает. Если же длина community string или всего пакета превысит 127 байт, потребуется многобайтовая BER-длина (0x81 XX). Без неё bytes([len(...)]) сформирует невалидный BER-пакет молча — Python создаст байт, но BER интерпретирует его как начало многобайтовой длины.
UDP-сокеты Python: отправка и обработка таймаута
Почему «голый» сокет, а не библиотека pysnmp? socket.SOCK_DGRAM — прямой UDP без абстракций. Мы контролируем каждый байт. pysnmp оборачивает всё в высокоуровневый API и скрывает механику — а нам нужна именно она.
def try_community(target, community, timeout=2.0):
pkt = build_snmp_get(community)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.settimeout(timeout)
sock.sendto(pkt, (target, 161))
try:
sock.recvfrom(4096)
return True
except socket.timeout:
return False
Что здесь происходит:
socket.AF_INET— IPv4.socket.SOCK_DGRAM— UDP (для TCP было быSOCK_STREAM).settimeout(2.0)— если ответ не пришёл за 2 секунды,recvfrom()выброситsocket.timeout. Для HTB через VPN обычно хватает. На нестабильных каналах стоит поднять до 3–5.sendto()— отправляет пакет на порт 161 цели.recvfrom(4096)— ждём ответа. Любой полученный пакет означает, что community string принята: по данным HackTricks, SNMP-агенты молча отбрасывают запросы с неверной строкой. Сам факт ответа — надёжное подтверждение.with ... as sock— автоматически закрывает сокет при выходе из блока.
Автоматизация перебора SNMP: многопоточность через ThreadPoolExecutor
UDP-запрос — «отправил и жди». Пока один поток висит на двухсекундном таймауте неверной строки, другие спокойно отправляют свои пакеты. ThreadPoolExecutor из стандартной библиотеки позволяет запустить десятки проверок одновременно. Предпосылка: import socket и from concurrent.futures import ThreadPoolExecutor, as_completed в начале файла.
def brute_snmp(target, wordlist, threads=50):
with open(wordlist) as f:
words = [l.strip() for l in f if l.strip()]
found = []
with ThreadPoolExecutor(max_workers=threads) as pool:
futs = {pool.submit(try_community, target, w): w for w in words}
for f in as_completed(futs):
if f.result():
print(f'[+] {futs[f]}')
found.append(futs[f])
return found
Разбор ключевых моментов:
ThreadPoolExecutor(max_workers=50)— пул из 50 потоков. Каждый проверяет одну community string — 50 одновременных UDP-пакетов в сети.pool.submit(try_community, target, w)— ставит задачу в очередь. Словарьfutsсвязывает объект future с исходным словом.as_completed(futs)— итерирует по мере завершения, а не по порядку добавления. Первая валидная строка отобразится сразу, не дожидаясь таймаутов остальных.
При словаре в 1000 строк и 50 потоках с таймаутом 2 секунды перебор в худшем случае (все строки неверны) занимает около 40 секунд (1000 / 50 × 2). Нюанс: скрипт не прерывает оставшиеся проверки после первой находки — ThreadPoolExecutor продолжит все поставленные задачи. Чтобы остановить досрочно, добавьте pool.shutdown(wait=False, cancel_futures=True) после первого успеха (параметр cancel_futures появился в Python 3.9). onesixtyone на аналогичном списке из 1000 строк справится заметно быстрее благодаря асинхронным raw-сокетам на C.
Запуск: python3 snmp_brute.py. В конец файла добавьте стандартный if __name__ == '__main__' с вызовом brute_snmp(sys.argv[1], sys.argv[2]). Ожидаемый вывод при находке: [+] public или [+] s3cr3t_community.
Wordlist community strings: какие словари использовать для перебора
Словарь — самое важное после скрипта. Без правильного списка даже идеальный инструмент бесполезен.
SecLists (Daniel Miessler) — стандарт де-факто. Нужные файлы: Discovery/SNMP/common-snmp-community-strings.txt (базовый набор), Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt (расширенный, совместимый с onesixtyone) и Discovery/SNMP/snmp.txt (минимальный набор для быстрой проверки).
Nmap включает свой список nselib/data/snmpcommunities.lst, который использует скрипт snmp-brute. Путь в Kali: /usr/share/nmap/nselib/data/snmpcommunities.lst.
Metasploit хранит словарь в /usr/share/metasploit-framework/data/wordlists/snmp_default_pass.txt.
На практике стоит объединить источники и убить дубликаты: cat list1.txt list2.txt list3.txt | sort -u > combined.txt. Для HTB-машин иногда нужны кастомные строки: имя машины, вариации с цифрами, типичные для конкретного вендора значения. Добавляйте их в конец объединённого файла.
Сравнение: наш скрипт vs onesixtyone vs nmap snmp-brute
| Параметр | onesixtyone | nmap snmp-brute | Metasploit snmp_login | Наш скрипт |
|---|---|---|---|---|
| Язык | C | Lua (NSE) | Ruby | Python |
| Архитектура | Async raw sockets | Send + pcap sniff | Последовательный с потоками | ThreadPool + UDP |
| Относительная скорость | Очень высокая | Высокая | Средняя | Средняя |
| Кастомизация | Только wordlist | Аргументы скрипта | Модульные опции MSF | Полная (код ваш) |
| Постобработка | Нет | Nmap XML/grepable | Сохранение в БД MSF | Любая (JSON, pipe) |
| Доступность | Установка через apt/сборка | Часть nmap | Часть Metasploit | python3 + socket (stdlib) |
| SNMPv3 | Нет | Нет | Частично | Можно дописать |
Когда какой инструмент брать:
- onesixtyone — быстрый sweep по подсети (/24 и больше). Задача: найти хоть что-то за минимальное время.
- nmap snmp-brute — уже запустили nmap-сканирование и хотите добавить SNMP-перебор в тот же проход.
- Metasploit snmp_login — работаете в консоли MSF, хотите сохранить creds в базу и сразу проверить RW-доступ.
- Свой скрипт — нужна кастомная логика (автоматический snmpwalk при находке, фильтрация по версии протокола, запись в нужный формат) или готовых инструментов нет в окружении.
HTB walkthrough SNMP: от брутфорса до enumeration
Пошаговый сценарий для машины с открытым SNMP. Предпосылка: вы подключены к VPN площадки (HTB или TryHackMe), IP цели — 10.10.10.20, на вашей машине стоит Python 3.6+ и пакет snmp (для snmpwalk; в Kali он есть по умолчанию).
Шаг 1. Подтвердить, что SNMP открыт. Запустите nmap -sU -p 161 10.10.10.20. Флаг -sU — UDP-сканирование, -p 161 — только порт SNMP. Ожидаемый вывод: 161/udp open snmp. Если видите open|filtered — порт может быть за файрволом, но перебор попробовать стоит.
Шаг 2. Запустить перебор. Сохраните три функции (build_snmp_get, try_community, brute_snmp) и импорты в файл snmp_brute.py, добавьте блок __main__ с вызовом brute_snmp('10.10.10.20', 'wordlist.txt'). Запуск: python3 snmp_brute.py. Ожидаемый результат: строки [+] public или [+] <нестандартная_строка>. Если ничего не найдено — увеличьте таймаут до 5 секунд (аргумент timeout в try_community) или подключите более обширный словарь.
Шаг 3. Энумерация. Когда community string найдена, запускайте snmpwalk -v2c -c <строка> 10.10.10.20 .1 для полной выгрузки MIB-дерева. Флаг -v2c — версия протокола, -c — community string, .1 — начать обход с корня иерархии. Ожидаемый вывод: десятки или сотни строк с OID и их значениями.
Полезные OID для HTB-машин: 1.3.6.1.2.1.25.4.2.1.2 — список запущенных процессов, 1.3.6.1.2.1.25.1.6.0 — системные процессы (часто содержат пароли в аргументах), 1.3.6.1.2.1.1.5.0 — hostname, 1.3.6.1.4.1.77.1.2.25 — пользователи Windows (LAN Manager MIB; актуально для legacy Windows Server с установленным SNMP Service, который отсутствует по умолчанию в современных версиях). Утилита snmp-check 10.10.10.20 -c <строка> из Kali выводит те же данные в структурированном виде.
Шаг 4. Проверить уровень доступа. Если нашлись несколько строк — одна может быть RO (read-only), другая RW (read-write). Metasploit-модуль snmp_login автоматически определяет уровень доступа. В нашем скрипте можно добавить проверку: отправить SET-запрос с найденной строкой и посмотреть, вернёт ли агент ошибку notWritable (RO) или примет значение (RW).
По данным Rapid7, в выгрузках SNMP полезно грепать по ключевым словам: grep -i "trap" *.snmp для поиска дополнительных community string в trap-данных, grep -i "fail" *.snmp для обнаружения логов неудачных аутентификаций с потенциальными паролями.
Большинство материалов по SNMP-пентесту в рунете заканчиваются на «запустите patator» или «используйте snmpwalk с community public». На реальных проектах и CTF-машинах community string нестандартная, а готовый инструмент может не стоять в окружении. Скрипт на 40 строк решает обе проблемы: вы понимаете каждый байт и можете адаптировать логику под задачу.
Есть устойчивая иллюзия, что Python для пентеста — только обвязка над чужими библиотеками. Написание инструментов на уровне протокола — навык, который отделяет оператора от инженера. Оператор запускает onesixtyone -c list.txt target и ждёт. Инженер может объяснить, почему инструмент не нашёл строку (таймаут слишком низкий, версия протокола не совпала, пакет потерялся по UDP) — и пофиксить. В сетевом пентесте таких ситуаций с UDP-протоколами — SNMP, TFTP, DNS — половина рабочего дня. Если хотите выстроить эту базу системно, а не собирать по случайным статьям — на IB Basics на codeby.school фундамент закрывается за пару месяцев, без академического тона.
Эту тему и смежные навыки разбирают на практике в курсе «Python для пентестера» Codeby Academy.
