[Информационная безопасность, Python, Тестирование веб-сервисов] Пишем расширение для Burp Suite с помощью Python
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр!Думаю многие знают о таком инструменте, как 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На этом настройку Burp можно считать завершенной, однако я предлагаю обратить внимание на репозиторий burp-exceptions. Если во время работы возникнет исключение, то оно будет выведено в страшном формате Java:Java исключение
java.lang.RuntimeException: org.python.core.PyException
at burp.fl.a(Unknown Source)
at burp.edd.a(Unknown Source)
at burp.e2g.a(Unknown Source)
at burp.e2g.g(Unknown Source)
at burp.i1c.stateChanged(Unknown Source)
at javax.swing.JTabbedPane.fireStateChanged(JTabbedPane.java:416)
at javax.swing.JTabbedPane$ModelListener.stateChanged(JTabbedPane.java:270)
...
Так как пишем мы на Python, и скорее всего не знакомы с Java, то приятней было бы получать в исключения в привычном и читабельном виде. Расширение из этого репозитория превращает Java исключения в обычный Python вид:Python исключение
*** PYTHON EXCEPTION
Traceback (most recent call last):
File "/Users/mb/Desktop/burp extension/exceptions_fix.py", line 8, in decorated_function
return original_function(*args, **kwargs)
File "/Users/mb/Desktop/burp extension/CustomEditorTab.py", line 78, in setMessage
self._txtInput.setEsditable(self._editable)
AttributeError: 'burp.ul' object has no attribute 'setEsditable'
Поэтому предлагаю установить этот модуль, дабы помочь в дальнейшем решении проблем.Установка достаточно простая и подробно описана на гитхабе:
- Открываем Burp, заходим в Extender - Options - Python environment. Указываем папку, в которую поместим данный модуль, в поле Folder for loading modules.
- Загружаем exceptions_fix.py и кладем его в выбранную папку
- В файл с нашим расширением нужно будет добавить дополнительные строки, которые опишем уже в следующей главе
Создаем файл с расширениемНапомню, что в данной статье мы будем делать сканер 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? Правильный ответ - заголовки ответа сервера. Нас интересуют эти два заголовка:
- Access-Control-Allow-Origin
- 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
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'])
body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()
request_body = baseRequestResponse.getRequest()[body_offset:]
if len(request_body) == 0:
request = self._helpers.buildHttpMessage(payload_headers, None)
else:
request = self._helpers.buildHttpMessage(payload_headers, request_body)
response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)
response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())
for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header:
return issues
Нам не хватает класса, описывающего уязвимость. Давайте его создадим!Создаем кастомный класс уязвимостиСоздаем класс, наследуясь от 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
Передаем в наш класс следующие аргументы:
- HTTP сервисы
- URL, на котором найдена уязвимость
- response, который мы получили после makeHttpRequest, сюда так же можно передать маркеры, которые будут подсвечивать находку в UI, но нам такое не нужно
- Название уязвимости, у нас оно будет одно для всех
- Описание уязвимости из пейлоада
- 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
from burp import IBurpExtender, IScannerCheck, IScanIssue
from java.io import PrintWriter
import sys
try:
from exceptions_fix import FixBurpExceptions
except ImportError:
pass
class BurpExtender(IBurpExtender, 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)
def _add_origin(self, headers, value):
headers = list(headers)
headers.append('Origin: {}'.format(value))
return headers
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 regex 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
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()
if response_header == 'Access-Control-Allow-Origin: *':
return CustomScanIssue(
baseRequestResponse.getHttpService(),
request_url,
[baseRequestResponse],
'CORS Misconfiguration',
'Site trust any origin',
'Medium'
)
issues = []
payloads = self._generate_payloads(request_url)
for payload in payloads.values():
payload_headers = self._add_origin(request_headers, payload['origin'])
body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()
request_body = baseRequestResponse.getRequest()[body_offset:]
if len(request_body) == 0:
request = self._helpers.buildHttpMessage(payload_headers, None)
else:
request = self._helpers.buildHttpMessage(payload_headers, request_body)
response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)
response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())
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
return issues
def consolidateDuplicateIssues(self, existingIssue, newIssue):
if existingIssue.getIssueDetail() == newIssue.getIssueDetail():
return -1
return 0
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
try:
FixBurpExceptions()
except:
pass
На этом все, время устанавливать и тестировать наше творение.Загружаем расширение в BurpТут все просто - открываем вкладку Extender, далее вкладка Extensions -> Add.
Extension type - естественно, Python
Extension file - выбираем наш файлЖмем Next, начнется загрузка расширения. Должно появится сообщение, что расширение загружено успешно. Можем тестироватьТестированиеБуквально во время написания статьи пришло уведомление с HackerOne о том, что была закрыта зарепорченая мной CORS misconfiguration, при которой сайт проверял только префикс Origin, так что такой пример не удалось показать.Не будем далеко ходить - проведем наши тесты прям в лабах PortSwigger.Первая из них - Origin Reflect. Ничего сложного, уязвимость присутствует в принципе всегда. Открываем лабу, логинимся в наш аккаунт под wiener:peter. Включаем прокси в браузере, обновляем нашу страницу, в которую мы залогинились. Как ни странно, ловим сразу 6 High репортов.
Сработали все 6 пейлоадов, что верноВторая лаба - уязвимость, когда сайт доверяет Origin: null
Мы такую нагрузку делали, поэтому просто заходим в аккаунт под wiener:peter. Не удивляясь, ловим уязвимость
null originПоследняя третья лаба - на доступ с небезопасного протокола. Это как раз тот спорный момент, о котором я говорил. Логинимся в аккаунт, получаем это:
Medium, как и было задуманоМожно проверить работу так же на настоящих сайтах (хотя, от лабы ничего не будет отличаться), но как я уже сказал, найденную мной хорошую уязвимость закрыли, так что глянем на другой субдомен этого же таргета, но уязвимость будет другого типа, а именно сайт разрешает запросы с любых субдоменов. Она существует потому что использовать ее нелегко, требуется либо XSS на субдомене, либо subdomain-takeover. Заходим на уязвимый URL, видим результат:
Репорт в dashboard
Заголовки ответаИтогиВ этой статье я рассказал о процессе создания расширений для Burp Suite на языке Python. Я далеко не самый лучший разработчик расширений под него, да и сам процесс дело затруднительное. Возможно, я допустил некоторые ошибки в названиях либо в логике работы. Сказывается отсутствие опыта в Java и отсутствие документации для Python. Тем не менее, мы получили желаемое - а именно рабочее расширение для таких типов уязвимости как CORS misconfiguration. На мой взгляд - данная уязвимость гораздо интереснее, чем пресловутая XSS, и легче в исполнении. Однако импакт, которого можно достичь, достаточно велик. Я сканирую CORS всегда и везде, достаточно одной ошибки и в кармане уже Information Disclosure или сразу OTA.Сканер такого рода уязвимостей для Burp - вещь несомненно хорошая, тем более расширения для CORS по какой-то причине отсутствуют в магазине (либо я не заметил).Burp - отличный инструмент для хобби и заработка. Багхантеры знают, насколько высока бывает конкуренция, и автоматизация всего что можно в этом играет огромную роль. Я находил ошибки в известных компаниях только потому что автоматизирую все процессы, зачастую просматривая результаты автоматического сканирования и изучая руками. Читателям - спасибо за то, что читали, багхантерам - удачи! Никогда не переставайте учиться и изучать новые инструменты.
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Big Data, Визуализация данных, Машинное обучение] Новые возможности анализа табличных данных с алгоритмами машинного обучения в Elastic
- [Python, PostgreSQL] Обрезаем большую таблицу PostgreSQL в production
- [Python, Программирование, Машинное обучение] Взлом reCAPTCHA v2
- [Тестирование IT-систем, Тестирование веб-сервисов, Подготовка технической документации] Decision Table — что это и как применять
- [Python, Научно-популярное, Биотехнологии, Будущее здесь] Визуализация и анализ белков в Biopython (перевод)
- [Информационная безопасность, Конференции, Облачные сервисы] «Инфосистемы Джет» проведет вебинар о практике защиты сред Microsoft 365
- [Информационная безопасность, Тестирование веб-сервисов] Чек-лист устранения SQL-инъекций
- [Информационная безопасность] Компания Microsoft выпустила мартовские обновления безопасности
- [Информационная безопасность, Системное администрирование, Софт, IT-компании] Microsoft выпустила обновления для серверов Exchange с неподдерживаемыми версиями CU, уязвимыми для атаки ProxyLogon
- [Информационная безопасность] Интервью с Лукой Сафоновым
Теги для поиска: #_informatsionnaja_bezopasnost (Информационная безопасность), #_python, #_testirovanie_vebservisov (Тестирование веб-сервисов), #_burp, #_python, #_rasshirenie (расширение), #_informatsionnaja_bezopasnost (информационная безопасность), #_informatsionnaja_bezopasnost (
Информационная безопасность
), #_python, #_testirovanie_vebservisov (
Тестирование веб-сервисов
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:12
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр!Думаю многие знают о таком инструменте, как 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На этом настройку Burp можно считать завершенной, однако я предлагаю обратить внимание на репозиторий burp-exceptions. Если во время работы возникнет исключение, то оно будет выведено в страшном формате Java:Java исключение java.lang.RuntimeException: org.python.core.PyException
at burp.fl.a(Unknown Source) at burp.edd.a(Unknown Source) at burp.e2g.a(Unknown Source) at burp.e2g.g(Unknown Source) at burp.i1c.stateChanged(Unknown Source) at javax.swing.JTabbedPane.fireStateChanged(JTabbedPane.java:416) at javax.swing.JTabbedPane$ModelListener.stateChanged(JTabbedPane.java:270) ... *** PYTHON EXCEPTION
Traceback (most recent call last): File "/Users/mb/Desktop/burp extension/exceptions_fix.py", line 8, in decorated_function return original_function(*args, **kwargs) File "/Users/mb/Desktop/burp extension/CustomEditorTab.py", line 78, in setMessage self._txtInput.setEsditable(self._editable) AttributeError: 'burp.ul' object has no attribute 'setEsditable'
try:
from exceptions_fix import FixBurpExceptions except ImportError: pass from burp import IBurpExtender, IScannerCheck, IScanIssue
from java.io import PrintWriter
import sys try:
FixBurpExceptions() except: pass class BurpExtender(IBurpExtender, IScannerCheck):
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)
def doPassiveScan(self, baseRequestResponse):
def doPassiveScan(self, baseRequestResponse):
response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders()) 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']
# и так далее request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()
request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders() 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 = [] def _generate_payloads(self, url):
host = url.getHost() protocol = url.getProtocol() payloads = {} {'trust_any_origin': {'payload_url': 'XXX', 'description': 'YYY', 'severity': 'ZZZ'}}
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 Отправляем пейлоадыНу хорошо, вернемся к doPassiveScan. Но до этого быстро создадим короткий метод def _add_origin(self, headers, value):
headers = list(headers) headers.append('Origin: {}'.format(value)) return headers 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']) body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()
request_body = baseRequestResponse.getRequest()[body_offset:] if len(request_body) == 0:
request = self._helpers.buildHttpMessage(payload_headers, None) else: request = self._helpers.buildHttpMessage(payload_headers, request_body) response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)
response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders()) for response_header in response_headers:
if 'Access-Control-Allow-Origin' in response_header: 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']) body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset() request_body = baseRequestResponse.getRequest()[body_offset:] if len(request_body) == 0: request = self._helpers.buildHttpMessage(payload_headers, None) else: request = self._helpers.buildHttpMessage(payload_headers, request_body) response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request) response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders()) for response_header in response_headers: if 'Access-Control-Allow-Origin' in response_header: return issues 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' return 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 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
if response_header == 'Access-Control-Allow-Origin: *':
return CustomScanIssue( baseRequestResponse.getHttpService(), request_url, [baseRequestResponse], 'CORS Misconfiguration', 'Site trust *', 'Medium' ) def consolidateDuplicateIssues(self, existingIssue, newIssue):
if existingIssue.getIssueDetail() == newIssue.getIssueDetail(): return -1 return 0 from burp import IBurpExtender, IScannerCheck, IScanIssue
from java.io import PrintWriter import sys try: from exceptions_fix import FixBurpExceptions except ImportError: pass class BurpExtender(IBurpExtender, 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) def _add_origin(self, headers, value): headers = list(headers) headers.append('Origin: {}'.format(value)) return headers 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 regex 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 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() if response_header == 'Access-Control-Allow-Origin: *': return CustomScanIssue( baseRequestResponse.getHttpService(), request_url, [baseRequestResponse], 'CORS Misconfiguration', 'Site trust any origin', 'Medium' ) issues = [] payloads = self._generate_payloads(request_url) for payload in payloads.values(): payload_headers = self._add_origin(request_headers, payload['origin']) body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset() request_body = baseRequestResponse.getRequest()[body_offset:] if len(request_body) == 0: request = self._helpers.buildHttpMessage(payload_headers, None) else: request = self._helpers.buildHttpMessage(payload_headers, request_body) response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request) response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders()) 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 return issues def consolidateDuplicateIssues(self, existingIssue, newIssue): if existingIssue.getIssueDetail() == newIssue.getIssueDetail(): return -1 return 0 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 try: FixBurpExceptions() except: pass Extension type - естественно, Python Extension file - выбираем наш файлЖмем Next, начнется загрузка расширения. Должно появится сообщение, что расширение загружено успешно. Можем тестироватьТестированиеБуквально во время написания статьи пришло уведомление с HackerOne о том, что была закрыта зарепорченая мной CORS misconfiguration, при которой сайт проверял только префикс Origin, так что такой пример не удалось показать.Не будем далеко ходить - проведем наши тесты прям в лабах PortSwigger.Первая из них - Origin Reflect. Ничего сложного, уязвимость присутствует в принципе всегда. Открываем лабу, логинимся в наш аккаунт под wiener:peter. Включаем прокси в браузере, обновляем нашу страницу, в которую мы залогинились. Как ни странно, ловим сразу 6 High репортов. Сработали все 6 пейлоадов, что верноВторая лаба - уязвимость, когда сайт доверяет Origin: null Мы такую нагрузку делали, поэтому просто заходим в аккаунт под wiener:peter. Не удивляясь, ловим уязвимость null originПоследняя третья лаба - на доступ с небезопасного протокола. Это как раз тот спорный момент, о котором я говорил. Логинимся в аккаунт, получаем это: Medium, как и было задуманоМожно проверить работу так же на настоящих сайтах (хотя, от лабы ничего не будет отличаться), но как я уже сказал, найденную мной хорошую уязвимость закрыли, так что глянем на другой субдомен этого же таргета, но уязвимость будет другого типа, а именно сайт разрешает запросы с любых субдоменов. Она существует потому что использовать ее нелегко, требуется либо XSS на субдомене, либо subdomain-takeover. Заходим на уязвимый URL, видим результат: Репорт в dashboard Заголовки ответаИтогиВ этой статье я рассказал о процессе создания расширений для Burp Suite на языке Python. Я далеко не самый лучший разработчик расширений под него, да и сам процесс дело затруднительное. Возможно, я допустил некоторые ошибки в названиях либо в логике работы. Сказывается отсутствие опыта в Java и отсутствие документации для Python. Тем не менее, мы получили желаемое - а именно рабочее расширение для таких типов уязвимости как CORS misconfiguration. На мой взгляд - данная уязвимость гораздо интереснее, чем пресловутая XSS, и легче в исполнении. Однако импакт, которого можно достичь, достаточно велик. Я сканирую CORS всегда и везде, достаточно одной ошибки и в кармане уже Information Disclosure или сразу OTA.Сканер такого рода уязвимостей для Burp - вещь несомненно хорошая, тем более расширения для CORS по какой-то причине отсутствуют в магазине (либо я не заметил).Burp - отличный инструмент для хобби и заработка. Багхантеры знают, насколько высока бывает конкуренция, и автоматизация всего что можно в этом играет огромную роль. Я находил ошибки в известных компаниях только потому что автоматизирую все процессы, зачастую просматривая результаты автоматического сканирования и изучая руками. Читателям - спасибо за то, что читали, багхантерам - удачи! Никогда не переставайте учиться и изучать новые инструменты. =========== Источник: habr.com =========== Похожие новости:
Информационная безопасность ), #_python, #_testirovanie_vebservisov ( Тестирование веб-сервисов ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:12
Часовой пояс: UTC + 5