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

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

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

На внутренних пентестах 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 по шагам:

  1. Создаём сокет вызовом nmap.new_socket().
  2. Подключаемся к целевому хосту через sock:connect(host, port). Метод возвращает два значения: status (true/false) и err (текст ошибки). Если подключение не удалось — возвращаем nil.
  3. Отправляем команду Redis: sock:send("PING\r\n"). Символы \r\n — возврат каретки и новая строка, так требует протокол.
  4. Читаем ответ: sock:receive_lines(1) — одна строка. Тоже возвращает status и response.
  5. Закрываем сокет: sock:close().
  6. Анализируем ответ через 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

Паттерн создания проверочного скрипта повторяется:

  1. portrule с shortport.port_or_service() для нужного порта/сервиса.
  2. В action — сокет, probe-запрос, парсинг ответа.
  3. Возврат человекочитаемого результата или 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.