Пишем расширение для Burp Suite с помощью Python

Kate

Administrator
Команда форума
Думаю многие знают о таком инструменте, как Burp Suite от PortSwigger. Burp Suite – популярная платформа для проведения аудита безопасности веб-приложений. Помимо того, что Burp и так содержит тонну полезных функций, он еще и дает возможность пользователям создавать свои расширения, позволяющие невероятно увеличить встроенный функционал приложения.

Однако, статей по созданию расширений на Python в интернете не так и много, думаю, здесь сказалось то, что Burp написан на Java, и документация для расширений, естественно, описывает работу с Java. Но что поделать, расширения очень нужны и помогают получить преимущество, если речь идет о Bug Bounty. Так что предлагаю сегодня рассмотреть азы создания расширений для Burp Suite на Python, а писать мы будем непосредственно сканер CORS misconfiguration.

Подготовка к работе​

Как уже было сказано выше, Burp использует Java, поэтому для разработки на Python нам нужно будет загрузить Jython Standalone Edition, как об этом говорит документация на сайте PortSwigger. После загрузки открываем Burp и заходим в Extender - Options - Python environment и выбираем путь до нашего Jython файла.

Настраиваем Python Environment
Настраиваем Python Environment
На этом настройку Burp можно считать завершенной, однако я предлагаю обратить внимание на репозиторий burp-exceptions. Если во время работы возникнет исключение, то оно будет выведено в страшном формате Java:

Java исключение
Так как пишем мы на Python, и скорее всего не знакомы с Java, то приятней было бы получать в исключения в привычном и читабельном виде. Расширение из этого репозитория превращает Java исключения в обычный Python вид:

Python исключение
Поэтому предлагаю установить этот модуль, дабы помочь в дальнейшем решении проблем.

Установка достаточно простая и подробно описана на гитхабе:

  1. Открываем Burp, заходим в Extender - Options - Python environment. Указываем папку, в которую поместим данный модуль, в поле Folder for loading modules.
  2. Загружаем exceptions_fix.py и кладем его в выбранную папку
  3. В файл с нашим расширением нужно будет добавить дополнительные строки, которые опишем уже в следующей главе

Создаем файл с расширением​

Напомню, что в данной статье мы будем делать сканер CORS уязвимостей в пассивном режиме. Для других типов расширений могут понадобиться другие интерфейсы.

Создаем Python файл. Назовем его, допустим, cors-scanner.py.

Для начала импортируем загруженный модуль для преобразования ошибок

try:
from exceptions_fix import FixBurpExceptions
except ImportError:
pass
Из класса burp импортируем интерфейсы, которые нам понадобятся для работы

from burp import IBurpExtender, IScannerCheck, IScanIssue
Ну и еще несколько импортов для корректной работы модуля преобразования ошибок

from java.io import PrintWriter
import sys
И сразу добавим в самый конец файла саму обработку ошибок:

try:
FixBurpExceptions()
except:
pass
На этом с импортами покончено, время писать код

Создаем класс BurpExtender​

Следует оговориться - наше расширение направлено только сканирование только одного типа уязвимостей. Для создания большого количества сканеров нужно будет создать несколько классов, как например в популярном расширении activeScan++, которое тоже написано на Python. Мы же обойдемся только одним, в котором объединим и сканер и регистрацию расширения.

Создаем наш класс:

class BurpExtender(IBurpExtender, IScannerCheck):
IBurpExtender - главный интерфейс, его должны наследовать все расширения для Burp.
IScannerCheck - интерфейс сканера, он позволит нам использовать пассивный и/или активный режим сканирования, именно благодаря нему мы будем обрабатывать все наши запросы.

Внутри этого класса создадим главный метод

def registerExtenderCallbacks(self, callbacks):

sys.stdout = PrintWriter(callbacks.getStdout(), True)

self._callbacks = callbacks
self._helpers = callbacks.getHelpers()

callbacks.setExtensionName('CORS Passive Scanner')

callbacks.registerScannerCheck(self)
Здесь мы объявили вспомогательные инструменты, которые используем в будущем и указали имя нашего расширения. А так же зарегистрировали наш кастомный сканер с помощью

callbacks.registerScannerCheck(self)
Если бы мы использовали несколько классов-сканеров, аргументом в registerScannerCheck(..) мы бы передали нужные нам классы. Но так как класс у нас один - передадим self.

Создадим наш метод, который будет вызываться автоматически во время пассивного сканирования

def doPassiveScan(self, baseRequestResponse):
baseRequestResponse - интерфейс, который содержит информацию о запросе и ответе, и позволяет получать или изменять информацию.

Немного теории: что будет являться триггером на возможное наличие CORS misconfiguration? Правильный ответ - заголовки ответа сервера. Нас интересуют эти два заголовка:

  1. Access-Control-Allow-Origin
  2. Access-Control-Allow-Credentials
Наличие их в заголовках ответа намекает нам на возможные проблемы. Поэтому далее мы будем работать только с теми запросами, в ответ на которые мы получили один из этих заголовков.

Наши _helpers, которые мы определили выше, имеют такой метод как analyzeResponse(..), возвращающий нам детальную информацию об ответе сервера. В analyzeResponse(..) необходимо передать наш ответ (который изначально пришел в виде байтов). Получить ответ мы можем из нашей переменной baseRequestResponse (которая содержит и запрос, и ответ, как видно из названия) с помощью метода getResponse(). После чего, analyzeResponse(..)возвращает нам удобный для работы ответ сервера. Устав от слова "ответ" наконец-то получаем данные в ввиде IResponseInfo. Теперь мы можем свободно использовать методы для получения нужных нам данных, а нужны нам, как мы помним - заголовки. Так что просто достаем их методом getHeaders(). Не забываем превратить их в список, потому что изначально они идут в Java формате.

def doPassiveScan(self, baseRequestResponse):

response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())

Далее мы итерируем полученные заголовки и ищем есть ли среди них Access-Control-Allow-Origin либо Access-Control-Allow-Credentials.

def doPassiveScan(self, baseRequestResponse):

response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())

for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:
Если мы выведем заголовки с помощью

sys.stdout.println(response_headers)
То увидим примерно следующее:

[u'Access-Control-Allow-Credentials: true', u'cache-control: private, s-maxage=0, no-store, no-cache']
# и так далее
Если находим нужные заголовки, то можно приступать к дальнейшей работе, для которой нам понадобятся заголовки запроса и URL. Получаем их

request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()
request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()
Так как уязвимостей может быть много, например если сайт доверяет любому субдомену и небезопасному протоколу http, то нам придется зарегистрировать несколько исключений (но можно и одно, тут каждому на вкус и цвет. Однако, если прекратить тестирование при первой уязвимости, например сайт доверяет одному из субдоменов, то мы пропустим дальнейшие тесты, а ведь там может вскрыться, что сайт доверяет любому домену, и такая уязвимость, естественно, будет нести гораздо большую опасность, так что мы оставим все ошибки). Для этого создадим просто список issues, в который будем складывать все найденные уязвимости, и вернем их в конце всех тестов.

По итогу наша функция выглядит примерно так:

def doPassiveScan(self, baseRequestResponse):

response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())

for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:

request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()
request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()

issues = []
Теперь настала пора подумать о наших payloads.

Генерируем Origin для тестирования​

Сделаю отступление - вариантов различной нагрузки существует множество. Есть хороший сканер, написанный на Python - CORScanner, можно взять генератор оттуда. Для статьи я ограничусь только самыми популярными тестами (охватывают большинство misconfiguration, как мне кажется), поэтому при желании - добавляйте свои варианты нагрузок.

Наша функция будет иметь один аргумент - URL.

def _generate_payloads(self, url):

host = url.getHost()
protocol = url.getProtocol()

payloads = {}
Достаем из нашего URL два необходимых параметра - хост и протокол. Я предлагаю хранить все сгенерированные пейлоады в формате словаря словарей. Так мы сможем хранить наш ключ (по примеру из сканера с гитхаба), который можно использовать например для отладки. Значением будет являться словарь, который содержит в себе нужные нам параметры. Можно описать все что угодно, я сделаю сокращенный вариант. Выглядеть это примерно будет так:

{'trust_any_origin': {'payload_url': 'XXX', 'description': 'YYY', 'severity': 'ZZZ'}}
Помимо самой ссылки и описания добавим severity, которую показывает Burp. Так как очевидно, что уязвимость, позволяющая отправлять и принимать запросы с любого URL будет гораздо опаснее, чем уязвимость, позволяющая делать это только с субдомена нашего таргета. Далее ничего сложного, описываем пейлоады, добавляем их в словарь и возвращаем

def _generate_payloads(self, url):

host = url.getHost()
protocol = url.getProtocol()

payloads = {}

# trust any origin
payload_url = '{}://evil.com'.format(protocol)
payloads['trust_any_origin'] = {'origin': payload_url, 'description': 'Site trust any origin', 'severity': 'High'}

# trust any subdomain
payload_url = '{}://evil.{}'.format(protocol, host)
payloads['trust_any_subdomain'] = {'origin': payload_url, 'description': 'Site trust any subdomain', 'severity': 'High'}

# trust insecure protocol
if protocol == 'https':
payload_url = 'http://evil.{}'.format(host)
payloads['trust_http'] = {'origin': payload_url, 'description': 'Site trust insecure protocol', 'severity': 'Medium'}

# trust null
payload_url = 'null'
payloads['trust_null'] = {'origin': payload_url, 'description': 'Site trust null origin', 'severity': 'High'}

# prefix match full url
payload_url = '{}://{}.evil.com'.format(protocol, host)
payloads['trust_prefix'] = {'origin': payload_url, 'description': 'Site trust prefix', 'severity': 'High'}

# trust invalid dot escape
splitted_host = host.split('.')
payload_host = '{}A{}.{}'.format('.'.join(splitted_host[:-1]), splitted_host[-1], splitted_host[-1])
payload_url = '{}://{}'.format(protocol, payload_host)
payloads['trust_invalid_regex'] = {'origin': payload_url, 'description': 'Site trust origin with unescaped dot', 'severity': 'High'}

return payloads
Сделаю пояснение по поводу {'severity': 'Medium'} для http протокола. Дело в том, что данный тип атаки был показан на одной из конференций, не найду сейчас ссылку, но автор презентации показывал, что отправил репорт в Google - те подумали и приняли его. Подобный репорт так же был принят на HackerOne (#629892). Однако, когда я отправлял подобную уязвимость - мне выставили N/A. Так что я думаю все зависит от триагера и правил программы, поэтому поставим Medium, уязвимость вроде как есть, но не все готовы ее приянять, так как она непростая в реализации.

Примерное описание работы

Отправляем пейлоады​

Ну хорошо, вернемся к doPassiveScan. Но до этого быстро создадим короткий метод

def _add_origin(self, headers, value):
headers = list(headers)
headers.append('Origin: {}'.format(value))
return headers
В него мы просто будем передавать заголовки, которые мы отправляли в оригинальном запросе, при этом добавим к ним наш новый Origin и вернем обратно.

Итерируем наши пейлоады, и создаем новую переменную payload_headers, в которую будут записаны наши новые заголовки для атаки, полученные от _add_origin.

def doPassiveScan(self, baseRequestResponse):

response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())

for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:

request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()
request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()

issues = []
payloads = self._generate_payloads(request_url)

for payload in payloads.values():
payload_headers = self._add_origin(request_headers, payload['origin'])
Далее, чтобы сформировать запрос, нам нужно его тело. Получаем offset для тела запроса, затем получим само тело, срезав запрос по этому индексу.

body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()
request_body = baseRequestResponse.getRequest()[body_offset:]
И, если тело присутствует в запросе, то отправляем его. Если нет - передаем None

if len(request_body) == 0:
request = self._helpers.buildHttpMessage(payload_headers, None)
else:
request = self._helpers.buildHttpMessage(payload_headers, request_body)
Все это нужно для того, чтобы в POST запросах не терялось тело, как ни трудно догадаться. Осталось совсем немного. Создаем наш запрос и отправляем его. После чего, достаем из него заголовки по уже известной схеме

response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)
response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())
Что мы ищем в заголовках? Правильно - наличие Access-Control-Allow-Origin, который говорит нам о том, что сервер разрешил нашему Origin получать ответ.

for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header:
Если данное условие выполняется - соответственно мы нашли некоторую уязвимость. Осталось только сообщить о ней Burp, чтобы он сообщил о ней нам. Оставим пока что эту часть недописанной.

Итого, наша функция сейчас выглядит примерно вот так:

cors-scanner.py
Нам не хватает класса, описывающего уязвимость. Давайте его создадим!

Создаем кастомный класс уязвимости​

Создаем класс, наследуясь от IScanIssue

class CustomScanIssue(IScanIssue):
Описываем __init__

def __init__(self, httpService, url, httpMessages, name, detail, severity):
self._httpService = httpService
self._url = url
self._httpMessages = httpMessages
self._name = name
self._detail = detail
self._severity = severity
self._confidence = 'Certain'
return
Здесь просто различные параметры для описания ошибки. URL, имя, описание, severity, confidence и так далее. Вся эта информация отображается, когда мы в Dashboard нажимаем на URL, в котором найдена уязвимость. Добавим методы, чтобы Burp мог получать нужные значения. В итоге класс выглядит так:

class CustomScanIssue(IScanIssue):
def __init__(self, httpService, url, httpMessages, name, detail, severity):
self._httpService = httpService
self._url = url
self._httpMessages = httpMessages
self._name = name
self._detail = detail
self._severity = severity
self._confidence = 'Certain'

def getUrl(self):
return self._url

def getIssueName(self):
return self._name

def getIssueType(self):
return 0

def getSeverity(self):
return self._severity

def getConfidence(self):
return self._confidence

def getIssueBackground(self):
return None

def getRemediationBackground(self):
return None

def getIssueDetail(self):
return self._detail

def getRemediationDetail(self):
return None

def getHttpMessages(self):
return self._httpMessages

def getHttpService(self):
return self._httpService

Описываем уязвимость​

Теперь можно с помощью данного класса создать объект уязвимости.

В наш блок, где мы проверяем наличие заголовка Access-Control-Allow-Origin в ответе после атаки, добавим следующее:

for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header:

issues.append(
CustomScanIssue(
baseRequestResponse.getHttpService(),
request_url,
[response],
'CORS Misconfiguration',
payload['description'],
payload['severity']
)
)

break
Передаем в наш класс следующие аргументы:

  1. HTTP сервисы
  2. URL, на котором найдена уязвимость
  3. response, который мы получили после makeHttpRequest, сюда так же можно передать маркеры, которые будут подсвечивать находку в UI, но нам такое не нужно
  4. Название уязвимости, у нас оно будет одно для всех
  5. Описание уязвимости из пейлоада
  6. Severity, так же из пейлоада
После чего прерываем цикл, чтобы не проверять остальные заголовки, мы уже нашли все, что нужно.

Так же вернемся немного в начало и добавим описание уязвимости, если вдруг у нас Access-Control-Allow-Origin: *

if response_header == 'Access-Control-Allow-Origin: *':
return CustomScanIssue(
baseRequestResponse.getHttpService(),
request_url,
[baseRequestResponse],
'CORS Misconfiguration',
'Site trust *',
'Medium'
)
Сделаем мы это для того, чтобы зря не сканировать URL, так как в Allow-Origin у нас wildcard.

Дополнительно так же нужно определить метод consolidateDuplicateIssues. Он будет вызываться чтобы не дублировать уязвимость, если такая уже была найдена для данного URL.

def consolidateDuplicateIssues(self, existingIssue, newIssue):
if existingIssue.getIssueDetail() == newIssue.getIssueDetail():
return -1

return 0
Так как названия у нас одинаковые, то будем сравнивать по описанию. Если нашли уязвимость с таким же описание на одном и том же URL - просто проигнорируем.

Финальный скрипт выглядит так:

cors-scanner.py
На этом все, время устанавливать и тестировать наше творение.

Загружаем расширение в Burp​

Тут все просто - открываем вкладку Extender, далее вкладка Extensions -> Add.
Extension type - естественно, Python
Extension file - выбираем наш файл

Жмем Next, начнется загрузка расширения. Должно появится сообщение, что расширение загружено успешно. Можем тестировать

Тестирование​

Буквально во время написания статьи пришло уведомление с HackerOne о том, что была закрыта зарепорченая мной CORS misconfiguration, при которой сайт проверял только префикс Origin, так что такой пример не удалось показать.

Не будем далеко ходить - проведем наши тесты прям в лабах PortSwigger.

Первая из них - Origin Reflect. Ничего сложного, уязвимость присутствует в принципе всегда. Открываем лабу, логинимся в наш аккаунт под wiener:peter. Включаем прокси в браузере, обновляем нашу страницу, в которую мы залогинились. Как ни странно, ловим сразу 6 High репортов.

Сработали все 6 пейлоадов, что верно
Сработали все 6 пейлоадов, что верно
Вторая лаба - уязвимость, когда сайт доверяет Origin: null
Мы такую нагрузку делали, поэтому просто заходим в аккаунт под wiener:peter. Не удивляясь, ловим уязвимость

null origin
null origin
Последняя третья лаба - на доступ с небезопасного протокола. Это как раз тот спорный момент, о котором я говорил. Логинимся в аккаунт, получаем это:

Medium, как и было задумано
Medium, как и было задумано
Можно проверить работу так же на настоящих сайтах (хотя, от лабы ничего не будет отличаться), но как я уже сказал, найденную мной хорошую уязвимость закрыли, так что глянем на другой субдомен этого же таргета, но уязвимость будет другого типа, а именно сайт разрешает запросы с любых субдоменов. Она существует потому что использовать ее нелегко, требуется либо XSS на субдомене, либо subdomain-takeover. Заходим на уязвимый URL, видим результат:

Репорт в dashboard
Репорт в dashboard
Заголовки ответа
Заголовки ответа

Итоги​

В этой статье я рассказал о процессе создания расширений для Burp Suite на языке Python. Я далеко не самый лучший разработчик расширений под него, да и сам процесс дело затруднительное. Возможно, я допустил некоторые ошибки в названиях либо в логике работы. Сказывается отсутствие опыта в Java и отсутствие документации для Python. Тем не менее, мы получили желаемое - а именно рабочее расширение для таких типов уязвимости как CORS misconfiguration. На мой взгляд - данная уязвимость гораздо интереснее, чем пресловутая XSS, и легче в исполнении. Однако импакт, которого можно достичь, достаточно велик. Я сканирую CORS всегда и везде, достаточно одной ошибки и в кармане уже Information Disclosure или сразу OTA.

Сканер такого рода уязвимостей для Burp - вещь несомненно хорошая, тем более расширения для CORS по какой-то причине отсутствуют в магазине (либо я не заметил).

Burp - отличный инструмент для хобби и заработка. Багхантеры знают, насколько высока бывает конкуренция, и автоматизация всего что можно в этом играет огромную роль. Я находил ошибки в известных компаниях только потому что автоматизирую все процессы, зачастую просматривая результаты автоматического сканирования и изучая руками.

Читателям - спасибо за то, что читали, багхантерам - удачи!

Никогда не переставайте учиться и изучать новые инструменты.


Источник статьи: https://habr.com/ru/post/546476/
 
Сверху