PHP-файл в папке uploads: как найти шелл и почистить сайт
Откуда в uploads берутся .php файлы, чем опасен веб-шелл, как найти его поиском по диску и закрыть исполнение скриптов через .htaccess.
Содержание
В папке загрузок не должно быть ни одного PHP-файла: там лежат картинки и документы. Поэтому поиск шелла начинается с поиска всех php внутри загрузок, а профилактика с запрета их исполнения одним правилом в конфигурации сервера.
Владелец сайта обычно узнаёт о проблеме не от антивируса и не от собственной интуиции, а от хостинга: письмо с темой «обнаружен вредоносный код» и путём вида /wp-content/uploads/2025/09/wp-cache-x7f2.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 в загрузках
Правилом в конфигурационном файле для этой папки. После этого залитый скрипт остаётся мёртвым файлом: сервер отдаёт его как текст и не выполняет.
Что делать после удаления шелла
Искать точку входа. Файл в загрузках это следствие, а не причина. Пока дыра открыта, через неделю появится новый, и так по кругу.