Написание NSE скриптов Nmap: ловим открытый Redis без пароля за один скан

На внутренних пентестах Redis без аутентификации попадается с пугающей регулярностью: порт 6379 открыт, команда AUTH не настроена, все данные доступны любому, кто добрался до сетевого сегмента. Штатный скрипт redis-info из поставки Nmap покажет версию сервера и количество ключей — но не ответит на конкретный вопрос: «можно ли прямо сейчас читать и писать данные без пароля?». Для таких точечных проверок и существует написание NSE скриптов Nmap: ты создаёшь ровно ту проверку, которая нужна под задачу, и запускаешь её одной командой на всю подсеть.
Разберём анатомию NSE-скрипта, напишем с нуля рабочий Lua-скрипт для обнаружения незащищённого Redis, протестируем его на локальном стенде и разберём ошибки, на которых спотыкается каждый второй новичок.
Зачем нужен кастомный скрипт Nmap для сканирования Redis без пароля
Nmap поставляется с 600+ скриптами в каталоге /usr/share/nmap/scripts/. Для Redis есть два штатных: redis-info вытаскивает метаданные сервера, redis-brute перебирает пароли. Ни один не делает простейшую проверку: отвечает ли Redis на команду PING без аутентификации. Если в ответ приходит +PONG — сервер открыт полностью.
Прежде чем писать скрипт, стоит понять, почему незащищённый Redis — не просто «мисконфигурация для отчёта», а реальная дыра. Атакующий с доступом к открытому Redis может:
- Читать данные напрямую. Сессионные токены, кешированные пароли, персональные данные — всё лежит в ключах Redis в открытом виде.
- Получить RCE через
CONFIG SET. Техника известна с середины 2010-х: атакующий меняет директорию и имя файла дампа Redis, записывает SSH-ключ командойSETи получает shell на сервере. Начиная с Redis 3.2 (2016) по умолчанию включён protected mode — он активен, если одновременно не заданbindна конкретный адрес и не установленrequirepass. В этом случае Redis блокирует удалённые подключения. Сценарий актуален, когда protected mode отключён или Redis привязан к внешнему интерфейсу. - Двигаться по сети. Redis обычно стоит во внутреннем сегменте с повышенным уровнем доверия — скомпрометированный инстанс становится опорной точкой для латерального движения.
В терминах MITRE ATT&CK (это открытая база тактик и техник атак; T-коды вроде T1046 — её идентификаторы) картина выглядит так: наш скрипт выполняет Network Service Discovery (T1046, тактика Discovery) — обнаруживает сервис и его конфигурацию. Обнаруженный открытый Redis при доступности из интернета соответствует Exploit Public-Facing Application (T1190, тактика Initial Access). Во внутренней сети точного соответствия в ATT&CK нет: T1133 (External Remote Services) описывает VPN/RDP, а T1210 (Exploitation of Remote Services) предполагает эксплуатацию программных уязвимостей, а не мисконфигурации. Чтение кешированных учётных данных из ключей попадает под Credentials In Files (T1552.001, тактика Credential Access). Одна мисконфигурация — три техники в цепочке атаки.
Кастомный скрипт Nmap закрывает задачу за один проход: сканируем подсеть, скрипт подключается к каждому открытому порту 6379 и проверяет, нужен ли пароль. На выходе — список проблемных хостов, готовый для отчёта.
Nmap Scripting Engine — руководство по структуре NSE-скрипта
NSE (Nmap Scripting Engine) — скриптовый движок Nmap. Он использует язык Lua — компактный встраиваемый язык, который часто встречается в игровых движках и сетевых инструментах. Если ты раньше не писал на Lua — не страшно: синтаксис минимальный, для NSE-скриптов хватит переменных, функций и условных конструкций.
Каждый NSE-скрипт состоит из трёх обязательных блоков: заголовок, правило, действие. Согласно официальной документации Nmap (Chapter 9, NSE Script Format) именно эту структуру движок ожидает при загрузке скрипта.
Заголовок скрипта: описание и категории
Заголовок — метаинформация для справки и фильтрации. Обязательные поля:
description— что делает скрипт. Выводится командойnmap --script-help=имя_скрипта. По конвенции пишется на английском.author— автор (строка с именем).license— лицензия. Стандартная формулировка:"Same as Nmap--See https://nmap.org/book/man-legal.html".categories— массив категорий, определяющий характер скрипта. Nmap определяет 14 стандартных категорий:auth,broadcast,brute,default,discovery,dos,exploit,external,fuzzer,intrusive,malware,safe,version,vuln. Формально можно указать произвольную строку, но для фильтрации через--script=categoryиспользуются именно эти значения. Для нашей проверки Redis подойдутdiscovery(обнаруживаем конфигурацию сервиса) иsafe(не модифицируем данные — только отправляемPING).
Также в заголовке размещают документацию NSEDoc: тег @output показывает пример вывода, @usage — пример командной строки. Для работы это необязательно, но критически важно для повторного использования. Коллега, получивший твой скрипт, должен понять, что он делает, без чтения кода.
Правило запуска: portrule, hostrule и другие
Правило (rule) — функция, которая возвращает true или false. Если true — Nmap выполнит основной код для этого хоста или порта. Если false — пропустит молча.
NSE поддерживает четыре типа правил, каждый срабатывает на своей фазе сканирования:
prerule()— до сканирования хостов. Для discovery-скриптов, которые сами ищут цели в сети.hostrule(host)— после сканирования хоста. Получает объектhostс IP, информацией об ОС и другими данными.portrule(host, port)— после сканирования порта. Получает оба объекта:hostиport(номер, протокол, состояние, версия сервиса). Самый частый тип для проверки конкретных сервисов.postrule()— после завершения всего сканирования. Удобен для агрегации и финальных отчётов.
Для проверки Redis нужен portrule — мы запускаем скрипт только на открытых TCP-портах, где предполагается Redis.
Чтобы не писать проверку руками каждый раз, NSE даёт библиотеку shortport с готовыми хелперами. Вызов shortport.port_or_service(6379, "redis") вернёт true, если порт 6379 открыт ИЛИ если Nmap определил сервис как redis на любом порту. Это удобнее жёсткой привязки к номеру — Redis иногда поднимают на нестандартных портах, и shortport поймает оба случая.
Для понимания механики полезно увидеть ручную реализацию того же правила. Portrule, привязанный строго к порту 6379: проверяем три условия — протокол TCP, порт открыт (state == "open"), номер 6379. Все три должны быть true, иначе скрипт не запустится. В Lua это записывается как return port.protocol == "tcp" and port.state == "open" and port.number == 6379. Обрати внимание на двойное == — это оператор сравнения, а не присваивание. Путаница с одинарным = — классическая ошибка, о которой поговорим ниже.
Действие: сокеты и сетевое взаимодействие
Функция action(host, port) — основная логика скрипта. Выполняется только когда правило вернуло true. Внутри action доступны:
Сокеты через nmap.new_socket() — создают TCP- или UDP-соединение. Сокет (socket) — программный интерфейс для сетевого взаимодействия: ты подключаешься к порту, отправляешь данные и читаешь ответ. Методы: connect(host, port) для подключения, send(data) для отправки, receive_lines(n) для чтения как минимум n строк ответа (может вернуть больше, если они уже в буфере), close() для закрытия.
NSE-библиотеки для работы с конкретными протоколами (HTTP, SSH, SMB и т.д.) — для Redis специализированной библиотеки нет, поэтому работаем напрямую через сокет.
Возврат результата: если action возвращает строку — Nmap покажет её в выводе рядом с портом. Если возвращает nil — ничего не выведет (тихий пропуск). По конвенции скрипт молчит, когда проверка не нашла ничего интересного.
Lua скрипты для Nmap: пишем проверку Redis на уязвимости шаг за шагом
Собираем всё вместе. Задача: написать NSE-скрипт, который подключается к Redis, отправляет PING и по ответу определяет, требуется ли аутентификация.
Протокол Redis текстовый и предсказуемый: отправляем PING\r\n, получаем +PONG\r\n если сервер открыт, или -NOAUTH Authentication required.\r\n если нужен пароль. Это всё, что нужно знать о протоколе для нашей проверки.
Шаг 1 — файл, заголовок и portrule
Создай файл redis-noauth.nse. Его можно положить в стандартный каталог /usr/share/nmap/scripts/ (нужны root-права) или в любую директорию — при запуске укажешь путь вручную.
Заголовок заполняем стандартно: description — краткое описание на английском, categories — массив {"discovery", "safe"}, author и license — строки. В первой строке скрипта подключаем shortport: local shortport = require "shortport". Ключевое слово local в Lua означает локальную переменную — она видна только в текущей области видимости.
Portrule задаём одной строкой: portrule = shortport.port_or_service(6379, "redis"). Готово — Nmap будет запускать скрипт на каждом хосте, где открыт порт 6379 или обнаружен сервис redis.
Шаг 2 — action с отправкой PING
Логика action по шагам:
- Создаём сокет вызовом
nmap.new_socket(). - Подключаемся к целевому хосту через
sock:connect(host, port). Метод возвращает два значения:status(true/false) иerr(текст ошибки). Если подключение не удалось — возвращаемnil. - Отправляем команду Redis:
sock:send("PING\r\n"). Символы\r\n— возврат каретки и новая строка, так требует протокол. - Читаем ответ:
sock:receive_lines(1)— одна строка. Тоже возвращаетstatusиresponse. - Закрываем сокет:
sock:close(). - Анализируем ответ через
string.match(). Если содержит+PONG— Redis открыт. ЕслиNOAUTH— защищён паролем.
Полный код скрипта
Вот весь скрипт целиком — можно скопировать, сохранить и использовать:
local shortport = require "shortport"
description = [[Checks if a Redis server requires authentication
by sending a PING command and analyzing the response.]]
author = "Your Name"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.port_or_service(6379, "redis")
action = function(host, port)
local sock = nmap.new_socket()
sock:set_timeout(5000)
local status, err = sock:connect(host, port)
if not status then return nil end
local sendStatus, sendErr = sock:send("PING\r\n")
if not sendStatus then sock:close() return nil end
local status, response = sock:receive_lines(1)
sock:close()
if not status then return "No response received" end
if string.match(response, "%+PONG") then
return "NO AUTH REQUIRED - server is open"
elseif string.match(response, "NOAUTH") then
return "Authentication required (protected)"
end
return "Unexpected response: " .. response
end
Пара комментариев к коду. sock:set_timeout(5000) задаёт таймаут в 5000 миллисекунд (5 секунд) — без неё скрипт может зависнуть на медленных или отфильтрованных хостах. Паттерн "%+PONG" в string.match использует %+ для экранирования символа +. В Lua-паттернах (это не регулярные выражения, а отдельный формат сопоставления строк) знак + означает «один или более повторений предыдущего символа», поэтому литеральный + нужно экранировать через %.
Обнаружение незащищенного Redis: тестируем и отлаживаем скрипт
Скрипт написан — теперь убедимся, что он работает корректно, прежде чем пускать его на живую инфраструктуру.
Подготовка тестового стенда
Что понадобится: машина с Nmap (в Kali Linux он установлен по умолчанию) и Docker для быстрого запуска Redis.
Поднимаем два контейнера — один без пароля, другой с паролем. Redis без аутентификации: docker run -d -p 6379:6379 --name redis-open redis:latest. Redis с паролем: docker run -d -p 6380:6379 --name redis-auth redis:latest --requirepass testpass123 (аргументы --requirepass testpass123 передаются не Docker, а самому redis-server — entrypoint официального образа Redis). На порту 6379 висит открытый Redis, на 6380 — защищённый. Идеальные условия для проверки обоих сценариев.
Убедись, что контейнеры запустились: docker ps должен показать оба. Если redis-cli -p 6379 PING возвращает PONG — всё работает.
Запуск скрипта и ожидаемый вывод
Если скрипт лежит в /usr/share/nmap/scripts/, обнови базу командой nmap --script-updatedb — Nmap проиндексирует новый файл. Если скрипт в текущей директории — укажи путь напрямую через --script=./redis-noauth.nse.
Запуск: nmap -p 6379,6380 --script=redis-noauth 127.0.0.1. Ожидаемый вывод:
PORT STATE SERVICE
6379/tcp open redis
|_ redis-noauth: NO AUTH REQUIRED - server is open
6380/tcp open redis
|_ redis-noauth: Authentication required (protected)
Видишь NO AUTH REQUIRED для 6379 и Authentication required для 6380 — скрипт работает. Пустота вместо результата — portrule вернул false, скрипт вообще не запустился. Ошибка — проблема в action.
Отладка через —script-trace
Когда скрипт молчит или ведёт себя неожиданно, главный инструмент — флаг --script-trace. Он выводит все данные, которые скрипт отправляет и получает по сети:
nmap -p 6379 --script=redis-noauth --script-trace 127.0.0.1
В выводе появятся строки: NSE: TCP ... > 127.0.0.1:6379 | CONNECT (подключение), затем отправленные данные в hex-формате (50 49 4e 47 0d 0a — это PING\r\n в ASCII) и полученный ответ (2b 50 4f 4e 47 0d 0a — это +PONG\r\n). Данные не уходят — проблема в connect или send. Ответ не тот, что ожидал — проблема в string.match.
Дополнительный флаг -d (debug) покажет, запускается ли скрипт вообще и почему portrule вернул конкретное значение. Команда nmap -p 6379 --script=redis-noauth -d 127.0.0.1 добавит в вывод строки вида NSE: Script redis-noauth ... matched или did not match.
Типичные ошибки при написании NSE скриптов для пентеста
Коллекция граблей, по которым я ходил сам и наблюдал у других. Знание этих ошибок экономит часы отладки.
Одинарное = вместо двойного == в portrule. Написать port.number = 6379 вместо port.number == 6379 — ошибка, которая не вызывает синтаксического сбоя в Lua. Одинарное = — присваивание, и выражение вернёт truthy-значение. Результат: скрипт запустится на КАЖДОМ открытом порту, а не только на 6379. При сканировании /24-подсети это сотни ложных срабатываний. Используй двойное == для сравнения — всегда.
Забытый return в action. Если функция не возвращает строку — Nmap ничего не выведет. Ты будешь думать, что скрипт не запустился, хотя он отработал и промолчал. Каждая ветка логики (if/elseif/else) должна заканчиваться return "текст" или явным return nil.
Незакрытый сокет. Без sock:close() сокет остаётся открытым. На одном хосте это незаметно, но при сканировании подсети из 254 хостов Nmap исчерпает лимит файловых дескрипторов и начнёт пропускать цели. Для надёжности используй nmap.new_try(): создаёшь функцию-перехватчик (catch), которая закрывает сокет при любой ошибке, и оборачиваешь сетевые вызовы в try(). Если любой из них вернёт ошибку — catch отработает автоматически:
local catch = function() sock:close() end
local try = nmap.new_try(catch)
try(sock:connect(host, port))
try(sock:send("PING\r\n"))
local response = try(sock:receive_lines(1))
Этот паттерн описан в официальном Script Writing Tutorial на nmap.org и используется в большинстве штатных скриптов.
Lua-паттерны — не регулярные выражения. Lua использует собственный формат сопоставления строк, отличный от PCRE. Спецсимволы экранируются через %: точка — %., плюс — %+, скобки — %(. Если написать string.match(response, "+PONG") без экранирования, + будет означать «одно или более повторений предыдущего символа» (в данном случае — пустоты перед P). Работать может случайно, но привычка экранировать спецсимволы спасёт от трудноуловимых багов в более сложных скриптах.
Отсутствие require для библиотек. Если используешь shortport — подключи его в начале скрипта: local shortport = require "shortport". Без require Lua не найдёт библиотеку и скрипт упадёт при загрузке. Nmap при этом не всегда явно сообщает причину — просто пропускает скрипт.
Отсутствие таймаута. Без sock:set_timeout() скрипт будет ждать ответа бесконечно, если хост принимает соединение, но не отвечает (типичная ситуация с фаерволом, который дропает пакеты после handshake). Ставь 3000–5000 миллисекунд — хватит для любого живого сервиса.
Автоматизация сканирования портов Nmap: от одного скрипта к процессу
Один скрипт для Redis — отправная точка. В реальной работе аналогичных проверок десятки: MongoDB без аутентификации (порт 27017, отправляем isMaster и проверяем ответ), Memcached с открытым доступом (порт 11211, отправляем stats\r\n), Elasticsearch без авторизации (порт 9200, GET-запрос к /). Принцип тот же: подключиться, отправить минимальный probe-запрос, проверить ответ.
Аудит открытых сервисов: Redis, MongoDB, Memcached
Паттерн создания проверочного скрипта повторяется:
portruleсshortport.port_or_service()для нужного порта/сервиса.- В
action— сокет, probe-запрос, парсинг ответа. - Возврат человекочитаемого результата или
nilпри отсутствии проблемы.
Собственные скрипты стоит хранить в отдельной директории (например, ~/nse-custom/), а не в системном каталоге Nmap. Два преимущества: скрипты не потеряются при обновлении Nmap и ты чётко разделяешь штатные и кастомные проверки.
Для масштабного аудита удобно объединять скрипты общей категорией. Добавь во все свои скрипты проверки noauth-доступа категорию "noauth-check", обнови базу через nmap --script-updatedb и запускай одной командой: nmap --script="noauth-check" -p 6379,11211,27017,9200 10.0.0.0/24. Один проход — полная картина незащищённых сервисов в подсети.
С точки зрения MITRE ATT&CK наши действия попадают в Active Scanning (T1595, тактика Reconnaissance) при внешнем аудите и Network Service Discovery (T1046, тактика Discovery) при работе внутри периметра. Для отчёта это важно: заказчик видит, какие конкретно техники атакующего ты воспроизвёл и какие мисконфигурации обнаружил.
Что проверить перед запуском на реальной инфраструктуре
Чек-лист перед использованием скрипта на продакшен-сети (с письменного разрешения, разумеется):
| Критерий | На что смотреть | Почему это важно |
|---|---|---|
| Категория | safe для read-only, intrusive если скрипт пишет данные |
Коллега запустит --script=safe и не ожидает побочных эффектов |
| Таймаут | sock:set_timeout(5000) сразу после создания сокета |
Без таймаута скрипт зависает на filtered-портах |
| Обработка ошибок | Проверка status после каждого connect/send/receive |
Один сбой не должен ронять весь скан |
| Чистота вывода | nil для «всё в порядке», строка только для проблем |
На 500 хостах выводить «Auth OK» бессмысленно |
| Документация | @output и @usage в заголовке |
Через месяц ты сам забудешь, что скрипт делает |
Для изучения лучших практик стоит читать код штатных скриптов. Файлы лежат в /usr/share/nmap/scripts/, каждый — полноценный пример: ftp-anon.nse проверяет анонимный доступ к FTP по тому же принципу (подключение, отправка команды, анализ ответа), http-robots.txt.nse разбирает текстовый ответ веб-сервера. Команда grep -l "nmap.new_socket" /usr/share/nmap/scripts/*.nse покажет все скрипты, работающие через сокеты — готовая библиотека референсов.
Написание NSE скриптов Nmap — навык, который окупается с первого применения. Я начинал с того же redis-noauth и за полгода собрал библиотеку из двух десятков проверок под задачи, которые штатные скрипты не покрывают. Кто-то скажет: проще запустить redis-cli руками — и для одного хоста это правда. Но когда в скоупе 500 хостов и три подсети, ручная проверка превращается в рутину, а кастомный скрипт делает ту же работу за секунды. Главный эффект, по моему опыту, не в скорости, а в воспроизводимости: скрипт с внятным description и правильными категориями — инструмент, который коллега использует без твоего участия и получит идентичный результат. Большинство пентестеров пишут одноразовые bash-однострочники, которые умирают после проекта. NSE-скрипт живёт годами — он встроен в среду, его запускают через тот же Nmap, к которому привыкла вся команда. Если хочешь не просто повторить шаги из статьи, а системно пройти базу от сетевых основ до первых реальных задач в ИБ — на codeby.school есть трек IB Basics, где подобные навыки закладывают с фундамента.
Эту тему и смежные навыки разбирают на практике в курсе «Специалист по тестированию на проникновение» Codeby Academy.
