РАЗБОР
На проверке
Написание NSE скриптов для Nmap: от основ к бою
[ обложка статьи ]
Режим чтения
На CTF-турнире убил полтора часа на порт 31337 -
nmap -sV выдал tcpwrapped, штатные скрипты промолчали. Сервис оказался кастомным: текстовый протокол, баннер с номером версии, а за ним - command injection через непроверяемый ввод. Десять строк Lua, один запуск - баннер извлечён, версия распарсена, вектор найден. С тех пор написание NSE скриптов для Nmap стало такой же частью арсенала, как сам сканер. И на CTF, и на реальных проектах. Дальше - разборка структуры скрипта, сборка с нуля, отладка и встраивание в рабочий процесс.NSE-скрипт изнутри: Lua для Nmap и три обязательных блока
Nmap Scripting Engine работает на Lua - компактном встраиваемом языке. Для NSE хватит переменных, функций, условий и строковых паттернов - этим покрывается процентов 90 задач. Каждый скрипт, который загружает движок, обязан содержать три блока: заголовок, правило, действие. Именно эту структуру парсер ожидает при загрузке (Chapter 9, Script Format в официальной документации Nmap). Подробнее - в нашем обзоре разработка red team инструментов.Заголовок (Head) - метаданные для справки и фильтрации. Поле
description объясняет, что скрипт делает, и выводится через nmap --script-help=имя. Поле categories определяет группы при запуске через --script=category. Nmap NSE категории скриптов: auth, broadcast, brute, default, discovery, dos, exploit, external, fuzzer, intrusive, malware, safe, version, vuln - четырнадцать штатных. Формально можно вписать произвольную строку, но фильтрация работает именно по этим значениям. На CTF обычно хватает discovery и safe - скрипт находит информацию, не трогая данные на цели. Плюс author и license - без них тоже никуда.Правило (Rule) - функция, возвращающая
true или false. Движок поддерживает четыре типа правил, каждый срабатывает на своей фазе: prerule() - до сканирования хостов, для discovery-скриптов, которые сами ищут цели; hostrule(host) - после сканирования хоста, получает объект с IP и ОС; portrule(host, port) - после сканирования порта, самый частый тип; postrule() - после завершения всего сканирования, для агрегации.Для проверки конкретного сервиса нужен
portrule. Чтобы не городить проверки руками, nselib библиотеки дают shortport с набором хелперов: port_or_service, portnumber, service, ssl, version_port_or_service. Вызов shortport.port_or_service(9090, "custom") вернёт true, если порт 9090 открыт или Nmap определил сервис как custom на любом порту. Ручной вариант - return port.protocol == "tcp" and port.state == "open" and port.number == 9090. Обратите внимание: двойное == - сравнение, одинарное = - присваивание. Путаница между ними - классические грабли в Lua, на которые наступает каждый второй.Действие (Action) - функция
action(host, port) с основной логикой. Запускается, только когда правило вернуло true. NSE API функции внутри action: сокеты через nmap.new_socket() с методами connect, send, receive_lines, close; вспомогательные nmap.get_port_state и nmap.new_try для обработки ошибок; nselib-библиотеки для HTTP, SMB, SSH и прочих протоколов. Если action возвращает строку - Nmap покажет её рядом с портом в выводе. Возврат nil - тихий пропуск.Кастомные скрипты Nmap: фингерпринтинг нестандартного сервиса
Типичная CTF-ситуация: нестандартный порт, сервис отдаёт текстовый баннер при подключении, штатныйbanner.nse его не парсит. Задача - захватить баннер, вытащить версию, вывести в отчёт. Фингерпринтинг сервисов Nmap через кастомный скрипт решает это за один проход.
Код:
description = [[Grabs and parses banner from custom service]]
author = "yourname"; license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = require("shortport").port_or_service({9090, 31337}, nil, "tcp")
action = function(host, port)
local sock = nmap.new_socket()
if not sock:connect(host.ip, port.number) then return nil end
local _, banner = sock:receive_lines(1)
sock:close()
return banner and (banner:match("([Vv]ersion%s+[%d%.]+)") or banner) or nil
end
portrule через shortport.port_or_service. Массив портов {9090, 31337}, nil вместо имени сервиса (принимаем любое), протокол TCP. Скрипт запустится на любом из указанных портов в состоянии open.Строки 6–8 - создаём сокет, подключаемся к
host.ip по port.number, читаем первую строку ответа. receive_lines(1) возвращает два значения: статус (в _, игнорируем) и данные. Не подключились - возвращаем nil, скрипт молчит.Строка 10 - паттерн-матчинг Lua. Выражение
banner:match("([Vv]ersion%s+[%d%.]+)") ищет подстроку вида Version 2.1 или version 3.0.1 и возвращает найденное. %s+ - один или более пробелов, [%d%.]+ - цифры и точки. Паттерн не сработал - возвращаем сырой баннер целиком. Баннера нет совсем - nil.Файл сохраняем как
custom-banner.nse куда угодно. Для запуска указываем путь явно - тащить в /usr/share/nmap/scripts/ не обязательно.Отладка NSE скриптов: --script-trace и типичные ошибки
Забытый
\r\n при отправке данных. Текстовые протоколы - Redis, SMTP, кастомные CTF-сервисы - ждут CRLF. Шлёшь PING\n вместо PING\r\n - получаешь таймаут или мусор в ответе. Всегда sock:send("COMMAND\r\n"). Без вариантов.Утечка сокетов. Если
receive_lines падает с ошибкой, а close стоит только в конце - сокет остаётся открытым. При сканировании целой подсети файловые дескрипторы кончатся за минуты. Решение - конструкция nmap.new_try(catch) из NSE API: вызывает функцию-обработчик при любой сетевой ошибке. Рабочий пример есть в identd-скрипте из официального туториала (Chapter 9).Скрипт не находит nselib. Если файл лежит вне стандартного каталога,
require("shortport") падает. Два варианта: флаг --datadir с путём к каталогу Nmap или копирование скрипта в /usr/share/nmap/scripts/ с последующим nmap --script-updatedb.Боевые сценарии Nmap скриптов: от CTF до автоматизации разведки
Один скрипт - тактический инструмент. Настоящий выхлоп начинается, когда встраиваешь их в процесс автоматизации разведки Nmap.На CTF (CTFtime, HTB Seasonal, TryHackMe) я держу набор из 3–4 кастомных скриптов: баннер-граббер для нестандартных портов, проверка аутентификации на типичных сервисах по аналогии с Redis-скриптом (отправка команды, анализ ответа - подход разбирается в материале Codeby Academy) и
postrule-агрегатор, собирающий результаты в JSON. Весь набор запускается одной командой: nmap --script=./ctf-scripts/ -p- target. Каждый новый челлендж с нестандартным сервисом - повод дописать скрипт в коллекцию.На реальных проектах кастомные скрипты закрывают задачи за пределами штатных 600+ скриптов из поставки. Проприетарный протокол на промышленном контроллере, OT-устройство с нестандартным handshake, внутренний API на высоком порту - всё это Network Service Discovery (T1046, тактика Discovery по MITRE ATT&CK). Обнаруженный открытый сервис без аутентификации на внешнем периметре - Exploit Public-Facing Application (T1190, Initial Access). Без кастомного скрипта такие сервисы остаются невидимыми для стандартного сканирования. Просто слепое пятно.
Связка NSE с Metasploit и Python
NSE находит - Metasploit эксплуатирует. XML-вывод Nmap (-oX) импортируется командой db_import в Metasploit Framework. Кастомный скрипт, обнаруживший открытый сервис без аутентификации, передаёт хост на следующий шаг: проверка RCE, lateral movement. Для автоматизации в Python берут [URL='https://pypi.org/project/python-nmap/']python-nmap[/URL] - запускает сканирование и парсит XML, включая вывод кастомных NSE скриптов. Связка: NSE фингерпринтит, Python агрегирует по подсетям, Metasploit проверяет exploit.Nmap NSE категории скриптов помогают управлять агрессивностью: скрипты для пассивного сбора помечаются
safe и discovery, а скрипты с активным взаимодействием - intrusive или exploit. На проекте с жёстким scope это решает, какие проверки запускаются по умолчанию, а какие - только по явному согласованию.Большинство CTF-игроков и начинающих пентестеров останавливаются на
nmap -sC -sV и сразу прыгают в эксплуатацию. Первый год я делал так же - пока не упёрся в машину на HTB, где штатная разведка не дала ни одного вектора. Три часа разбора трафика в Wireshark, потом десять строк Lua - и нестандартный порт заговорил.С тех пор каждый assessment начинается с вопроса: есть ли здесь что-то, чего стандартные скрипты не видят?
Lua скриптинг для пентеста - навык, который разделяет «решил две задачи и застрял» от стабильного финиша в топе. На OSCP фаза enumeration - больше половины экзамена, и кастомный скрипт, проверяющий конфигурацию сервиса на всех хостах за секунды, экономит десятки минут. Которых на экзамене не бывает в запасе.
Из того, что наблюдаю на реальных проектах: команды месяцами шлифуют privilege escalation, но пропускают разведку - а именно разведка даёт начальный вектор. Без вектора нет точки входа. NSE закрывает этот зазор: любой вопрос к любому сервису на любом порту с машиночитаемым ответом за один проход сканирования.
Возьмите скрипт из этой статьи, поменяйте порт на свой - и прогоните по ближайшему CTF-таргету. Если хочется отработать связку NSE + эксплуатация на живых лабах - на HackerLab есть машины, где без кастомного скрипта дальше первого порта не уедешь.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Продолжить чтение
Следующий разбор
MSSP Россия 2026: модели аутсорсинга ИБ для NGFW
Комментарии
0