Как стать автором
Обновить

REcollapse: фаззинг с использованием unicode-нормализации

Уровень сложностиСложный
Время на прочтение6 мин
Количество просмотров3.9K
Автор оригинала: 0xacb
⚠ Дисклеймер ⚠

Данный инструмент/метод создан/описан[/переведён] исключительно для образовательных и этических целей тестирования. Использование данного инструмента для атаки целей без предварительного взаимного согласия является незаконным. Разработчики [и переводчики] не несут никакой ответственности и не отвечают за любое неправильное использование или ущерб, причиненный этим инструментом.

В этом посте я расскажу о технике REcollapse. Я изучал её последние пару лет, чтобы обнаружить до странности простые, но эффективные уязвимости в защищённых объектах. Эта техника может быть использована для захвата учётных записей с нулевым взаимодействием, обнаружения новых обходных путей для брандмауэров веб-приложений и многого другого.

Этот пост преимущественно основан на моём выступлении на BSidesLisbon 2022 и посвящён запуску инструмента REcollapse, который теперь доступен на GitHub. Это также то, что мы начали исследовать внутри Ethiack.

Работа с пользовательским вводом

Всё начинается с непредвиденного ввода. Современные приложения и API полагаются на валидацию, санацию и нормализацию. Обычно это делается с помощью пользовательских регулярных выражений и широко используемых библиотек, которые проверяют и преобразуют типичные форматы пользовательского ввода, такие как адреса электронной почты, URL и другие.

Вот несколько примеров валидации и санации:

Валидация (Python)

>>> re.match(r"^\S+@\S+\.\S+$", "aa.com")
>>> re.match(r"^\S+@\S+\.\S+$", "a@a.com")
<re.Match object; span=(0, 7), match='a@a.com'>

Санация (PHP)

> htmlspecialchars("input'\"><script>alert(1);</script>");
= "input&#039;&quot;&gt;&lt;script&gt;alert(1);&lt;/script&gt;"

Цель всегда состоит в том, чтобы предотвратить сохранение опасного пользовательского ввода. Рассмотрим приложение, которое отвергает специальные символы в имени пользователя в конечной точке /signup. Злоумышленник не может внедрить полезную нагрузку в имя, но это не означает, что в дальнейшем имя не будет санировано, что может привести к уязвимостям, таким как XSS. В этом случае мы можем попытаться найти альтернативные конечные точки, которые принимают специальные символы в одном параметре. Именно так я поступил с @itscachemoney в 2019 году в Dropbox, или же… мы можем попытаться обойти регулярное выражение "чёрным ящиком", как я покажу ниже в этом посте.

Нормализация же используется для обеспечения единообразия пользовательского ввода. Это удобно для приложений с несколькими учетными записями, чтобы избежать дублирования адресов электронной почты, таких как a@a.com / A@a.COM / á@ª.com и так далее. Разные библиотеки дают различные выходные нормализованные данные, как вы можете видеть в этих примерах, что может быть полезно для определения технологий, используемых бэкендом.

Нормализация (PHP)

> iconv("UTF-8", "ASCII//TRANSLIT", "Ãéï°úç");
= "~A'e\"i^0'uc"

Нормализация (Python)

>>> unidecode.unidecode("Ãéï°úç")
'Aeideguc'

Более подробную информацию о нормализации, если вы не знакомы с ней, можно найти здесь: https://🅂𝖍𝐤ₛᵖ𝒓.ⓜ𝕠𝒃𝓲/🆆🆃🅵/. Однако нормализация в браузере – только вершина айсберга.

В чём проблема?

Regex обычно заимствуется из StackOverflow, Github или других источников. Разработчики обычно не проверяют их должным образом и иногда вставляют разные регулярные выражения в конечные точки бэкенда. Например, вышеупомянутый регекс ^\S+@\S+.\S+$ не работает правильно при проверке электронной почты:

regex101.com
regex101.com

Тесты не всегда защищают от ошибок, так как regex может быть специфичен для подмножества возможных случаев. В следующем сценарии инъекция с кавычкой всё ещё проходит через assert:

>>> msg = 'Entity "test" is not available'
>>> assert re.match(r'^Entity ".+" is not available$', msg)
>>> msg = 'Entity ""><h1>x" is not available'
>>> assert re.match(r'^Entity ".+" is not available$', msg)

С GitHub Copilot тоже интересно. Генерация кода для проверки того, является ли URL частью домена из белого списка, даёт следующий результат в Python:

def url_is_subdomain(url, domain):
    """Return True if url is a subdomain of domain."""
    return re.match(r'^(?:https?://)?(?:[^/]+\.)?%s(?:/.*)?$' % domain, url)

Фаззинг этого regex с помощью инструмента REcollapse, представленного ниже, даёт на вход https://example՟com, который будет принят как example.com в качестве домена, но он преобразуется в xn--examplecom-ehl (punycode), что позволяет атакующему обойти валидацию.

Почему нормализация может быть проблемой?

Что касается нормализации, то иногда может возникнуть путаница и дублирование состояний, если нормализация не используется последовательно во всех конечных точках и путях. Допустим, у нас есть жертва с электронной почтой hildegarde@example.com. Атакующий может попытаться исследовать все пути с электронной почтой hil°arde@example.com.

Нормализация (Python)

>>> unidecode.unidecode("hil°arde@example.com")
'hildegarde@example.com'
>>> unidecode.unidecode("victim@exámple.com")
'victim@example.com'

Это относится и к доменной части, в результате чего можно получить ссылку восстановления на punycode-домене для victim@example.com на victim@exámple.com, который разрешается на victim@xn--exmple-qta.com, что потенциально может привести к ATO с нулевым взаимодействием.

Это также можно применить к SSO или OAuth, если исходное или целевое приложение нормализует критические идентификаторы, такие как адреса электронной почты.

Мы не одинаковые

Основные библиотеки regex разных языков программирования могут иметь небольшие различия при обработке одного и того же регулярного выражения. Рассмотрим следующее общее описание знака доллара:

$ обозначает позицию конца строки или перед терминальным символом в конце строки (если есть)

JavaScript

> "aaa".match(/^[a-z]+$/)
[ 'aaa', index: 0, input: 'aaa', groups: undefined ]
> "aaa123".match(/^[a-z]+$/)
null
> "aaa\n".match(/^[a-z]+$/)
null
> "aaa\n123".match(/^[a-z]+$/)
null

Python

>>> re.match(r"^[a-z]+$", "aaa")
<re.Match object; span=(0, 3), match='aaa'>

>>> re.match(r"^[a-z]+$", "aaa123")
>>> re.match(r"^[a-z]+$", "aaa\n")
<re.Match object; span=(0, 3), match='aaa'>

>>> re.match(r"^[a-z]+$", "aaa\n123")

Ruby

irb(main):001:0> "aaa".match(/^[a-z]+$/)
=> #<MatchData "aaa">
irb(main):002:0> "aaa123".match(/^[a-z]+$/)
=> nil
irb(main):003:0> "aaa\n".match(/^[a-z]+$/)
=> #<MatchData "aaa">
irb(main):004:0> "aaa\n123".match(/^[a-z]+$/)
=> #<MatchData "aaa">

Тестирование одних и тех же пар regex и input на разных библиотеках без установки флагов многострочного regex приводит к различному поведению:

Сравнительная таблица
Сравнительная таблица

Другая проблема заключается в том, что разработчики обычно хоть и валидируют входные данные, но всё равно используют исходный вариант вместо извлечения совпадающей части. Использование ^ и $ для проверки начала и конца строки может по прежнему допускать символы новой строки, или же эти проверки могут вовсе отсутствовать.

Техника REcollapse

Итак, как обойти текущую валидацию или санацию? Кроме того, как мы можем использовать преобразования пользовательского ввода?

Рассмотрим следующий сценарий:

https://example.com/redirect?url=https://legit.example.com ✅
https://example.com/redirect?url=https://evil.com ❌

Мы не можем перенаправить на URL, контролируемый атакующей стороной, с первого взгляда. Перебор полезной нагрузки также не даёт результата. Что мы можем сделать?

  • Определить опорные позиции регулярного выражения

    • Начало и конец входной строки

  • Определить позиции разделителей

    • До и после специальных символов

  • Определить позиции потенциально нормализованных символов

    • Обычно гласные ª > a

  • Фаззить позиции со всеми возможными байтами от %00 до %ff. Здесь вы можете увидеть больше примеров:

  • Проанализировать результаты: сортировка по кодам или длине ответов.

REcollapse

Инструмент REcollapse может генерировать входные данные в соответствии с этими правилами и поддерживает несколько размеров и кодировок подбора. Он также может быть полезен для обхода WAF и слабых средств защиты от уязвимостей. Цель этого инструмента – генерировать полезную нагрузку для тестирования. Фактический фаззинг должен выполняться с помощью других инструментов, таких как Burp (intruder), ffuf или подобных. Для перехода любого обхода на новый уровень обычно требуется ручной и творческий подход.

Инструмент опубликован здесь: https://github.com/0xacb/recollapse

Ресурсы

Примеры ошибок можно посмотреть на моих слайдах BSidesLisbon или NahamCon.

Видеоролики с более подробными объяснениями были опубликованы на YouTube: NahamCon 2022 EU, BSidesLisbon 2022.

Таблица нормализации также доступна здесь: https://0xacb.com/normalization_table.

Выводы

  • Разработчикам: всегда тестируйте и проверяйте свои regex, или полагайтесь на известные библиотеки.

    • Простые модификации ввода могут привести к большому ущербу

  • Выполняйте фаззинг путем переворачивания или добавления байтов

    • Тестирование регексов методом "черного ящика" всё ещё не очень распространено

  • Поведение регекса может раскрыть информацию о библиотеках и технологиях

  • Если что-то валидируется, и вы можете это обойти…

    • Подумайте о влиянии, и вы увидите общую картину!

Нашли опечатку или неточность в переводе? Выделите и нажмите CTRL/⌘+Enter

Читайте также

Теги:
Хабы:
Всего голосов 22: ↑21 и ↓1+20
Комментарии0

Публикации

Истории

Работа

Ближайшие события