Эксплуатация изъянов реализации кэша
В этом разделе мы узнаем, как получить значительно более широкую поверхность атаки для Web Cache Poisoning, эксплуатируя особенности конкретных реализаций систем кэширования. В частности, мы рассмотрим, почему ошибки в том, как генерируются ключи кэша, иногда оставляют сайты уязвимыми к отравлению кэша через отдельные уязвимости, которые традиционно считаются неэксплуатируемыми. Мы также покажем, как можно продвинуть классические техники ещё дальше, чтобы потенциально отравлять кэши на уровне приложения, зачастую с разрушительными последствиями.
Эти техники впервые задокументировал James Kettle в своём докладе «Web Cache Entanglement: Novel Pathways to Poisoning» на BlackHat USA 2020. Если вам интересно, как он смог обнаружить и эксплуатировать эти уязвимости в реальных условиях, вы можете посмотреть запись этого доклада.
Изъяны ключей кэша
Как правило, сайты получают большинство входных данных из пути URL (унифицированный указатель ресурса) и строки запроса. В результате это — хорошо исхоженная поверхность атаки для различных техник взлома. Однако, поскольку стартовая строка запроса обычно является частью ключа кэша, эти вхождения традиционно не считались подходящими для отравления кэша. Любая полезная нагрузка, внедрённая через учитываемые в ключе входные данные, сработает как кэш-бастер, что означает, что ваша отравленная запись кэша почти наверняка никогда не будет выдана другим пользователям.
При более внимательном рассмотрении поведение отдельных систем кэширования не всегда всё работает как ожидается. На практике многие сайты и CDN выполняют различные преобразования учитываемых компонентов при сохранении их в ключе кэша. Это может включать:
Исключение строки запроса
Фильтрацию отдельных параметров запроса
Нормализацию ввода в учитываемых компонентах
Эти преобразования могут вносить неожиданные особенности. В основном они основаны на расхождениях между данными, которые записываются в ключ кэша, и данными, которые передаются в код приложения, даже если всё это происходит из одного и того же ввода. Эти ошибки ключа кэша можно использовать для отравления кэша через вхождения, которые поначалу кажутся непригодными.
В случае полностью интегрированных кэшей на уровне приложения эти особенности могут быть ещё более выраженными. Фактически внутренние кэши могут быть настолько непредсказуемыми, что иногда их трудно тестировать вообще, не отравив кэш для живых пользователей по ошибке.
Методология исследования кэша
В этом разделе мы изложим высокоуровневую методология исследования кэша, чтобы научиться понимать его поведение и выявлять потенциальные ошибки. Затем мы рассмотрим более конкретные примеры распространённых ошибок с ключом кэша и способов их эксплуатации. Методология включает следующие шаги:
Определите подходящий «оракул кэша» (cache oracle)
Первый шаг — определить подходящий «оракул кэша», который мы сможем использовать для тестирования. Оракул кэша — это просто страница или конечная точка, которая предоставляет обратную связь о поведении кэша. Она должна кэшироваться и каким-то образом указывать, получили ли вы кэшированный ответ или ответ напрямую от сервера. Эта обратная связь может принимать различные формы, например:
Заголовок HTTP, который явно сообщает, был ли получен кэш-хит
Наблюдаемые изменения динамического содержимого
Различающиеся времена отклика
В идеале оракул кэша также будет отражать в ответе весь URL и по крайней мере один параметр запроса. Это упростит заметить расхождения в разборе между кэшем и приложением, что пригодится для построения различных эксплойтов позже.
Если вы можете определить, что используется конкретный сторонний кэш, вы также можете обратиться к соответствующей документации. В ней может содержаться информация о том, как строится ключ кэша по умолчанию. Возможно, вы даже наткнётесь на полезные советы и приёмы, например функции, позволяющие увидеть ключ кэша напрямую. Например, сайты на базе Akamai могут поддерживать заголовок Pragma: akamai-x-get-cache-key, который можно использовать для отображения ключа кэша в заголовках ответа:
Проверьте обработку ключа
Следующий шаг — исследовать, выполняет ли кэш дополнительную обработку вашего ввода при генерации ключа кэша. Вы ищете дополнительную поверхность атаки, скрытую в компонентах, казалось бы, учитываемых в ключе.
Вам следует внимательно изучить любые выполняемые преобразования. Что-то исключается из учитываемого компонента при добавлении в ключ кэша? Типичные примеры — исключение определённых параметров запроса или даже всей строки запроса, а также удаление порта из заголовка Host.
Если вам повезло иметь прямой доступ к ключу кэша, вы можете просто сравнивать ключ после внедрения различных вхождений. В противном случае можно использовать оракул кэша, чтобы сделать вывод, получили ли вы правильный кэшированный ответ. Для каждого проверяемого случая вы отправляете два похожих запроса и сравниваете ответы. Предположим, что наш гипотетический оракул кэша — домашняя страница целевого сайта. Она автоматически перенаправляет пользователей на региональную страницу. Она использует заголовок Host для динамической генерации заголовка Location в ответе:
Чтобы проверить, исключается ли порт из ключа кэша, нам сначала нужно запросить произвольный порт и убедиться, что мы получаем свежий ответ от сервера, отражающий этот ввод:
Далее отправим ещё один запрос, но на этот раз порт указывать не будем:
Как видите, нам отдали кэшированный ответ, хотя заголовок Host в запросе не содержит порт. Это доказывает, что порт исключается из ключа кэша. Важно, что полный заголовок по-прежнему передаётся в код приложения и отражается в ответе.
Хотя заголовок Host и учитывается в ключе, способ его преобразования кэшем позволяет передать полезную нагрузку приложению, при этом сохранив «нормальный» ключ кэша, который будет сопоставлён с запросами других пользователей. Такое поведение — ключевая идея всех эксплойтов, которые мы рассмотрим в этом разделе. Вы можете использовать похожий подход для исследования любой другой обработки вашего ввода кэшем. Нормализуется ли каким-то образом наш ввод? Как он хранится? Замечаете ли какие-нибудь аномалии? Позже мы рассмотрим, как отвечать на эти вопросы, на конкретных примерах.
Найдите эксплуатируемый «гаджет» (gadget)
К настоящему моменту у вас должно быть достаточно чёткое понимание того, как ведёт себя кэш целевого сайта, и, возможно, вы нашли интересные ошибки в построении ключа кэша. Последний шаг — определить подходящий «гаджет», который можно связать с этой ошибкой ключа кэша. Это важный навык, поскольку серьёзность любой атаки отравления веб-кэша сильно зависит от гаджета, который вы сможете эксплуатировать.
Этими гаджетами часто будут классические уязвимости на стороне клиента, такие как Reflected-XSS и OpenRedirect. Комбинируя их с Web Cache Poisoning, вы можете резко повысить серьёзность этих атак, превратив Reflected XSS в Stored. Вместо необходимости заставлять жертву перейти по специально созданному URL, ваша полезная нагрузка будет автоматически выдаваться всем, кто заходит по обычному, вполне легитимному URL.
Эти техники позволяют эксплуатировать ряд неклассифицированных уязвимостей, которые часто отклоняют как «неэксплуатируемые» и оставляют без исправления. Это включает использование динамического содержимого в файлах ресурсов и эксплойты, требующие некорректных запросов, которые браузер никогда не отправит.
Эксплуатация изъянов ключа кэша
Теперь, когда вы знакомы с методологией, давайте рассмотрим типичные ошибки ключа кэша и то, как вы можете их эксплуатировать. Мы охватим:
Неучитываемый порт
Заголовок Host часто является частью ключа кэша и, как таковой, изначально кажется маловероятным кандидатом для внедрения какой-либо полезной нагрузки. Однако некоторые системы кэширования разбирают заголовок и исключают порт из ключа кэша. В этом случае вы потенциально можете использовать этот заголовок для Web Cache Poisoning. Например, рассмотрим случай, который мы видели ранее, когда URL перенаправления динамически генерируется на основе заголовка Host. Это может позволить вам построить атаку отказа в обслуживании, просто добавив произвольный порт к запросу. Все пользователи, заходящие на домашнюю страницу, будут перенаправляться на нерабочий порт, фактически «роняя» домашнюю страницу до истечения срока действия кэша.
Такую атаку можно ещё усилить, если сайт позволяет указать нечисловой порт. Вы могли бы использовать это для внедрения полезной нагрузки XSS
Неучитываемая строка запроса
Как и заголовок Host, стартовая строка запроса обычно учитывается в ключе. Однако одно из самых распространённых преобразований ключа кэша — исключение всей строки запроса.
Выявление неучитываемой строки запроса
Если ответ явно сообщает, получили ли вы кэш-хит или нет, это преобразование относительно просто заметить — но что, если нет? Это имеет побочный эффект: динамические страницы кажутся полностью статичными, потому что сложно понять, общаетесь ли вы с кэшем или с сервером.
Чтобы определить динамическую страницу, вы обычно наблюдаете, как изменение значения параметра влияет на ответ. Но если строка запроса не учитывается, в большинстве случаев вы всё равно получите кэш-хит и, следовательно, неизменный ответ, независимо от добавляемых параметров. Очевидно, это также делает классические кэш-бастеры в строке запроса бесполезными.
К счастью, есть альтернативные способы добавить кэш-бастер, например, поместив его в учитываемый заголовок, который не вмешивается в поведение приложения. Типичные примеры:
Если вы используете Param Miner, вы также можете выбрать параметры «Add static/dynamic cache buster» и «Include cache busters in headers». Тогда он будет автоматически добавлять кэш-бастер в общие учитываемые заголовки во всех запросах, отправляемых с помощью ручных инструментов Burp.
Другой подход — посмотреть, есть ли расхождения между тем, как кэш и бэкенд нормализуют путь запроса. Путь почти гарантированно учитывается в ключе, поэтому иногда можно воспользоваться этим, чтобы отправить запросы с разными ключами, которые всё же попадают в одну и ту же конечную точку. Например, следующие записи могут кэшироваться отдельно, но обрабатываться бэкендом как эквивалент GET /: Apache: GET // Nginx: GET /%2F PHP: GET /index.php/xyz .NET GET /(A(xyz)/ Это преобразование иногда может скрывать то, что в противном случае были бы бросающиеся в глаза Reflected XSS. Если пентестеры или автоматические сканеры получают только кэшированные ответы и не понимают этого, может показаться, что на странице нет Reflected XSS.
Эксплуатация неучитываемой строки запроса
Исключение строки запроса из ключа кэша на самом деле может сделать эти Reflected XSS ещё более серьёзными.
Обычно такая атака зависела бы от того, чтобы убедить жертву перейти по специально сформированному URL. Однако Web Cache Poisoning через неучитываемую строку запроса приведёт к тому, что полезная нагрузка будет выдаваться пользователям, которые посещают в остальном совершенно обычный URL. Это может затронуть намного больше жертв без дальнейшего взаимодействия со стороны атакующего.
Неучитываемые параметры запроса
До сих пор мы видели, что на некоторых сайтах из ключа кэша исключается вся строка запроса. Но некоторые сайты исключают только конкретные параметры запроса, не относящиеся к бэкенд-приложению, например параметры для аналитики или таргетированной рекламы. Параметры UTM, такие как utm_content, — хорошие кандидаты для проверки во время тестирования.
Параметры, исключённые из ключа кэша, вряд ли окажут существенное влияние на ответ. Скорее всего, не будет полезных гаджетов, принимающих ввод из этих параметров. Тем не менее некоторые страницы обрабатывают весь URL уязвимым образом, что делает возможной эксплуатацию произвольных параметров.
Маскировка параметров для кэша (cache parameter cloaking)
Если кэш исключает безобидный параметр из ключа кэша, и вы не можете найти эксплуатируемые гаджеты на основе полного URL, можно подумать, что вы зашли в тупик. Однако именно здесь всё может стать интереснее.
Если вы сможете понять, как кэш парсит URL для идентификации и удаления ненужных параметров, вы можете найти любопытные особенности. Особый интерес представляют любые расхождения в разборе между кэшем и приложением. Это потенциально позволяет протащить произвольные параметры в логику приложения, «маскируя» их под исключаемый параметр.
Например, де-факто стандарт таков: параметр либо предшествует вопросительный знак (?), если он первый в строке запроса, либо амперсанд (&). Некоторые плохо написанные алгоритмы разбора будут воспринимать любой ?как начало нового параметра, независимо от того, является ли он первым или нет.
Предположим, что алгоритм исключения параметров из ключа кэша ведёт себя именно так, но серверный алгоритм принимает в качестве разделителя только первый ?. Рассмотрим следующий запрос:
В этом случае кэш определит два параметра и исключит второй из ключа кэша. Однако сервер не принимает второй ? как разделитель и вместо этого видит только один параметр, example, чьё значение — вся остальная строка запроса, включая нашу полезную нагрузку. Если значение example передаётся в полезный гаджет, мы успешно внедрили полезную нагрузку, не влияя на ключ кэша.
Эксплуатация особенностей разбора параметров
Похожие проблемы маскировки параметров могут возникать в обратной ситуации, когда бэкенд определяет отдельные параметры, которых кэш не видит. Фреймворк Ruby on Rails, например, интерпретирует и амперсанды (&), и точки с запятой (;) как разделители. В сочетании с кэшем, который этого не позволяет, вы потенциально можете использовать другую особенность, чтобы переопределить значение учитываемого параметра в логике приложения.
Рассмотрим следующий запрос:
Как подсказывают названия, keyed_param включается в ключ кэша, а excluded_param — нет. Многие кэши интерпретируют это лишь как два параметра, разделённые амперсандом:
keyed_param=abcexcluded_param=123;keyed_param=bad-stuff-here
После того как алгоритм разбора удалит excluded_param, ключ кэша будет содержать только keyed_param=abc. Однако на бэкенде Ruby on Rails видит точку с запятой и разделяет строку запроса на три отдельных параметра:
keyed_param=abcexcluded_param=123keyed_param=bad-stuff-here
Но теперь у нас дублируется keyed_param. Здесь вступает в игру вторая особенность. Если есть дубликаты параметров с разными значениями, Ruby on Rails отдаёт приоритет последнему вхождению. В результате ключ кэша содержит безобидное, ожидаемое значение параметра, позволяя кэшированному ответу нормально выдаваться другим пользователям. Однако на бэкенде у того же параметра совершенно иное значение — наша внедрённая полезная нагрузка. Именно это второе значение будет передано в гаджет и отражено в отравленном ответе.
Этот эксплойт может быть особенно мощным, если даёт вам контроль над функцией, которая будет выполнена. Например, если сайт использует JSONP для междоменных запросов, он часто содержит параметр callback для выполнения указанной функции над возвращёнными данными:
В этом случае вы можете использовать эти техники, чтобы переопределить ожидаемую функцию обратного вызова и выполнить произвольный JavaScript.
Эксплуатация поддержки «fat» GET-запросов
В отдельных случаях метод HTTP может не учитываться в ключе. Это может позволить вам отравить кэш с помощью запроса POST, содержащего вредоносную полезную нагрузку в теле. Тогда ваша нагрузка будет отдаваться даже в ответ на GET-запросы пользователей. Хотя такой сценарий довольно редок, иногда можно добиться аналогичного эффекта, просто добавив тело к GET-запросу, создав «fat» GET-запрос:
В этом случае ключ кэша будет основан на стартовой строке запроса, но серверное значение параметра будет взято из тела.
Это возможно только если сайт принимает GET-запросы с телом, но есть потенциальные обходы. Иногда можно вызвать обработку «fat-GET», переопределив метод HTTP, например:
До тех пор, пока заголовок X-HTTP-Method-Override не учитывается в ключе, вы можете отправить псевдо-POST-запрос, сохранив GET-ключ кэша, полученный из стартовой строки запроса.
Эксплуатация динамического содержимого в импортируемых ресурсах
Импортируемые файлы ресурсов обычно статичны, но некоторые отражают ввод из строки запроса. Это в основном считается безвредным, поскольку браузеры редко выполняют эти файлы при прямом просмотре, и у атакующего нет контроля над URL, используемыми для загрузки подресурсов страницы. Однако, сочетая это с отравлением веб-кэша, вы иногда можете внедрять содержимое в файл ресурса.
Например, рассмотрим страницу, которая отражает текущую строку запроса в операторе импорта:
Вы могли бы использовать это поведение, чтобы внедрить вредоносный CSS, который эксфильтрирует чувствительную информацию со страниц, которые импортируют /style.css.
Если страница, импортирующая CSS-файл, не указывает doctype, вы, возможно, даже сможете эксплуатировать статические CSS-файлы. При правильной конфигурации браузеры просто «прочёсывают» документ в поисках CSS, а затем выполняют его. Это означает, что вы иногда можете отравлять статические CSS-файлы, вызывая серверную ошибку, которая отражает исключённый параметр запроса:
Нормализованные ключи кэша
Любая нормализация, применяемая к ключу кэша, также может приводить к эксплуатируемому поведению. Фактически она иногда позволяет реализовать эксплойты, которые в противном случае были бы почти невозможны.
Например, когда вы находите Reflected XSS в параметре, её часто невозможно эксплуатировать на практике. Это потому, что современные браузеры обычно URL-кодируют необходимые символы при отправке запроса, а сервер их не декодирует. Ответ, который получает предполагаемая жертва, будет содержать лишь безвредную URL-кодированную строку.
Некоторые реализации кэширования нормализуют учитываемый ввод при добавлении его в ключ кэша. В этом случае оба следующих запроса будут иметь один и тот же ключ:
Это поведение может позволить вам эксплуатировать такие «неэксплуатируемые» уязвимости XSS. Если вы отправите вредоносный запрос через Burp Repeater, вы сможете отравить кэш не кодированной полезной нагрузкой XSS. Когда жертва зайдёт по вредоносному URL, полезная нагрузка всё равно будет URL-закодирована её браузером; однако после нормализации URL кэшем у неё будет тот же ключ кэша, что и у ответа, содержащего вашу не кодированную полезную нагрузку.
В результате кэш отдаст отравленный ответ, и полезная нагрузка выполнится на стороне клиента. Вам лишь нужно убедиться, что кэш отравлен в момент, когда жертва посещает URL.
Инъекция в ключ кэша (cache key injection)
Иногда вы обнаружите уязвимость на стороне клиента в учитываемом заголовке. Это также классическая «неэксплуатируемая» проблема, которую иногда можно эксплуатировать с помощью отравления кэша.
Учитываемые компоненты часто объединяются в строку для создания ключа кэша. Если кэш не реализует должное экранирование разделителей между компонентами, вы потенциально можете использовать это поведение, чтобы создать два разных запроса с одинаковым ключом кэша.
В следующем примере двойные подчёркивания используются для разделения различных компонентов в ключе кэша и никак не экранируются. Вы можете эксплуатировать это, сначала отравив кэш запросом, содержащим вашу полезную нагрузку в соответствующем учитываемом заголовке:
Если затем вы убедите жертву перейти по следующему URL, ей будет выдан отравленный ответ:
Отравление внутренних кэшей
До сих пор мы рассматривали, как можно эксплуатировать ошибки в том, как реализованы внешние веб-кэши, чтобы раскрыть расширенную поверхность атаки, скрытую в компонентах, казалось бы, учитываемых в ключе. Однако некоторые сайты реализуют поведение кэширования непосредственно в приложении в дополнение к использованию отдельного внешнего компонента. Это может давать ряд преимуществ, например избежание тех самых расхождений в парсинге, которые мы рассматривали ранее.
Поскольку эти интегрированные кэши создаются специально для конкретного приложения, это даёт разработчикам свободу более тонко настраивать их поведение. В результате такие кэши иногда могут вести себя необычным образом, которого вы обычно не увидите у более стандартизированного внешнего кэша, совместимого с несколькими приложениями. Иногда такие странности также предоставляют возможность осуществления атак отравления кэша.
Вместо кэширования целых ответов некоторые из этих кэшей разбивают ответ на повторно используемые фрагменты и кэшируют каждый из них отдельно. Например, сниппет для импорта широко используемого ресурса может храниться как самостоятельная запись кэша. Пользователи тогда могут получать ответ, состоящий из смеси содержимого от сервера, а также нескольких отдельных фрагментов из кэша.
Поскольку эти кэшированные фрагменты предназначены для повторного использования в нескольких различных ответах, концепция ключа кэша здесь по сути не применяется. Каждый ответ, содержащий данный фрагмент, будет переиспользовать тот же кэшированный фрагмент, даже если остальная часть ответа полностью отличается. В такой ситуации отравление кэша может иметь широкомасштабные последствия, особенно если вы отравите фрагмент, используемый на каждой странице. Поскольку ключа кэша нет, вы отравите каждую страницу для каждого пользователя одним единственным запросом.
Для этого часто достаточно использовать базовые техники отравления веб-кэша, например манипулирование заголовком Host.
Как выявлять внутренние кэши
Одна из проблем, создаваемых интегрированными кэшами на уровне приложения, — их бывает трудно выявить и исследовать, поскольку часто отсутствует обратная связь приложения, видимая пользователю. Чтобы распознать такие кэши, ищите несколько характерных признаков. Например, если ответ отражает смесь ввода из последнего отправленного вами запроса и ввода из предыдущего запроса — это ключевой индикатор того, что кэш хранит фрагменты, а не целые ответы. То же самое, если ваш ввод отражается в ответах на нескольких различных страницах, особенно на тех, в которые вы никогда не пытались внедрить свой ввод.
Иногда поведение кэша может быть настолько необычным, что самый логичный вывод — это должен быть уникальный специализированный внутренний кэш.
Когда сайт реализует несколько уровней кэширования, может быть сложно понять, что происходит «под капотом», и разобраться, как ведёт себя система кэширования сайта.
Безопасное тестирование внутренних кэшей
При тестировании обычных веб-кэшей советуем использовать кэш-бастер, чтобы предотвратить выдачу вашего отравленного ответа другим пользователям. Однако если интегрированный кэш не использует концепцию ключей кэша, традиционные кэш-бастеры бесполезны. Это означает, что очень легко случайно отравить кэш для реальных пользователей.
Поэтому важно сделать всё возможное для смягчения потенциального вреда другим пользователям при тестировании таких уязвимостей. Тщательно продумывайте эффект вашей внедряемой полезной нагрузки перед отправкой каждого запроса. В частности, убедитесь, что вы отравляете кэш, используя домен, которым вы управляете, а не какой-то произвольный «evil-user.net». Так вы контролируете, что произойдёт дальше, если что-то пойдёт не так.
Last updated