• XSS.stack #1 – первый литературный журнал от юзеров форума

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

top

(L3) cache
Пользователь
Регистрация
03.02.2020
Сообщения
252
Реакции
342
Отравление кеша (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
 
В двух словах раскажу про Opcode на примере opcache (https://www.php.net/manual/ru/book.opcache.php) часто его встречаю и переодически кто-нибудь просит помощь разобраться "почему я редактирую файл, вижу на шеле изменения но на сайте их нет".


OPcache — это расширение для PHP, которое кэширует байт-код скриптов PHP, уменьшая необходимость повторной компиляции кода при каждом запросе. Это помогает значительно улучшить производительность PHP-приложений. Рассмотрим его принцип работы более подробно:

1. Загрузка скрипта:
- Когда PHP-скрипт вызывается впервые, PHP интерпретатор считывает исходный код и компилирует его в байт-код.

2. Кэширование байт-кода:
- После компиляции байт-код сохраняется в OPcache.
- При последующих вызовах того же скрипта PHP интерпретатор проверяет, есть ли актуальная версия байт-кода в OPcache. Если она есть, интерпретатор использует байт-код из кэша, минуя стадию компиляции.

3. Проверка актуальности кэша:
- OPcache отслеживает изменения в исходных PHP-файлах. Если исходный файл был изменен, кэш становится неактуальным, и скрипт снова компилируется, а новая версия байт-кода сохраняется в кэше.

4. Параметры конфигурации:
- OPcache имеет множество параметров конфигурации, таких как `opcache.enable`, `opcache.memory_consumption`, `opcache.interned_strings_buffer`, и т.д., которые позволяют управлять его поведением и настройками кэширования.

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

Пример конфигурации OPcache в файле php.ini:

Код:
; Включить OPcache
opcache.enable=1

; Максимальный размер памяти для хранения байт-кода в МБ
opcache.memory_consumption=128

; Максимальный размер буфера для хранения строк в МБ
opcache.interned_strings_buffer=8

; Количество кэшируемых файлов
opcache.max_accelerated_files=10000

; Время жизни кэша файлов в секундах
opcache.revalidate_freq=60

Так вот после того как мы ознакомились что такое opcache нас интересует 2 настройки:
opcache.validate_timestamps bool При включении опции OPcache будет проверять актуальность закешированных скриптов каждые opcache.revalidate_freq секунд. При отключении директивы требуется сбросить OPcache вручную функциями opcache_reset() и opcache_invalidate() или перезапустить веб-сервер, чтобы изменения в файловой системе вступили в силу.
opcache.revalidate_freq int Как часто в секундах проверять обновления меток времени скрипта. При значении 0 OPcache будет проверять обновления при каждом запросе. Модуль игнорирует эту директиву конфигурации, если опцию opcache.validate_timestamps отключили.

Ну и "живой" пример:
Например у нас есть сервер где validate_timestamps выключен:
Код:
┌──(kali㉿kali)-[/etc/php/8.2/apache2/conf.d]
└─$ cat 10-opcache.ini                     
; configuration for php opcache module
; priority=10
zend_extension=opcache.so
opcache.jit=off
opcache.validate_timestamps = false

и вот наш условный сайт:
Код:
┌──(kali㉿kali)-[/var/www/html/op-inj]
└─$ cat index.php
<?php

echo 'Hello world!';

Он выдает нам Hello world! при посещении. Теперь делаем инжект:
Код:
┌──(kali㉿kali)-[/var/www/html/op-inj]
└─$ cat index.php
<?php

echo 'Hello world!';
echo 'INJECTED';
Ну и конечно видим что ничего не изменилось т.к. мы закешированы. К счастью есть удобный инструмент https://github.com/amnuts/opcache-gui который мы устанавливаем на сайт и с помощью которого мы сбрасываем кеш для конкретного скрипта нажав на force file invalidation:

opca-gui.png



Обновляем страницу и видим Hello world!INJECTED теперь мы удаляем из файла наши "следы" а сайт продолжит выдавать Hello world!INJECTED из кеша байткода. Таким образом мы можем прятать бэкдоры и прочее в кеше. Но не забывайте что кеш живет либо указанное время в opcache.revalidate_freq либо может жить до принудительного сброса, который рано или поздно случиться, например когда админ накатит апдейты и сам сбросит кеш, как это делаю я на своем сайте.

Если вы не хотите делать сброс кеша скриптом из гитхаба, читайте доки: https://www.php.net/manual/en/function.opcache-invalidate.php но помните что вызвать данную функцию нужно будет из свеже-созданного скрипта т.к. старые скрипты все кешированые. Так же полезная функция для ознакомления var_dump(opcache_get_status());
 
Насколько понял, этим способ можно осуществить установку шелла или перелив трафа с донора/жертвы на свой целевой урл или на инсталлы. Интересный способ, надо попробовать.
 
Насколько понял, этим способ можно осуществить установку шелла или перелив трафа с донора/жертвы на свой целевой урл или на инсталлы. Интересный способ, надо попробовать.
ну каждый видит по своему, вообще это демонстрация условного fileless бэкдора, защита от грепающего админа
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх