Отравление кеша. Разбираемся с кешированием страниц и атаками на него

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Отравление кеша (cache poisoning) — это вид атаки, при котором атакующий вносит в кеш системы некорректные данные. Когда система пытается их использовать, это приводит к проблемам: от нарушения работы до компрометации данных. В этой статье я покажу отравление кеша на примере реальной уязвимости в движке сайта, а также разберемся c HTTP-заголовками, которые препятствуют этой атаке.

В качестве примера мы будем разбирать CVE-2016-2784 — уязвимость в опенсорсном движке CMS Made Simple. Однако принцип атак на кеш схожий, и изложенное по большей части будет применимо и к другим случаям.

ЧТО ТАКОЕ КЕШ?​

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

Есть несколько типов кеша, но нам здесь наиболее важны два из них:
  • кеш веб‑браузера. Современные браузеры вроде Chrome и Firefox хранят копии веб‑страниц, изображений и других ресурсов локально на твоем компьютере. Это позволяет быстрее загружать контент при повторном просмотре веб‑страницы, так как данные будут получены с диска, а не из интернета;
  • кеш на стороне сервера. Веб‑серверы вместо того, чтобы создавать содержимое для каждого пользовательского запроса с нуля, хранят часто запрашиваемое содержимое непосредственно на сервере и выдают его пользователям. Это избавляет от необходимости повторно обрабатывать одни и те же данные и значительно снижает как нагрузку на сервер, так и время ответа.
Некоторые механизмы кеширования на стороне сервера:
  • объектный кеш. Основан на кешировании запросов к базе данных. Вместо того чтобы кешировать все страницы, он хранит результаты запросов к базе в памяти и использует их при сборке страниц;
  • кеш Opcode. Opcode (операционный код) — системная операция, которую может выполнять центральный процессор компьютера. Кеширование опкодов предназначено для серверных скриптовых языков, таких как PHP. Когда ты пишешь код на PHP, он изначально находится в форме, доступной для чтения человеком. При выполнении скрипты на PHP компилируются в операционный код. Кеширование опкодов сохраняет скомпилированный код в памяти, чтобы его можно было повторно использовать для последующих запросов и не требовалось каждый раз повторно компилировать скрипт;
  • кеш CDN. Сети доставки контента (Content Delivery Network, CDN) — это мощности, расположенные в разных странах. Они кешируют статические активы, такие как изображения, таблицы стилей и файлы JavaScript, на серверах, которые ближе всего к пользователям.
Для демонстрации локального кеша возьмем движок XenForo, который используется для создания форумов. Если ты перезагрузишь сайт на XenForo несколько раз, а затем отключишь интернет, то получишь кешированную страницу с сообщением «Страница не может быть загружена». При этом если ты откроешь инструменты разработчика, то увидишь service worker в разделе «Передано». Сервисный работник действует как прокси‑сервер и позволяет заменять ответы сервера данными закешированными данными, что и произошло в нашем примере.

image6.png


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

ЗАГОЛОВКИ​

Cache-Control​

Cache-Control — самый важный HTTP-заголовок кеширования. Он включает в себя несколько директив для управления поведением кеширования. Первые две из них — самые важные:
  • public/private — указывает, может ли ответ быть закеширован любым кешем (public) или только кешем браузера клиента (private). Когда директива указана как public, ответ может быть сохранен в любом кеше на пути запроса‑ответа. Это включает кеш клиентского браузера, промежуточные прокси и даже CDN. Например: открыто закешированное изображение на сайте может быть сохранено в кеше браузера пользователя, кеше прокси‑сервера интернет‑провайдера и на серверах CDN по всему миру. Когда указано private, ответ может быть закеширован только браузером клиента;
  • no-cache — заставляет кеши отправлять запрос на исходный сервер для проверки перед выдачей закешированной копии. В режиме no-cache при каждом запросе ресурса системы кеширования должны отправлять запрос на исходный сервер, чтобы проверить, актуальна ли копия. Проверка обычно происходит с использованием заголовков ETag или Last-Modified. ETag — это уникальный идентификатор, присвоенный сервером конкретной версии ресурса. Клиент при запросе ресурса может отправить значение закешированной копии в заголовке If-None-Match. Сервер сравнивает этот ETag с текущим ETag ресурса. Если они совпадают, это значит, что ресурс не изменился, и сервер отвечает статусом 304 Not Modified.
Пример: мы отправили запрос на получение гифки. Получаем ответ с заголовками ETag и Last-Modified.
image1.png


Полученный нами заголовок ETag будет отправлен как заголовок If-None-Match, а заголовок Last-Modified — как If-Modified-Since. Здесь сервер сравнивает отправленный нами ETag (значение If-None-Match) с текущим ETag ресурса, и, как видишь, они совпадают, поэтому статус ответа — 304.
image4.png


Может возникнуть вопрос: что произойдет, если изображение изменится? Если запрашиваемый нами ресурс был изменен и мы (клиентская сторона) об этом не знаем, запрос будет отправлен со старыми заголовками If-None-Match и If-Modified-Since. Если ETag не совпадает, сервер ответит новыми заголовками ETag и Last-Modified, которые станут использоваться в будущих запросах.
image2.png

image7.png


Пройдемся по остальным возможным директивам заголовка Cache-Control:
  • no-store — запрещает кешам сохранять ответ при любых обстоятельствах;
  • max-age=[секунды] — определяет максимальное время, в течение которого ресурс считается актуальным;
  • s-maxage=[секунды] — похоже на максимальный возраст (max-age), но применимо только к общедоступным кешам (таким как кеши CDN). Предположим, что новостной сайт использует CDN для распространения статей. На таком сайте может быть такой заголовок: Cache-Control: s-maxage=600, max-age=300. Это говорит общедоступным кешам (таким как CDN), что они должны хранить статью десять минут (s-maxage=600), но браузер должен хранить ее только пять минут (максимальный возраст равен 300);
  • must-revalidate — указывает, что кеш должен быть проверен на актуальность. Когда срок годности закешированного ресурса (определяется заголовками max-age, s-maxage или expires) истекает, он становится устаревшим. Директива must-revalidate обязывает системы кеширования проверять у исходного сервера (используя ETag или Last-Modified), актуален ли еще устаревший ресурс, перед его повторным использованием, чтобы пользователи не видели устаревший контент;
  • proxy-revalidate — похоже на must-revalidate, но применимо только к общедоступным кешам;
  • no-transform — запрещает любым промежуточным устройствам (таким как прокси) изменять данные ответа. Например, оператор мобильной сети может снижать качество изображений для экономии передаваемых данных. При использовании no-transform такие изменения запрещены.

Pragma​

Pragma: no-cache — это старый заголовок, существующий со времен HTTP/1.0. Он сообщает системам кеширования, что нужно отправить запрос на исходный сервер для проверки перед выдачей закешированной копии. Сейчас он в основном заменен Cache-Control: no-cache.

Expires​

Expires — это абсолютная временная метка (timestamp), которая задает время, когда закешированный ресурс начнет считаться устаревшим. Это похоже на директиву max-age в Cache-Control. Если присутствует и то и другое, то Cache-Control имеет приоритет.

Другие заголовки​

  • Age — показывает, как долго ресурс хранился в прокси‑кеше (в секундах).
  • Vary — используется, когда в ответ на запросы с разными заголовками запроса предоставляются разные версии ответа.
Представь, что у тебя есть веб‑сайт, который обслуживает изображения в двух форматах: WebP и JPEG. WebP поддерживается не всеми браузерами, но Chrome его поддерживает. Когда браузер запрашивает изображение с сайта, в запросе будет заголовок Accept. Этот заголовок сообщает серверу, какие типы контента может обрабатывать клиент. Для браузеров, поддерживающих WebP, в заголовке Accept будет среди прочего указано image/webp, то есть готовность принимать WebP.

Наличие заголовка Vary: Accept гарантирует, что система кеширования будет хранить несколько версий одного и того же ресурса, если он запрашивается с разными заголовками Accept. Таким образом, если браузер Chrome запрашивает изображение, система кеширования на сервере сохранит эту версию с ключом для заголовка запроса Accept: image/webp. Если позже более старый браузер запросит то же изображение, сервер ответит версией изображения в формате JPEG, и кеш сохранит эту версию отдельно.

ОТРАВЛЕНИЕ КЕША​

С точки зрения отравления кеша нас интересует именно серверный кеш. Он может находиться как на самом веб‑сервере, так и на промежуточном кеш‑сервере (например, на обратном прокси или CDN).

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

Например: у меня есть две страницы, каждая из которых содержит ссылку на другую.

Код первой страницы:
Код: Скопировать в буфер обмена
Код:
<?php
$host = $_SERVER['HTTP_HOST'];
$uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\');
$page2Link = "http://" . $host . $uri . "/page2.php";
?>
<!DOCTYPE html>
<html>
<head>
  <title>Page 1</title>
</head>
<body>
  <h1>This is Page 1</h1>
  <p><a href="<?php echo $page2Link; ?>">Go to Page 2</a></p>
</body>
</html>
Код второй страницы:
Код: Скопировать в буфер обмена
Код:
$host = $_SERVER['HTTP_HOST'];
$uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\');
$page1Link = "http://" . $host . $uri . "/page1.php";
?>
<!DOCTYPE html>
<html>
<head>
  <title>Page 2</title>
</head>
<body>
  <h1>This is Page 2</h1>
  <p><a href="<?php echo $page1Link; ?>">Go back to Page 1</a></p>
</body>
</html>
Когда я открою эту страницу, я получу ссылку, перенаправляющую меня на вторую страницу. Приведенный выше код на PHP получает хост (172.16.240.142) из заголовка Host.
image5.png


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

image8.png


Есть много способов решить эту проблему (помимо отключения кеширования). На мой взгляд, самый удобный — использование относительных ссылок.

Когда URL-адрес полный, например xakep.ru/?hack=true, и злоумышленник будет отправлять запросы с заголовком Host: xack.ru десять минут подряд, а кеширование у тебя настроено каждые пять минут, то при входе пользователя на сайт все полные URL-адреса будут изменены на xack.ru (например, ссылка xakep.ru/home превратится в xack.ru/home). Однако если ссылка относительная и указан просто адрес /home без домена, то, даже если есть проблема с кешированием, злоумышленник не сможет ей воспользоваться.

Описанное выше — это, наверное, основной пример «отравления кеша», с которым ты столкнешься во многих CVE.

Я рекомендую прочитать статью Practical Web Cache Poisoning. Она написана директором по исследованиям PortSwigger Джеймсом Кеттлом. Он обсуждает уязвимости отравления кеша, обнаруженные им в Unity, Red Hat, Firefox и прочем популярном софте. Думаю, я объяснил уже достаточно для того, чтобы ты разобрался с тем, о чем он пишет.

Слайды доклада Джеймса Кеттла на Black Hat, PDF

CVE-2016-2784​

Мы же возьмем для примера CMS Made Simple, где в версиях 2.x до 2.1.3 и 1.x до 1.12.2 есть уязвимость к отравлению кеша, когда активирована опция Smarty Cache. Мой выбор пал именно на этот баг, поскольку я без труда отыскал нужные версии в репозитории и смогу показать тебе дифф.

Давай начнем с основ настройки среды. Этот CVE был найден в 2016 году, поэтому я рекомендую загрузить Ubuntu 20.04 с версиями MySQL и PHP, которые использовались в то время.
NIST уже определил, что уязвимость находится в заголовке хоста, но для этого упражнения давай представим, что у нас нет этой информации. Кто‑то (не я) провел анализ патча и нашел изменения в коде.

Давай посмотрим, что изменилось в основном в файле class.cms_config.php.

Добавленный код:
PHP: Скопировать в буфер обмена
Код:
private function calculate_request_hostname()
  {
    if( $_SERVER['HTTP_HOST'] === $_SERVER['SERVER_NAME'] ) return $_SERVER['SERVER_NAME'];
    // $_SERVER['HTTP_HOST'] can be spoofed... so if a root_url is not specified
    // we determine if the requested host is in a whitelist.
    // If all else fails, we use $_SERVER['SERVER_NAME']
    $whitelist = (isset($this['host_whitelist'])) ? $this['host_whitelist'] : null;
    if( !$whitelist ) return $_SERVER['SERVER_NAME'];
    $requested = $_SERVER['HTTP_HOST'];
    $out = null;
    if( is_callable($whitelist) ) {
      $out = call_user_func($whitelist,$requested);
    }
    else if( is_array($whitelist) ) {
      // Could use array_search here, but can’t rely on the quality of the input (empty strings, whitespace etc).
      for( $i = 0, $n = count($whitelist); $i < $n; $i++ ) {
        $item = $whitelist[$i];
        if( !is_string($item) ) continue;
        if( !$item ) continue;
        if( strcasecmp($requested,$item) == 0 ) {
          $out = $item;
          break;
        }
      }
    }
    else if( is_string($whitelist) ) {
      $whitelist = explode(',',$whitelist);
      // Could use array_search here, but can’t rely on the quality of the input (empty strings, whitespace etc).
      for( $i = 0, $n = count($whitelist); $i < $n; $i++ ) {
        $item = $whitelist[$i];
        if( !is_string($item) ) continue;
        $item = strtolower(trim($item));
        if( !$item ) continue;
        if( strcasecmp($requested,$item) == 0 ) {
          $out = $item;
          break;
        }
      }
    }
    if( !$out ) {
      trigger_error('HTTP_HOST attack prevention: The host value of '.$requested.' is not whitelisted.  Using '.$_SERVER['SERVER_NAME']);
      $out = $_SERVER['SERVER_NAME'];
    }
    return $out;
  }
Удаленный код:
PHP: Скопировать в буфер обмена
Код:
if( CmsApp::get_instance()->is_https_request() ) $prefix = 'https://';
 $str = $prefix.$_SERVER['HTTP_HOST'].$path;
Замененный код:
PHP: Скопировать в буфер обмена
Код:
if( cmsms()->is_https_request() ) $prefix = 'https://';
 $str = $prefix.$this->calculate_request_hostname().$path;
Функция calculate_request_hostname добавлена в 2.1.3 (блок кода выше).

Как мы узнали, значение $SERVER['HTTP_HOST'] берется из заголовка Host, но в этом коде используется $SERVER['SERVER_NAME']. Он проверяет, равно ли значение HTTP_HOST значению SERVER_NAME. Если они равны, он возвращает $SERVER['SERVER_NAME']. Это основная проверка для предотвращения инъекции заголовка хоста. Если HTTP_HOST и SERVER_NAME не равны, функция проверяет, был ли предоставлен список разрешенных имен хостов. Белый список хранится в переменной $this['host_whitelist'], если белый список не предоставлен (null или не определен), то по умолчанию возвращается $SERVER['SERVER_NAME'].

Имя сервера хранится в config.php, например:
PHP: Скопировать в буфер обмена
Код:
<?php
$_SERVER['SERVER_NAME'] = 'xakep.ru';
?>
Затем этот конфиг используется в коде:
PHP: Скопировать в буфер обмена
Код:
<?php
require_once('config.php');
?>
<!DOCTYPE html>
<html>
<head>
  <title>Server Name and HTTP Host</title>
</head>
<body>
  <h1>Server Name:</h1>
  <p><?php echo $_SERVER['SERVER_NAME']; ?></p>
  <h1>HTTP Host:</h1>
  <p><?php echo $_SERVER['HTTP_HOST']; ?></p>
</body>
Даже если значение заголовка хоста установлено на другой домен, будет использоваться имя сервера.
image3.png


Теперь мы уверены, что уязвимость находится в заголовке хоста, и именно поэтому разработчики используют $SERVER['SERVER_NAME'].

Демонстрация уязвимости (видео)

ВЫВОДЫ​

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

Автор @az_AZ
Источник xakep.ru
 
Сверху Снизу