#12 Gray Hat C#. Руководство для хакера по созданию и автоматизации инструментов безопасности. Написание фаззера JSON.
Здравствуйте, дорогие друзья.
Как пентестер или инженер по безопасности, Вы, скорее всего, столкнетесь с веб-сервисами, которые принимают в качестве входных данных данные, сериализованные в виде нотации объектов JavaScript (JSON), в той или иной форме. Чтобы помочь Вам научиться фаззить HTTP-запросы JSON, я написал небольшое веб-приложение под названием CsharpVulnJson, которое принимает JSON и использует содержащуюся в нем информацию для сохранения и поиска данных, связанных с пользователем. Была создана небольшая виртуальная машина, позволяющая веб-сервису работать «из коробки»; она доступна на веб-сайте VulnHub (http://www.vulnhub.com/).
Настройка уязвимого устройства
CsharpVulnJson поставляется в виде файла OVA, полностью автономного архива виртуальной машины, который Вы можете просто импортировать в выбранный вами пакет виртуализации. В большинстве случаев двойной щелчок по файлу OVA должен вызвать программное обеспечение виртуализации для автоматического импорта устройства.
Перехват уязвимого запроса JSON
После запуска CsharpVulnJson укажите Firefox на порт 80 на виртуальной машине, и Вы должны увидеть интерфейс управления пользователями, подобный показанному на рисунке ниже. Мы сосредоточимся на создании пользователей с помощью кнопки «Создать пользователя» и HTTP-запроса, который эта кнопка выполняет при создании пользователя.
Предполагая, что Firefox по-прежнему настроен для прохождения через Burp Suite в качестве HTTP-прокси, заполните поля «Создать пользователя» и нажмите «Создать пользователя», чтобы получить HTTP-запрос с информацией о пользователе внутри хеша JSON, в панели запросов Burp Suite, как в листинге ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
POST /Vulnerable.ashx HTTP/1.1 Host: 192.168.1.56 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:26.0) Gecko/20100101 Firefox/26.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: application/json; charset=UTF-8 Referer: http://192.168.1.56/ Content-Length: 190 Cookie: ASP.NET_SessionId=5D14CBC0D339F3F054674D8B Connection: keep-alive Pragma: no-cache Cache-Control: no-cache {"username":"whatthebobby","password":"propane1","age":42,"line1":"123 Main St", "line2":"","city":"Arlen","state":"TX","zip":78727,"first":"Hank","middle":"","last":"Hill", "method":"create"} |
Теперь щелкните правой кнопкой мыши на панели запроса и выберите «Копировать в файл». Когда Вас спросят, где сохранить HTTP-запрос на Вашем компьютере, сделайте свой выбор и запишите, где был сохранен запрос, потому что Вам нужно будет передать путь к фаззеру.
Создание фаззера JSON
Чтобы фаззить этот HTTP-запрос, нам нужно отделить JSON от остальной части запроса. Затем нам нужно перебрать каждую пару ключ/значение в JSON и изменить значение, чтобы попытаться вызвать любые ошибки SQL на веб-сервере.
Чтение файла запроса
Чтобы создать фаззер HTTP-запроса JSON, мы начинаем с заведомо исправного HTTP-запроса (запрос на создание пользователя). Используя ранее сохраненный HTTP-запрос, мы можем прочитать его и начать процесс фаззинга, как показано в листинге ниже.
Первое, что мы делаем, это сохраняем переданные фаззеру параметры first [1] и второй [2], в двух переменных (url и requestFile соответственно). Мы также объявляем массив строк, которому будут присвоены данные в нашем HTTP-запросе после чтения запроса из файловой системы.
В контексте оператора using мы открываем наш файл запроса для чтения с помощью File.OpenRead() [4] и передаем поток файлов, возвращаемый конструктору StreamReader [3]. Создав экземпляр нового класса StreamReader, мы можем прочитать все данные в файле с помощью метода ReadToEnd() [5]. Мы также разделяем данные в файле запроса с помощью метода Split() [6], передавая методу символ новой строки в качестве символа для разделения запроса. Протокол HTTP требует, чтобы символы новой строки (в частности, возврат каретки и перевод строки) использовались для отделения заголовков от данных, отправляемых в запросе. Массив строк, возвращаемый функцией Split(), присваивается переменной запроса, которую мы объявили ранее.
Прочитав и разделив файл запроса, мы можем получить данные JSON, которые нам нужны для фаззинга, и начать перебирать пары ключ/значение JSON, чтобы найти векторы внедрения SQL. Нам нужен JSON — это последняя строка HTTP-запроса, которая является последним элементом в массиве запросов. Поскольку 0 — это первый элемент массива, мы вычитаем 1 из длины массива запросов, используем полученное целое число, чтобы получить последний элемент в массиве запросов, и присваиваем значение строке json [7].
Как только мы отделим JSON от HTTP-запроса, мы сможем проанализировать строку json и создать JObject, который мы можем программно перебирать, при использовании JObject.Parse() [8]. Класс JObject доступен в библиотеке Json.NET, которую можно бесплатно получить через менеджер пакетов NuGet или по адресу http://www.newtonsoft.com/json/.
После создания нового JObject мы печатаем строку состояния, чтобы сообщить пользователю, что мы обрабатываем POST-запросы к данному URL-адресу. Наконец, мы передаем JObject и URL-адрес для выполнения HTTP-запросов POST методу IterateAndFuzz() [9] для обработки JSON и фаззинга веб-приложения.
Перебор ключей и значений JSON
Теперь мы можем начать перебирать каждую пару ключ/значение JSON и настроить каждую пару для проверки возможного вектора внедрения SQL. В листинге ниже показано, как это сделать с помощью метода IterateAndFuzz().
Метод IterateAndFuzz() начинается с перебора пар ключ/значение в JObject с помощью цикла foreach. Поскольку мы будем изменять значения в JSON, вставляя в них апострофы, мы вызываем DeepClone() [1], чтобы получить отдельный объект, идентичный первому. Это позволяет нам перебирать одну копию пар ключ/значение JSON, одновременно изменяя другую. (Нам нужно сделать копию, потому что в цикле foreach вы не можете изменить объект, который перебираете.)
В цикле foreach мы проверяем, является ли значение в текущей паре ключ/значение JTokenType.String [2] или JTokenType.Integer [3], и продолжаем фаззинг этого значения, если оно имеет строковый или целочисленный тип. После печати сообщения [4], предупреждающего пользователя о том, какой ключ мы подвергаем фаззингу, мы проверяем, является ли значение целым числом, чтобы сообщить пользователю, что мы преобразуем значение из целого числа в строку.
Поскольку целые числа в JSON не имеют кавычек и должны быть целыми числами или числами с плавающей запятой, вставка значения с апострофом может вызвать исключение синтаксического анализа. Многие слабо типизированные веб-приложения, созданные с помощью Ruby on Rails или Python, не заботятся о том, меняет ли тип значение JSON, но строго типизированные веб-приложения, созданные с помощью Java или C#, могут вести себя не так, как ожидалось. Веб-приложение CsharpVulnJson не заботится о том, изменен ли тип намеренно.
Затем мы сохраняем старое значение в переменной oldVal [5], чтобы иметь возможность заменить его после фаззинга текущей пары ключ/значение. После сохранения старого значения мы переназначаем текущее значение с исходным значением, но с апострофом, прикрепленным к концу значения [6], чтобы, если оно будет помещено в запрос SQL, это должно вызвать исключение синтаксического анализа.
Чтобы определить, вызовет ли измененное значение ошибку в веб-приложении, мы передаем измененный JSON и URL-адрес для отправки в метод Fuzz() [7] (обсуждается далее), который возвращает логическое значение, сообщающее нам, является ли значение JSON уязвимо для SQL-инъекций. Если Fuzz() возвращает true, мы сообщаем пользователю, что значение может быть уязвимо для SQL-инъекций. Если Fuzz() возвращает false, мы сообщаем пользователю, что ключ не кажется уязвимым.
Как только мы определили, уязвимо ли значение для внедрения SQL, мы заменяем измененное значение JSON исходным значение [8] и переходим к следующей паре ключ/значение.
Фаззинг с помощью HTTP-запроса
Наконец, нам нужно выполнить фактические HTTP-запросы с испорченными значениями JSON и прочитать ответ с сервера, чтобы определить, может ли значение быть внедрённым. В листинге ниже показано, как метод Fuzz() создает HTTP-запрос и проверяет ответ для определенных строк, чтобы определить, подвержено ли значение JSON уязвимости, связанной с внедрением SQL.
Поскольку нам нужно отправить всю строку JSON в виде байтов, мы передаем строковую версию нашего JObject, возвращенную ToString() [2], методу GetBytes() [1], который возвращает массив байтов, представляющий строку JSON. Мы также создаем первоначальный HTTP-запрос, вызывая статический метод Create() [3] из класса WebRequest для создания нового WebRequest, преобразуя полученный объект в класс HttpWebRequest. Далее мы назначаем метод HTTP, длину контента и тип контента запроса. Мы присваиваем свойству Method значение POST, поскольку значением по умолчанию является GET, и мы назначаем длину нашего массива байтов, который мы будем отправлять, свойству ContentLength. Наконец, мы присваиваем типу ContentType приложение/javascript, чтобы веб-сервер знал, что получаемые им данные должны быть в правильном формате JSON.
Теперь мы записываем наши данные JSON в поток запросов. Мы вызываем метод GetRequestStream() [4] и присваиваем возвращаемый поток переменной в контексте оператора using, чтобы наш поток правильно удалялся после использования. Затем мы вызываем метод Write() [5] потока, который принимает три аргумента: массив байтов, содержащий наши данные JSON, индекс массива, с которого мы хотим начать запись, и количество байтов, которые мы хотим записать. (Поскольку мы хотим записать их все, мы передаем всю длину массива данных.)
Чтобы получить ответ от сервера, мы создаем блок try, поэтому мы можем перехватывать любые исключения и получать их ответы. Мы вызываем GetResponse() [6] внутри блока try, чтобы попытаться получить ответ от сервера, но нас интересуют только ответы с кодами возврата HTTP 500 или выше, что приведет к тому, что GetResponse() выдаст исключение.
Чтобы перехватить эти ответы, мы сопровождаем блок try блоком catch, в котором вызываем GetResponseStream() [7] и создаем новый StreamReader из возвращенного потока. Используя метод ReadToEnd() [8] потока, мы сохраняем ответ сервера в строковой переменной соответственно (объявленной перед запуском блока try).
Чтобы определить, могло ли отправленное значение вызвать ошибку SQL, мы проверяем ответ на одну из двух известных строк, которые появляются в ошибках SQL. Первая строка, «syntax error» [9], представляет собой общую строку, присутствующую в ошибке MySQL, как показано в листинге ниже.
1 |
ERROR: 42601: syntax error at or near "dsa" |
Вторая строка, «untermination» [10], появляется при конкретной ошибке MySQL, когда строка не завершается, как в листинге ниже.
1 |
ERROR: 42601: unterminated quoted string at or near "'); " |
Появление любого сообщения об ошибке может означать, что в приложении существует уязвимость SQL-инъекции. Если ответ на возвращаемую ошибку содержит любую строку, мы возвращаем значение true вызывающему методу, что означает, что мы считаем приложение уязвимым. В противном случае мы возвращаем false.
Тестирование JSON-фаззера
Выполнив три метода, необходимые для фаззинга HTTP-запроса JSON, мы можем протестировать HTTP-запрос Create User, как показано в листинге ниже.
Запуск фаззера по запросу «Создать пользователя», должен показать, что большинство параметров уязвимы для атаки SQL-инъекцией (строки, начинающиеся с вектора SQL-инъекции), за исключением метода JSON [3], используемого веб-приложением для определения операции, которую необходимо выполнить. Обратите внимание, что даже параметры age [1] и zip [2], которые изначально были целыми числами в JSON, уязвимы, если при тестировании они преобразуются в строку.
На этом все. Всем хорошего дня!