Статья Отравление кэша CDN

1771197364591.webp
Сегодня на столе - Cache Poisoning на уровне CDN, он же Web Cache Deception.

Звучит как что-то из разряда «ну, отравить кэш - это когда хакер заставляет сервер отдать левый контент вместо нормального». Да, но это лишь верхушка. Мы поговорим о том, как CDN, этот умный распределенный буфер, который должен делать сайты быстрыми, превращается в твоего личного разносчика вирусов. В прямом смысле. Ты заставишь инфраструктуру Facebook, Google или какого-нибудь стартапа на React с корпоративной почтой раздавать приватные данные всем подряд, потому что «ну так кэш работает, извините».

Мы пройдемся по методологии, по инструментарию, по граблям, на которые наступили умные дяди до нас. Ты узнаешь, почему ;jsessionid в конце URL может стоить миллионы долларов убытков, и как это дебажить, когда в ответе сервера нет ни одного лишнего хедера.


Глава 1. Основы: Что вообще за дичь с этим кэшем?​

1.1. Кэш - это не просто «ускоритель». Это интерпретатор реальности​

Забудь на минуту про безопасность. Посмотрим на кэш глазами архитектора. Кэширование - это фундаментальный принцип информатики: сохрани результат тяжелой работы, чтобы не переделывать её заново. В контексте веба это выглядит так: пользователь Вася запросил страницу /catalog/item/123. Сервер сходил в базу данных, в эластик, подергал легаси-системы, сгенерировал HTML. Это дорого по времени (допустим, 500 мс). Следующий пользователь Петя просит то же самое - зачем опять грузить базу? Давайте отдадим Пете тот же HTML, что и Васе, если товар не менялся.

Это идеальный мир.

В реальности всё сложнее. Ответ сервера зависит от тысячи факторов: кто запрашивает (авторизация), в какой валюте показывать цену, какая версия сайта (A/B тест), какой язык. И вот здесь начинается магия.

CDN (Content Delivery Network) в этом контексте - это не просто жесткий диск в облаке. Это распределенная система, которая принимает решение: «Могу ли я обслужить этот запрос сам, не беспокоя сервер?»

Чтобы принять это решение, CDN смотрит на ключ кэша (cache key). Обычно ключ кэша - это:
  • Полный URL (схема + хост + путь + иногда параметры запроса).
  • Плюс значение заголовка Host.
  • Плюс, возможно, некоторые другие заголовки, указанные в директиве Vary (например, Accept-Encoding или User-Agent).
Если для данного ключа в хранилище есть свежий (не истекший) объект, CDN отдает его. Если нет - летит на бэкенд.

И вот тут начинается главная уязвимость: CDN и бэкенд по-разному интерпретируют один и тот же URL. Для CDN /profile.php/nonexistent.css - это просто строка, которая заканчивается на .css. Для бэкенда (скажем, PHP-FPM) - это запрос к скрипту profile.php с дополнительной path-информацией. Это несоответствие интерпретаций - и есть наша хлебная вотчина.

1.2. HTTP-хитрости: Cache-Control, Vary и их роль в нашем деле​

Ты должен знать врага в лицо. Заголовки HTTP - это язык, на котором бэкенд говорит с CDN. И иногда бэкенд врёт, а иногда CDN его не слушает.

1.2.1. Cache-Control​

Это директива, которая приходит от бэкенда. Самые важные флаги:
  • public: Можно кэшировать всем (и браузеру, и прокси, и CDN).
  • private: Можно кэшировать только браузеру клиента. CDN должна игнорировать такой ответ и не класть в общее хранилище.
  • no-store: Вообще не сохранять на диске. Самый строгий запрет.
  • max-age=секунды: Время жизни в кэше.
  • s-maxage=секунды: То же, что max-age, но специально для общих кэшей (CDN). Если есть s-maxage, CDN игнорирует обычный max-age.
Для нас, как для атакующих, идеальный ответ от бэкенда - это Cache-Control: public с большим max-age или вообще без Cache-Control. Но чаще всего на динамических страницах мы видим Cache-Control: private, no-cache. Это стена.

Но, как я уже говорил, стена может быть картонной. Некоторые CDN (особенно старые версии или неправильно сконфигурированные) могут игнорировать private, если в правиле на уровне CDN сказано "кэшировать всё с расширением .css". Это нарушение RFC, но это жизнь.

1.2.2. Vary​

Заголовок Vary говорит CDN: "Эй, парень, ключ кэша должен включать значение такого-то заголовка из запроса". Самый частый пример: Vary: Accept-Encoding. Это значит, что для URL /script.js будет отдельный кэш для gzip-версии и для plain-версии.

Почему это важно для нас? Если бэкенд отдает Vary: Cookie, то CDN будет хранить отдельную копию страницы для каждого набора кук (читай - для каждого пользователя или сессии). Это убивает Web Cache Deception, потому что данные Васи не смешаются с данными Пети. Кэш-ключ у них разный.

Но если Vary отсутствует или включает только Accept-Language или User-Agent, то все пользователи с одинаковым языком/браузером будут получать одну копию из кэша. И если мы туда положим данные Васи, Петя с таким же браузером их и получит.

1.2.3. Age и X-Cache​

Эти заголовки говорят о том, откуда пришел ответ.
  • Age: Сколько секунд объект уже висит в кэше.
  • X-Cache: HIT/MISS (или CF-Cache-Status): Индикатор того, попал ли запрос в кэш. Наш главный маяк.

1.3. Разбираем механику: PATH_INFO, mod_rewrite и роутинг фреймворков​

Теперь давай залезем в кишки сервера приложений. От того, как он парсит URL, зависит 90% успеха атаки.

1.3.1. PHP и PATH_INFO​

Это классика жанра. PHP (как модуль Apache или PHP-FPM) поддерживает так называемый PATH_INFO. Если скрипт называется /profile.php, и ты обращаешься к /profile.php/extra/data, то:
  1. Веб-сервер (Apache/Nginx) понимает, что скрипт - это /profile.php.
  2. Остаток пути /extra/data помещается в переменную окружения PATH_INFO.
  3. PHP-скрипт может проигнорировать PATH_INFO и просто отработать как обычно.
  4. Результат: Сервер вернет страницу профиля на запрос /profile.php/extra/data.css.
Это не баг PHP, это фича для красивых URL. Но она же - наша главная дыра.

Почему это вообще существует? Исторически, для поддержки URL вида /index.php/news/123, где news/123 - это параметры для index.php. Но когда разработчик забывает об этом и оставляет PATH_INFO включенным на всех скриптах, возникает уязвимость.

Как проверить:

Bash:
curl -I https://example.com/any/existing/script.php/anything_here

Если вернулся 200 OK, а не 404 - добро пожаловать в клуб.

1.3.2. Веб-серверы (Nginx/Apache) и try_files​

Nginx часто настраивают для одностраничных приложений (SPA) примерно так:

NGINX:
location / {
    try_files $uri $uri/ /index.html;
}

Это значит: если файл по запрошенному пути не найден, отдай /index.html. Это позволяет React-роутингу работать на стороне клиента.

Как это ломается? Мы просим /dashboard/test.css. Файла test.css в папке dashboard нет. Nginx честно пытается найти, не находит и отдает /index.html (или любой другой скрипт, указанный в try_files). CDN видит запрос на .css, получает HTML-страницу (индекс) и кэширует её как CSS. Бинго!

1.3.3. Современные фреймворки (Django, Rails, Express)​

У них обычно более строгий роутинг. Например, в Django, если у тебя есть путь path('profile/', views.profile), запрос на /profile/test.css вызовет 404, если явно не описан маршрут, принимающий любой суффикс.

Но и тут есть нюансы:
  • Использование path('path:slug', views.catch_all) - это моветон, но встречается.
  • Конфиги development-режима иногда ослабляют требования.
  • Некоторые middleware могут перехватывать 404 и перенаправлять на главную, создавая иллюзию, что страница существует.

1.4. Роль браузера и "свежесть" кэша​

Не забывай, что есть ещё и браузерный кэш. Но нас интересует только CDN. Когда мы говорим о Web Cache Deception, мы атакуем именно общий кэш (shared cache), который находится между клиентами.

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

Поэтому в реальных атаках часто используются:
  • Редкие имена файлов: /profile.php/ux2f9e1.css - чтобы никто случайно не закэшировал мусор.
  • Параметры, игнорируемые при кэшировании: Если CDN исключает параметры из ключа кэша (что часто делают для статики), то можно добавить ?cache_buster=123 и быть уверенным, что это новый ключ.
Мы разобрали, что:
  1. Кэш - это не просто ускоритель, а независимый слой, принимающий решения на основе ключа и заголовков.
  2. Cache-Control - язык, на котором бэкенд говорит с CDN, но CDN может ослушаться.
  3. Vary - директива, расширяющая ключ кэша. Враг атакующего, если там Cookie.
  4. PATH_INFO и try_files - главные технические причины, почему бэкенд обрабатывает запрос не так, как ожидает CDN.
  5. X-Cache и Age - наши индикаторы, позволяющие увидеть, сработала атака или нет.
Теперь, когда у тебя в голове сложилась эта схема, мы можем двигаться дальше. Следующие главы будут посвящены тому, как давить на эти точки, используя не только классические методы, но и извращенные техники обхода защит.

Запомни: Web Cache Deception - это атака не на алгоритмы шифрования, а на несоответствие интерпретаций. И пока люди будут писать код, а CDN - тупо смотреть на расширение файла, мы будем находить эти дыры.


Глава 2. Анатомия обмана: Почему это вообще работает?​

Итак, у нас есть три актора: Клиент (жертва или атакующий), CDN (прокси-сервер с амбициями) и Origin (бэкенд, который думает, что он главный). Их танец определяет всё. Давай разберем партии каждого из них в идеальной атаке Web Cache Deception, а потом посмотрим, как этот танец сбивается и какие па можно подстроить.

2.1. Схема идеальной атаки: Полный цикл​

Я хочу, чтобы ты визуализировал это. Нарисуй в голове ось времени.

Шаг 0: Подготовка.
Атакующий разведывает цели. Он находит эндпоинт, который:
  • Отдает приватные данные (например, ).
  • Требует авторизации (кука сессии).
  • В обычном состоянии не кэшируется (MISS на каждый запрос).
  • При этом бэкенд игнорирует лишние пути (поддержка PATH_INFO или кривой роутинг).
Шаг 1: Формирование снаряда.
Атакующий создает вредоносный URL. Он берет базовый приватный эндпоинт и приделывает к нему суффикс, имитирующий статический файл: /account/info/static/style.css. Почему .css? Потому что CSS, JS, PNG - это священные коровы для CDN. Их кэшировать можно и нужно. В большинстве конфигураций CDN это разрешено по умолчанию.

Шаг 2: Доставка снаряда.
Атакующий отправляет эту ссылку жертве любым способом: email, мессенджер, форум, третий глаз. Важно, чтобы жертва перешла по ссылке, будучи залогиненной в целевой системе. Механика перехода может быть разной:
  • Прямой клик.
  • Загрузка через iframe на подконтрольном атакующему сайте.
  • Использование в качестве источника изображения (img src=" ") - тогда браузер жертвы молча сходит за этим URL, даже не показывая, что что-то загружает. Это особенно опасный вектор, потому что жертва даже не подозревает, что участвует в атаке.
Шаг 3: Запрос жертвы.
Браузер жертвы (с куками, с заголовками, с User-Agent) шлет GET-запрос на . Запрос прилетает на ближайший PoP (Point of Presence) CDN.

Шаг 4: Решение CDN.
CDN смотрит на URL. Видит путь, заканчивающийся на .css. Сверяется со своими правилами кэширования. Правила говорят: "Все .css файлы нужно кэшировать, если можно". CDN проверяет свой локальный кэш по ключу (обычно схема + хост + путь). Ключ: . В кэше пусто (MISS). CDN принимает решение: "Лечу на Origin, забираю контент, сохраню его и отдам пользователю".

Важный технический нюанс: На этом этапе CDN не анализирует содержимое. Она не знает, что это HTML. Она знает только то, что это ответ на запрос статического файла. Она слепа.

Шаг 5: Запрос на Origin.
CDN транслирует запрос на бэкенд-сервер. Важно: CDN обычно не меняет путь (path) запроса, если это не настроено специально (например, через rewrite rules). Поэтому на Origin приходит точно такой же URL: /account/info/static/style.css.

Шаг 6: Обработка на Origin.
А вот здесь происходит магия, ради которой мы собрались. Origin (веб-сервер + бэкенд) имеет свою логику интерпретации пути. И эта логика, в отличие от логики CDN, не ограничивается последним сегментом.
  • Сценарий А (PATH_INFO): Если бэкенд на PHP, а веб-сервер - Apache с AcceptPathInfo On, то сервер видит, что скрипта /account/info/static/style.css не существует. Но он замечает, что часть пути /account/info/static/ ведет к существующей директории или скрипту? Не совсем. Он ищет скрипт по частям. Допустим, скрипт /account/info.php существует. Тогда сервер может интерпретировать запрос как: скрипт /account/info.php, а /static/style.css - это PATH_INFO. Он выполняет info.php, который, будучи вызванным без параметров (или игнорируя PATH_INFO), отдает страницу профиля.
  • Сценарий Б (SPA Fallback): Nginx с конфигом try_files $uri $uri/ /index.html; пытается найти файл по пути /account/info/static/style.css. Не находит. Он пытается найти директорию - не находит. Тогда он отдает содержимое /index.html, который часто является оберткой для одностраничного приложения. Но если на бэкенде index.html не требует авторизации, а API внутри подгружается отдельно, то может отдаться главная страница. Но нам нужен профиль. Сложнее. Однако, если try_files ведет на конкретный скрипт, например, /app.php, который обрабатывает все запросы, то ситуация похожа на PHP.
  • Сценарий В (Кривой роутинг фреймворка): В Express.js есть возможность определить маршрут app.get('/account/info/*', handler). Если разработчик случайно (или по незнанию) использовал такой паттерн, то хендлер, отвечающий за страницу профиля, вызовется и на запрос /account/info/static/style.css. Хендлер отработает, отдаст HTML с данными пользователя.
Шаг 7: Ответ Origin.
Origin генерирует ответ. Это HTML-страница профиля пользователя, с его именем, email, возможно, скрытыми полями. Вместе с ответом идут HTTP-заголовки. Вот тут самый критический момент для успеха атаки. Какие заголовки отдаст Origin?
  • Плохие для нас: Cache-Control: private, no-cache, no-store. Если CDN уважает RFC (а должна), она получив такой заголовок, не должна кэшировать ответ. Даже если запрос был на .css.
  • Хорошие для нас: Отсутствие Cache-Control, или Cache-Control: public, или Cache-Control: max-age=3600. Либо наличие только Cache-Control: no-cache (который разрешает кэширование, но требует проверки устаревания - но многие CDN все равно кэшируют). Или самый сладкий вариант: Cache-Control: s-maxage=3600 (разрешено кэшировать на CDN).
Также важна директива Vary. Если Vary: Cookie - нам конец, ключ кэша будет включать куку, и данные Васи не будут доступны Пете.

Шаг 8: Обработка ответа CDN.
CDN получает ответ от Origin. Смотрит на заголовки. Если они разрешают кэширование (или если у CDN есть принудительное правило кэшировать CSS независимо от заголовков), она сохраняет этот HTML-ответ в свое хранилище, привязывая его к ключу . После этого она отправляет ответ жертве. Жертва видит свою страницу профиля (или, в случае с img src, просто грузит невидимую картинку, но страница все равно закэшировалась).

Шаг 9: Сбор урожая.
Атакующий теперь (или через некоторое время) отправляет GET-запрос на тот же URL: . Он делает это без кук авторизации, или с куками другого пользователя. Запрос снова прилетает в CDN. CDN проверяет кэш по тому же ключу. Находит сохраненный HTML-ответ. Видит, что срок жизни не истек. Отвечает с заголовком X-Cache: HIT. И атакующий получает в ответе страницу профиля жертвы со всеми её приватными данными.

2.2. Рентгеновский снимок: Критические точки атаки​

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

2.2.1. Точка принятия решения CDN: Формирование ключа кэша​

CDN должна решить, что именно кэшировать. Это решение базируется на ключе кэша. Понимание того, как строится этот ключ на конкретной CDN, - это 50% успеха.
  • Что входит в ключ по умолчанию?
    • Метод HTTP (обычно только GET, HEAD).
    • Полный URL (протокол, хост, порт, путь).
    • Параметры запроса (query string) - иногда, но не всегда. Cloudflare, например, по умолчанию исключает параметры из кэш-ключа для статики, чтобы повысить эффективность. Это значит, что style.css?foo=1 и style.css?foo=2 будут считаться одним объектом. Для WCD это плюс - мы можем добавить рандомный параметр, чтобы создать новый объект, если не уверены, что кэш чист.
  • Что может расширять ключ (Vary)?
    Заголовок Vary от бэкенда - это инструкция CDN. Самые опасные для нас значения:
    • Vary: Cookie - разделяет кэш по кукам. Киллфича.
    • Vary: Authorization - разделяет по токенам авторизации.
    • Vary: User-Agent - разделяет по браузерам. Это не смертельно, но усложняет атаку: нам нужно знать User-Agent жертвы, чтобы получить её кэш.
  • Как это тестировать?
    Посылаем запрос с разными значениями потенциальных разделителей (User-Agent, куки) и смотрим, меняется ли X-Cache с HIT на MISS для одного и того же URL. Если при смене User-Agent мы все еще получаем HIT - значит, Vary: User-Agent не используется. Инструмент Param Miner отлично помогает выявить такие вещи.

2.2.2. Точка принятия решения Origin: Парсинг пути​

Это наша главная вотчина. Нужно понять, как именно бэкенд превращает строку запроса в вызов функции.
  • Веб-сервер (Apache/Nginx):
    • Apache + mod_php: AcceptPathInfo On - наша цель. По умолчанию может быть включен. Проверяется простым тыком.
    • Nginx + PHP-FPM: fastcgi_split_path_info - директива, которая явно включает поддержку PATH_INFO. Если она есть и правильно настроена, мы можем использовать PATH_INFO. Если нет - PATH_INFO не передастся в PHP.
    • Nginx try_files: Ищем конфиги, где в конце цепочки стоит index.php или index.html. Это частая причина WCD в SPA.
  • Языки и фреймворки:
    • PHP: Переменная $_SERVER['PATH_INFO'] содержит лишний путь. Если скрипт её игнорирует - уязвимость есть. Если использует для маршрутизации - тоже может быть уязвимость, но более сложная.
    • Python (Django/Flask): Встроенные роутеры обычно строги. Но если используется path конвертер, принимающий любую строку (path:slug), то можно. Или если на уровне WSGI-сервера (gunicorn/uwsgi) настроена особая обработка.
    • Node.js (Express): Порядок маршрутов важен. Если есть маршрут app.use('*', handler), он сработает на любой запрос. Если он стоит после маршрута /account/info, то запрос на /account/info/test.css сначала проверит точное совпадение, не найдет, и упадет в *. Хендлер для * должен либо отдавать 404, либо обрабатывать. Если он отдает главную страницу - мы в деле.

2.2.3. Точка перехвата: Заголовки кэширования​

Даже если бэкенд обработал запрос и отдал HTML, он может убить нашу атаку правильными заголовками.
  • Cache-Control: private - главный враг. CDN должна его слушаться. Но! Многие CDN позволяют настраивать "Edge Cache TTL" в обход заголовков. Например, в Cloudflare есть правило "Cache Level: Ignore Origin Cache Control". Если оно включено для этого URL, то даже private не спасет. Это редкая, но жирная конфигурационная ошибка.
  • Cache-Control: no-cache - не запрещает кэширование, а требует каждый раз проверять у бэкенда, не изменился ли контент (условные запросы с If-Modified-Since). Многие публичные кэши все равно могут сохранить ответ и использовать его, игнорируя семантику no-cache. Так что это не панацея.
  • Pragma: no-cache - устаревший заголовок для HTTP/1.0.
  • Expires - старый способ задания времени жизни. Если стоит дата в прошлом - кэшировать нельзя.

2.2.4. Точка исполнения: Метод доставки​

Как заставить жертву сходить по нашему URL?
  • Прямая ссылка: Самый простой, но и самый заметный. Жертва видит странный URL в адресной строке.
  • Скрытый запрос (img, script, iframe): Это наш конек. Встраиваем в свою страницу (или в комментарий на форуме, который поддерживает HTML) тег img src=" " width="0" height="0" /. Браузер жертвы, заходя на нашу страницу, молча грузит этот URL. Жертва ничего не подозревает, а кэш пополняется.
  • HTTP/2 Push (устарело, но было): Раньше можно было затолкать ресурс через Server Push, сейчас почти неактуально.
  • WebSocket и другие каналы: Теоретически, если есть уязвимость типа XSS, можно делать запросы от имени жертвы, но это уже другая история.

2.3. Глубокая разведка: Как составить карту местности​

Прежде чем дергать курок, нужно понять, по какому полю мы идем. Вот методология разведки для WCD:
  1. Сбор эндпоинтов: Используй Spider в Burp, Wayback Machine, gau (tool by tomnomnom), чтобы собрать все уникальные пути сайта. Особенно интересуют те, которые содержат слова: profile, account, dashboard, settings, api/user, private.
  2. Проверка авторизации: Для каждого эндпоинта проверь, требует ли он авторизации. Самый простой способ: запрос без кук должен возвращать 401, 302 на логин, или страницу логина.
  3. Проверка обработки лишнего пути: Для каждого защищенного эндпоинта попробуй добавить /test.js, /test.css, /nonexistent и посмотри на ответ. Нам нужен 200 OK с тем же содержимым, что и у основного эндпоинта.
  4. Проверка кэширования для лишнего пути:
    • Запрос 1 (с кукой жертвы): фиксируем X-Cache: MISS.
    • Ждем 1-2 секунды.
    • Запрос 2 (без куки, или с другой кукой) на тот же URL.
    • Смотрим результат: если X-Cache: HIT и содержимое совпадает - BINGO.
  5. Анализ заголовков: Смотрим Cache-Control, Vary. Если Cache-Control запрещает кэширование, но при этом X-Cache: HIT все равно приходит - значит, CDN настроена игнорировать бэкенд. Это отдельная уязвимость.
  6. Проверка глобальности кэша: Попробуй закэшировать через один VPN (например, Европа), а считать через другой (США). Если X-Cache: HIT приходит на другом континенте - кэш глобален, и масштаб бедствия огромен.

2.4. Практический пример: Исследование черного ящика​

Представь, что мы тестируем сайт redacted.com. У нас есть аккаунт. Мы находим страницу /me/settings. Без куки - редирект на /login. С кукой - отдает HTML с настройками.
  1. Пробуем /me/settings/test.css. Возвращает 404? Плохо.
  2. Пробуем /me/settings/../settings/test.css. Возвращает 200? О, уже интересно. Значит, путь обработан, но сервер срезал .. и отдал страницу настроек.
  3. Проверяем кэширование:

    Bash:
    curl -k -i -H "Cookie: session=my_session" https://redacted.com/me/settings/../settings/test.css

    Смотрим X-Cache: MISS. Cache-Control: public, max-age=300. Отлично.

    Bash:
    curl -k -i https://redacted.com/me/settings/../settings/test.css
    Ждем X-Cache: HIT и видим свои же настройки, но уже без куки. Уязвимость подтверждена.
Мы нашли точку, где .. в пути не был нормализован CDN (или был нормализован уже на бэкенде), что позволило обойти какие-то проверки.
Запомни, как отче наш: Web Cache Deception - это атака на разрыв между восприятием пути CDN и Origin. CDN смотрит на расширение. Origin смотрит на логику маршрутизации. Наша задача - найти эндпоинты, где Origin не ругается на лишний путь, и заставить CDN поверить, что это статика.

1771233924565.webp

Глава 3. Инструментарий хакера: С чем идти на промысел​

Хватит теории. Давай о насущном. Что нам понадобится, чтобы не просто тыкать пальцем в небо, а методично выносить кэш?

3.1. Burp Suite - наше всё (Pro версия рулит)​

Burp Suite - это швейцарский нож. Бесплатной Community версии может не хватить для некоторых фич, но для начала - окей.
  • Repeater: Для ручного дерганья ручек и анализа хедеров.
  • Intruder (Pro): Для фаззинга заголовков типа X-Forwarded-Scheme, X-Forwarded-Host и проверки, влияют ли они на кэш-ключ. В бесплатной версии это медленно, но можно скриптами на Python.
  • Project Discovery: Иногда я предпочитаю связку Caido + простые скрипты, но Burp - стандарт индустрии.

3.2. Param Miner (Расширение для Burp) - MUST HAVE​

Автор - Джеймс Кеттл (Albinowax), который, по сути, написал библию по кэш poisoning. Param Miner автоматически проверяет, какие параметры и заголовки влияют на кэш. Он тупо перебирает кучу скрытых хедеров (X-Original-URL, X-Rewrite-URL, X-Forwarded-Server и т.д.) и смотрит, меняется ли ответ и кэшируется ли он.

Ставим обязательно. Это экономит часы ручного перебора.

3.3. cURL - наша любовь и боль​

Никаких браузеров для начального тестирования. Только консоль. Ты должен видеть каждый хедер, каждый байт ответа.

Базовый синтаксис:

Bash:
curl -k -i -H "Host: example.com" https://target.com/profile.php/test.js
  • -i: Показывать хедеры ответа.
  • -k: Игнорировать проблемы с SSL (на тестовых стендах бывает).
  • -H: Добавить свой заголовок.

3.4. Приватные прокси и гео-распределение​

Помни: CDN глобальна. То, что закэшировано на ноде в Европе, может не быть закэшировано в США. Если ты тестишь с одной точки, ты можешь не увидеть уязвимость, потому что твой запрос попал на свежую ноду. Лучше иметь пул прокси в разных регионах или использовать VPN, чтобы переключаться между странами и проверять, действительно ли кэш глобальный.

3.5. Инструменты для анализа кэш-хедеров​

Учи матчасть. Ответ сервера может содержать кучу подсказок:
  • X-Cache: HIT / X-Cache: MISS (Cloudflare, Akamai, Fastly). Золотой стандарт. Видишь HIT - значит, контент пришел из кэша.
  • CF-Cache-Status (Cloudflare).
  • Age: Сколько секунд назад объект был сохранен в кэше.
  • Cache-Control: public, private, max-age, s-maxage. s-maxage - это специально для CDN (shared caches). Если стоит Cache-Control: private, CDN не должна кэшировать. Но мы проверим, слушается ли она?
  • Vary: Указывает, что кэш-ключ зависит от каких-то хедеров (например, Vary: User-Agent - тогда для каждого браузера своя копия). Это может быть вектором для атаки.

Глава 4. Практика: Ищем уязвимости с упорством сапера​

Теперь самое мясо. Как искать эту ебаторию в дикой природе. Мы пройдемся по шагам - от разведки до эксплуатации.

Шаг 1: Выбор цели и картографирование​

Мы ищем страницы с динамическим контентом, которые:
  1. Доступны только авторизованным пользователям (личный кабинет, дашборд, настройки профиля).
  2. Не имеют жестких заголовков Cache-Control: private или Cache-Control: no-store.
  3. (Опционально) Имеют предсказуемый URL (например, /account, /profile, /user/info).
Нужно понять архитектуру URL. Как твой бэкенд обрабатывает пути?
  • PHP (index.php)? Скорее всего, поддерживает PATH_INFO - то есть /index.php/extra/stuff все равно выполнит index.php, а /extra/stuff будет доступно в переменной $_SERVER['PATH_INFO']. Это идеальный вектор.
  • Python Flask/Django? У них роутинг жестче. /profile/123/ - это одно. /profile/123/test.js, скорее всего, вызовет 404, если такой роут не описан. Но если описан роут /profile/int:id&, а ты сунешь /profile/123/test.js - получишь 404, и кэш не сохранит. Но если разработчик небрежно написал роут /profile/path:anything - мы в дамках.
  • Node.js (Express)? Похоже на Flask. Нужно смотреть, как парсятся маршруты.
  • Apache/Nginx алиасы? Могут быть косяки с try_files.
Как проверить? Берем любой рабочий эндпоинт, например, /api/v1/user. Пытаемся добавить к нему /test.js, /test.css, /nonexistent и смотрим, что вернет сервер. Если вернул 200 OK с контентом /api/v1/user - мы нашли кандидата.

Шаг 2: Анализ кэширования целевого URL​

Нам нужно понять, кэшируется ли вообще целевой URL.
  1. Шлем запрос (с авторизацией, если нужно) к оригинальному URL, например, .
    Bash:
    curl -k -i -H "Cookie: session=..." https://example.com/dashboard
  2. Смотрим хедеры. Видим X-Cache: MISS, CF-Cache-Status: MISS, или вообще нет кэш-хедеров.
  3. Шлем тот же запрос еще раз. Если видим X-Cache: HIT - это плохой знак (для нас). Значит, динамическая страница уже кэшируется CDN. Это само по себе уязвимость (если там приватные данные), но это не совсем Deception, это просто Misconfiguration. Нам нужно, чтобы оригинальная страница НЕ кэшировалась.
  4. Если после второго раза MISS - отлично. Origin, скорее всего, отдает заголовки, запрещающие кэш (например, Cache-Control: private), или CDN настроена не кэшировать HTML. Это наш идеальный сценарий: страница приватная и не кэшируется в обычном виде.

Шаг 3: Проверка поведения с "левым" расширением​

Теперь самое интересное. Добавляем к нашему эндпоинту мусорный путь со статическим расширением.

Bash:
curl -k -i -H "Cookie: session=..." https://example.com/dashboard/test.css
Анализируем ответ:
  1. Статус ответа: Если сервер вернул 404 или 403 - увы, этот вектор не пройдет. Сервер ругается на несуществующий файл.
  2. Статус ответа: Если сервер вернул 200 OK и содержимое страницы /dashboard - мы на шаге к успеху. (Например, потому что бэкенд проигнорировал /test.css).
  3. Кэш-хедеры в ответе:Смотрим, что отдал Origin.
    • Если Cache-Control: private, no-cache, no-store - CDN, скорее всего, не кэширует, даже если мы просим .css. Но не факт. Некоторые CDN могут быть настроены принудительно кэшировать статику, игнорируя хедеры. Это редкий, но опасный случай. Продолжим проверку.
    • Если Cache-Control пустой или public, или есть max-age - отлично, CDN закэширует.

Шаг 4: Проверка попадания в кэш​

Теперь нам нужно доказать, что ответ для /dashboard/test.css действительно сохраняется.
  1. Шлем запрос с кукой сессии жертвы. Фиксируем X-Cache: MISS.
  2. Ждем пару секунд.
  3. Шлем тот же запрос без куки (или с кукой другого пользователя) на .
  4. Смотрим результат.
    • Если мы получили содержимое профиля первой жертвы и хедер X-Cache: HIT - БИНГО! Мы только что выпили чужой чай.
    • Если получили 401 Unauthorized или редирект на логин - значит, либо CDN не закэшировала, либо бэкенд на этом URL требует авторизацию в любом случае, и CDN это учла. Пробуем другие расширения (.js, .png, .gif, .txt, .map), другие пути (/dashboard/../test.css), другие методы (GET, POST - но POST редко кэшируют).

Шаг 5: Игра с заголовками и Vary​

Иногда кэш-ключ зависит от заголовков. CDN может хранить отдельную копию для каждого User-Agent. Это усложняет атаку, но не делает её невозможной. Нам нужно узнать, от чего именно зависит ключ.

Используем Param Miner или ручной перебор популярных заголовков: User-Agent, Accept-Language, Accept-Encoding, Origin, Referer.

Допустим, мы выяснили, что CDN использует Vary: User-Agent. Это значит, что если жертва использует Chrome, а мы атакуем с Firefox, мы не увидим её кэш. Нам нужно подделать User-Agent жертвы. А это уже социальная инженерия или подбор.


Глава 5. Эскалация привилегий и обход защиты​

Найти уязвимость - это полдела. Нужно понять, как ей воспользоваться с максимальным эффектом. "Украл профиль - сдал баг"? . Нет, мы пойдем дальше.

5.1. Атака на API-эндпоинты​

Классический WCD нацелен на HTML-страницы. Но современный веб - это API. Представь, что есть эндпоинт /api/user/me, который отдает JSON с данными пользователя. Он не кэшируется (потому что приватный). Но если мы попробуем /api/user/me/test.json? Если бэкенд схавает путь, а CDN решит, что это статический JSON, и закэширует его - мы получим кэшированный JSON с данными юзера. Это может быть еще вкуснее, потому что JSON часто парсится другими сервисами и содержит больше данных (email, айдишники, токены).

5.2. Bypass: Когда стоит Cache-Control: Private​

Иногда разработчики ставят правильные хедеры. Но CDN может быть настроена принудительно кэшировать определенные типы файлов. Есть такое понятие как "Edge Cache TTL" в настройках Cloudflare. Если админ поставил правило "кэшировать все JS 120 минут", то даже если бэкенд шепчет "не надо, это приватно!", Cloudflare может послать бэкенд лесом и сохранить ответ. Это называется "CDN override". Мы должны это проверять.

Как проверить:
  1. Находим эндпоинт, который отдает Cache-Control: private на /dashboard/test.js.
  2. Шлем запрос без сессии через несколько минут. Если получаем контент - CDN забила на хедеры. Profit.

5.3. Bypass: Очистка кэша и Time-of-Click​

Как доставить ссылку жертве?
  • Прямая ссылка: "Привет, глянь крутой отчет по ссылке". Жертва переходит -> кэшируется.
  • Если жертва уже посещала этот URL раньше, кэш может быть пуст или свеж. Нужно убедиться, что мы первые, кто просит этот URL. Можно использовать рандомные параметры: /dashboard/test.css?cachebuster=12345 - если CDN включает параметры в ключ кэша, то каждый раз будет новый объект. А если не включает (а часто не включает для статики, чтобы не засорять кэш), то параметр игнорируется, и мы получаем тот же объект. Нужно знать политику.
  • Используй технику "Cache Warm-up": сначала сам сходи на URL (имитируя жертву), потом отправляй жертву. Но это сложно, потому что твой IP может быть забанен.

5.4. Атака через XSS в кэше (Cache Poisoning + Deception гибрид)​

Это высший пилотаж. Представь, что страница профиля уязвима для Reflected XSS, но параметр отражается в теле. Обычно атакующий должен заставить жертву перейти по ссылке с XSS-пейлоадом, и он сработает один раз. Но мы можем закэшировать страницу с XSS!

Схема:
  1. Находим страницу, которая отражает параметр ?search= в HTML.
  2. Проверяем, что страница кэшируется (или мы можем заставить её кэшироваться через WCD, добавив .css).
  3. Создаем URL:
  4. Жертва переходит по ссылке. Бэкенд выполняет profile.php, видит параметр search с скриптом, отражает его. Генерирует HTML. CDN кэширует этот HTML под именем /profile.php/search.css?search=.... (Ключ кэша либо включает параметры, либо нет - нам нужно, чтобы включал, либо чтобы параметр был частью пути).
  5. Теперь ЛЮБОЙ пользователь, который зайдет на этот URL (или даже на URL без параметра, если ключ кэша его не учитывает), получит закэшированную страницу с XSS и выполнит скрипт.
Это уже persistent XSS через кэш. Очень мощно.


Глава 6. Реальные кейсы и хроники неудач (Wardriving)​

Давай пройдемся по тому, что уже было. Это не просто история, это хлеб наш насущный. Ошибки гигантов.

6.1. Классика: PHP и PATH_INFO​

Самый жирный куш. WordPress, старые форумы на phpBB, кастомные CMS. Если на сайте используется PHP и URL типа index.php/some/path работает, считай, что пол сайта уже лежит. Были баги в репортах HackerOne, где люди сливали кэшированные профили крупных платформ именно так. Исправлялось это выпиливанием поддержки PATH_INFO в конфиге Apache ( AcceptPathInfo Off) или настройкой роутинга в PHP-фреймворках.

6.2. GitHub Pages и Jekyll​

Был забавный случай (кажется, в 2018-м), когда на GitHub Pages обнаружили WCD. Если у тебя был Jekyll сайт с плагинами, генерирующими динамический контент (например, каталог товаров), то можно было добавить /style.css в конец URL и получить закэшированную HTML-страницу на CDN GitHub'а. Это позволяло обходить авторизацию на приватных страницах, если они были опубликованы по ошибке как публичные, но с доступом по ссылке. GitHub это быстро прикрыл.

6.3. Cloudflare и "Always Online"​

Фича Cloudflare "Always Online" может кэшировать контент, даже если сервер упал. Но иногда она кэширует то, что не надо. В связке с WCD это может привести к тому, что приватные данные будут висеть в кэше вечно, даже если оригинал уже исправлен.

6.4. Мой личный опыт (Анонимно, конечно)​

Как-то раз на баг-баунти программе одного крипто-стартапа я заметил, что их API отдает данные пользователя на /api/v2/user. Обычный запрос кэшировался с Cache-Control: private. Я начал тыкать пути. Обнаружил, что бэкенд на Node.js имел кривой роутер: если запросить /api/v2/user/avatar.png, он не находил роут /api/v2/user/avatar.png, но у него был middleware, который ловил все 404 и редиректил на /api/v2/user, отдавая данные пользователя с кодом 200 (костыль для SPA). CDN (Akamai) видел запрос на .png, кэшировал ответ (200 OK с JSON) и раздавал его всем. Это был фейспалм. Я получил $5000 и фикс от стартапа.


Глава 7. Автоматизация поиска: Пишем свой сканер (концепт)​

Ручной поиск - это круто, но скучно. Давай набросаем идею простого скрипта на Python, который поможет автоматизировать первичную проверку. Не для того, чтобы тупо запустить и уйти, а чтобы скрипт генерировал цели для ручного анализа.

Мы будем использовать requests с сессией. Идея:
  1. Берем список URL-эндпоинтов, которые требуют авторизации (можно собрать через спайдер в Burp или через Wayback Machine).
  2. Для каждого эндпоинта пробуем добавить несколько расширений (.css, .js, .png, .svg).
  3. Проверяем, отличается ли ответ по содержимому от 404 или логина.
  4. Проверяем кэш-статус.

Python:
import requests
import hashlib
import time

# Настройки
TARGET_URL = "https://example.com"
ENDPOINTS = ["/dashboard", "/profile", "/api/user/me", "/account"] # Список целей
EXTENSIONS = [".css", ".js", ".png", ".jpg", ".gif", ".svg", ".txt", ".json"]
COOKIES = {"session": "value_of_victim_session"} # Сессия жертвы

HEADERS = {
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}

PROXIES = {
    # "http": "http://127.0.0.1:8080",
    # "https": "http://127.0.0.1:8080", # Для отладки через Burp
}

s = requests.Session()
s.cookies.update(COOKIES)
s.headers.update(HEADERS)
s.proxies.update(PROXIES)

def test_cache_deception(endpoint):
    for ext in EXTENSIONS:
        test_url = f"{TARGET_URL}{endpoint}{ext}"
        print(f"[*] Testing {test_url}")

        # 1. Запрос с сессией (должен быть MISS)
        resp1 = s.get(test_url, allow_redirects=False)
        cache1 = resp1.headers.get('X-Cache', 'Unknown') # Меняй под свою CDN

        # Если ответ 404 или 403, смысла нет
        if resp1.status_code in [404, 403, 401, 500]:
            print(f"[-] {test_url} returned {resp1.status_code}, skipping.")
            continue

        # Сохраняем хеш содержимого для сравнения
        content_hash1 = hashlib.md5(resp1.text.encode()).hexdigest()
        print(f"[+] First request: {resp1.status_code}, Cache: {cache1}, Hash: {content_hash1}")

        # Ждем немного, чтобы кэш успел сохраниться
        time.sleep(2)

        # 2. Запрос БЕЗ сессии (другой пользователь)
        s2 = requests.Session() # Чистая сессия
        s2.headers.update(HEADERS)
        resp2 = s2.get(test_url, allow_redirects=False)
        cache2 = resp2.headers.get('X-Cache', 'Unknown')
        content_hash2 = hashlib.md5(resp2.text.encode()).hexdigest() if resp2.text else "no_content"

        print(f"[+] Second request (no auth): {resp2.status_code}, Cache: {cache2}, Hash: {content_hash2}")

        # Анализ
        if resp2.status_code == 200 and content_hash1 == content_hash2:
            if 'HIT' in cache2.upper():
                print(f"!!! POTENTIAL CACHE DECEPTION FOUND: {test_url}")
                with open("potential_wcd.txt", "a") as f:
                    f.write(f"{test_url} - {resp1.status_code} - {resp2.status_code}\n")
        else:
            print(f"[-] Not vulnerable or cache missed: {test_url}")

        time.sleep(1) # Вежливость

if __name__ == "__main__":
    for ep in ENDPOINTS:
        test_cache_deception(ep)

Важно: Этот скрипт - примитив. Он не учитывает Vary, редиректы, разные типы CDN, параметры в URL и динамический контент, который меняется при каждом запросе. Он лишь отправная точка. Не будь скрипткидди, дорабатывай под конкретную цель.


Глава 8. Защита: Как не облажаться админу​

Ну и на сладкое - взгляд со стороны защиты. Чтобы мы, как белые хакеры, могли давать рекоммендации, а не просто пугать.

8.1. Конфигурация сервера (Origin)​

Это база. Не давай бэкенду игнорировать лишние пути, если это не задумано.
  • PHP: Установи cgi.fix_pathinfo=0 и AcceptPathInfo Off в Apache/Nginx. Не позволяй выполнять скрипт, если после него идет какой-то путь.
  • Flask/Django: Используй строгие маршруты. Убедись, что путь /profile/ и /profile/test.js ведут на разные контроллеры, либо второй возвращает 404. Не используй path:anything без необходимости.
  • Nginx: Проверь конфиги try_files. Часто ошибка возникает, когда try_files пытается найти файл, не находит и отдает контроллеру, а контроллер отдает страницу, думая, что это запрос к корню.

8.2. Заголовки Cache-Control​

Это святое.
  • Для всех динамических страниц, которые содержат приватные данные, всегда отдавай:
    Cache-Control: no-store, private
    Pragma: no-cache
  • no-store запрещает сохранение в любом постоянном хранилище. private запрещает кэширование на публичных CDN (должен запрещать, но мы помним про overrides).
  • Для статики (CSS, JS, картинки) используй public, max-age=31536000, immutable. Это поможет CDN понять, что кэшировать можно и нужно.

8.3. Настройка CDN​

Самая частая проблема - неправильные правила кэширования на уровне CDN.
  • Не кэшируй HTML по умолчанию. Точка. Если тебе нужно кэшировать HTML для неавторизованных пользователей (например, главная страница блога), делай это выборочно, на основе наличия cookies или заголовка авторизации.
  • Используй "Cache Keys" с умом. Включай в ключ кэша заголовки Cookie или Authorization, если контент от них зависит. Это снизит производительность, но повысит безопасность. Cloudflare позволяет настраивать "Cache Key" включая определенные заголовки или куки.
  • Настрой расширения файлов. Убедись, что твоя CDN кэширует только те расширения, которые нужны. Если какой-то эндпоинт API отдает JSON, а у него в правиле стоит "кэшировать JSON", это может быть проблема. Лучше кэшировать только в конкретных папках (/static/, /assets/).

8.4. Регулярный аудит​

Используй те же инструменты, что и хакеры. Прогоняй свои эндпоинты через описанный выше скрипт. Смотри логи CDN: нет ли необычно высокого количества HIT на страницах, которые должны быть приватными.


Философия уязвимости​

Мы начали с классики - PHP и PATH_INFO. Казалось бы, древняя история. Но нет. Сегодня на сцену выходят JAMStack, Next.js с его Middleware, Cloudflare Workers и прочие «умные» штуки. И динамика усложняется.

Раньше было просто: CDN тупая, бэкенд тупой, мы умные. Теперь CDN может выполнять код (Edge Computing). Она может сама принимать решения: «О, у пользователя есть кука авторизации, не буду кэшировать». Но если разработчик, который пишет код для Cloudflare Workers, допустит ошибку в логике - например, будет проверять наличие куки, но не будет проверять наличие лишнего пути в URL - мы получим ту же уязвимость, но на новом витке эволюции.

Пока существует кэширование на основе URL, и пока бэкенд (или edge-функция) может игнорировать часть этого URL, будет существовать и атака на несоответствие интерпретаций. Это не лечится полностью. Это лечится только внимательностью.
Мы, хакеры, - не вредители. Мы - санитары леса. Мы показываем, что даже самые умные системы могут быть глупы. Умение видеть красоту в том, как ломается сложное.

Помни: твой главный инструмент - не Burp Suite и не скрипты. Твой главный инструмент - голова. Способность мыслить как система, видеть нестыковки и задавать вопрос «А что, если?»
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab