Безопасность сайта

PHP-файл в папке uploads: как найти шелл и почистить сайт

Откуда в uploads берутся .php файлы, чем опасен веб-шелл, как найти его поиском по диску и закрыть исполнение скриптов через .htaccess.

обновлено 8 минут чтения 2 Дмитрий Акиндинов
Содержание
  1. Почему злоумышленники выбирают именно uploads
  2. Как за пять минут найти все .php в uploads
  3. Как выглядит типичный шелл изнутри
  4. Чистка: что удалять сразу, что проверять отдельно
  5. Запрет исполнения PHP в uploads через .htaccess
  6. Что сделать после чистки
  7. Частые вопросы

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

Владелец сайта обычно узнаёт о проблеме не от антивируса и не от собственной интуиции, а от хостинга: письмо с темой «обнаружен вредоносный код» и путём вида /wp-content/uploads/2025/09/wp-cache-x7f2.php. Или ещё хуже, письма клиентов не доходят, потому что почтовый сервер попал в чёрный список за спам-рассылку, которую сайт вёл всю ночь без ведома владельца. В обоих случаях причина почти всегда одна: в папке uploads лежит PHP-файл, которого там быть не должно, и через него кто-то чужой управляет вашим сервером. Эта статья для тех, кто уже видит подозрение или держит в руках такое письмо. Разберём, откуда берётся шелл, как найти все его копии за пять минут, что с ними делать и почему одной чисткой проблема не закрывается.

Иконка папки uploads со скрытым PHP-файлом внутри

Почему злоумышленники выбирают именно uploads

Папка /wp-content/uploads/ существует для одной задачи: хранить картинки, документы и прочие файлы, которые загружает редактор через админку. По умолчанию она открыта на запись веб-сервером, потому что иначе загрузка изображений просто не сработает. WordPress при этом не проверяет содержимое файла построчно, только расширение и MIME-тип на входе, и то не всегда строго. Если где-то в цепочке эта проверка обходится, файл с расширением .php благополучно оказывается в директории, из которой веб-сервер готов исполнять любой скрипт по прямому запросу.

Обходят проверку типично через дырявый плагин загрузки файлов: формы обратной связи с вложениями, конструкторы страниц с drag-and-drop загрузкой медиа, старые версии файловых менеджеров для админки. У меня в практике чаще всего это была связка из заброшенного плагина трёхлетней давности и формы, которая принимала файл без проверки реального содержимого, доверяя только расширению из имени. Переименовать shell.php в shell.php.jpg или подложить в начало файла байты валидного GIF-заголовка (полиглот, который одновременно и картинка, и рабочий PHP-код), достаточно, чтобы обмануть слабую проверку.

Дальше атакующему даже не нужен доступ в админку. Он просто открывает https://ваш-сайт.ru/wp-content/uploads/2025/09/имя.php в браузере или дергает его через curl, а сервер честно выполняет то, что там написано: рассылает спам, докачивает второй, более функциональный шелл, разворачивает майнер или сканирует сеть в поисках следующей жертвы.

Как за пять минут найти все .php в uploads

Если есть доступ по SSH, поиск занимает буквально одну команду:

find /home/user/public_html/wp-content/uploads -type f -name "*.php"

Замените путь на свой (структура каталогов у разных хостингов отличается, но wp-content/uploads всегда лежит в корне сайта). Команда выведет полный список php-файлов внутри uploads, и в норме этот список должен быть пустым. Если он не пустой, каждый файл из вывода это кандидат на шелл.

Дальше полезно посмотреть, что менялось за последнее время в принципе, не только в uploads:

find /home/user/public_html -type f -mtime -14 -name "*.php" ! -path "*/uploads/*"

Флаг -mtime -14 покажет файлы, изменённые за последние две недели. Если вы точно помните, что последний раз трогали код месяц назад, а find выдаёт свежие правки в файлах темы или плагинов, это ещё одна точка входа, которую придётся проверить отдельно.

Без SSH та же задача решается через файловый менеджер хостинг-панели: у большинства (ISPmanager, cPanel, Timeweb) есть встроенный поиск по имени файла с фильтром по расширению, либо ClamAV-сканер, который запускается вручную из панели. Результат медленнее, но рабочий.

Полезно сверить список с бэкапом, снятым до заражения (если такой есть). Всё, чего не было в бэкапе и что появилось в uploads позже, разбираем в первую очередь.

Как выглядит типичный шелл изнутри

Открывать подозрительный файл лучше не в браузере (он выполнится), а текстовым редактором или через cat в консоли. Классический шелл почти никогда не выглядит как читаемый PHP-код. Обычно это одна длинная строка с base64_decode, gzinflate и eval, за которыми спрятана вся функциональность:

<?php
$p = 'ZXZhbCgkX1BPU1RbInJ1biJdKTs=';
eval(base64_decode($p));

Это иллюстративный пример, не рабочий код, но принцип именно такой: строка декодируется в момент выполнения, поэтому статический поиск по слову «eval» в файле не сработает, придётся смотреть по сигнатурам base64_decode, gzinflate, str_rot13, create_function рядом друг с другом. Ни у одной картинки, ни у одного легитимного плагина такой код в uploads не появляется никогда, так что само наличие этих функций в файле с расширением .php внутри uploads уже основание для удаления без долгих раздумий.

Имя файла обычно маскируется под системное: wp-cache.php, class-wp-includes.php, favicon.php, иногда просто случайный набор символов вроде 4f8a2c1.php. Иногда шелл кладут не один, а сразу пять-шесть копий с разными именами в разные подпапки по датам (uploads/2024/03, uploads/2025/01 и так далее), чтобы после чистки одной копии оставался запасной вход.

Чистка: что удалять сразу, что проверять отдельно

Порядок действий, который снимает большинство рисков за один заход:

  • Все .php файлы непосредственно внутри uploads и её подпапок удалить сразу. По архитектуре WordPress их там в принципе быть не должно, легитимных исключений не существует.
  • wp-config.php проверить на добавленные строки в самом низу файла, после закрывающего define. Это частое место для закладки, которая подгружает код с внешнего сервера при каждом запуске сайта.
  • .htaccess в корне сайта и в самой uploads проверить на чужие RewriteRule и RewriteCond, особенно редиректы мобильных посетителей на сторонние домены.
  • Файлы тем и mu-plugins сверить с чистой версией из репозитория WordPress.org или из своего git, если он есть. Отличия по датам изменения и по объёму подсказывают, где искать.
  • Таблицу wp_users проверить на новых администраторов, зарегистрированных в дни, когда владелец сайта никого не создавал.
  • Задания wp_cron (через плагин WP Crontab или запрос к базе) проверить на незнакомые хуки с интервалом запуска в несколько минут, это типичный способ держать переустановку шелла на автомате.

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

НаходкаЧто делатьСрочность
.php файл в uploadsУдалить немедленноСразу
Незнакомый admin в wp_usersУдалить, сменить паролиСразу
Изменённый wp-config.phpСверить с бэкапом, убрать лишнееСразу
Чужие правила в .htaccessУдалить, сверить с оригиналомВ этот же день
Изменённые файлы темы/плагинаПереустановить из чистого источникаВ этот же день
Подозрительное задание в wp_cronУдалить задание, найти источникВ течение суток

Запрет исполнения PHP в uploads через .htaccess

Даже после полной чистки правильный шаг, закрывающий именно эту дверь на будущее, добавить запрет на выполнение любых скриптов внутри uploads. Для Apache и LiteSpeed это делается через отдельный .htaccess прямо в папке uploads:

<FilesMatch ".(php|phpd|phtml|phar)$">
    Require all denied
</FilesMatch>

Для старых конфигураций Apache 2.2 синтаксис другой:

<FilesMatch ".(php|phpd|phtml|phar)$">
    Order allow,deny
    Deny from all
</FilesMatch>

На Nginx эквивалент прописывается в конфиге сервера (правки вносит хостинг или через панель, если она это позволяет):

location ~* /wp-content/uploads/.*.php$ {
    deny all;
}

Важная оговорка: это защита от симптома, а не лечение причины. Даже если запретить исполнение PHP в uploads, дырявый плагин или форма продолжит принимать вредоносные файлы, просто они перестанут запускаться напрямую по URL. Атакующий, зная про такую защиту, попробует загрузить шелл в другую директорию, где исполнение PHP не запрещено. Правило в .htaccess снижает ущерб, но точку входа всё равно нужно найти и закрыть отдельно: обновить или удалить уязвимый плагин, проверить форму загрузки файлов на строгую валидацию по содержимому, а не только по расширению.

Что сделать после чистки

Через неделю после уборки заражение нередко возвращается, потому что где-то остался незамеченный бэкдор или доступ, которым продолжает пользоваться атакующий. Минимальный список действий после чистки:

  • Сменить пароли везде: админка WordPress, FTP/SFTP, панель хостинга, база данных. Если пароль от FTP хранился где-то в открытом виде или совпадал с паролем от почты, поменять и там.
  • Перегенерировать ключи безопасности WordPress (AUTH_KEY, SECRET_KEY и остальные) через генератор на api.wordpress.org/secret-key/1.1/salt/ и вставить в wp-config.php. Это сбрасывает все активные сессии, включая сессию атакующего, если он успел авторизоваться под украденным паролем.
  • Переустановить ядро WordPress свежей копией с официального сайта поверх текущего, не трогая wp-content. Это закрывает вариант, при котором изменён один из core-файлов.
  • Проверить список плагинов на неактуальные версии и удалить те, которыми реально не пользуетесь. Каждый лишний плагин это лишняя потенциальная дыра.
  • Через неделю прогнать поиск .php в uploads ещё раз. Если он снова не пустой, точка входа не закрыта, и чистку придётся повторить с более внимательным разбором логов.

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

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

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

Как найти шелл в папке загрузок

Поискать в ней все файлы с расширением php. В нормальном сайте их там нет ни одного, поэтому любая находка это повод для разбора, а не для сомнений.

Как выглядит шелл изнутри

Обычно как нечитаемая строка с последующим выполнением: код закодирован и разворачивается на лету. Иногда маскируется под системный файл именем вроде кэша или конфигурации.

Как запретить исполнение PHP в загрузках

Правилом в конфигурационном файле для этой папки. После этого залитый скрипт остаётся мёртвым файлом: сервер отдаёт его как текст и не выполняет.

Что делать после удаления шелла

Искать точку входа. Файл в загрузках это следствие, а не причина. Пока дыра открыта, через неделю появится новый, и так по кругу.

Передайте техническую часть в надёжные руки.

Слежу за хостингом, бэкапами и обновлениями. Когда что-то пойдёт не так – звоните мне, не своему программисту, не в спортзал и не маме.

Короткие вопросы по теме

Все вопросы по теме "Безопасность сайта"

Заявка

Обсудим задачу

Напишите имя и телефон, выберите удобный канал. Перезвоню или напишу в течение часа в рабочее время.

Куда удобнее ответить?