Плагин Copypress Rest API расширяет возможности REST API WordPress функциями управления контентом по HTTP. Удобно для автоматического размещения постов. В сентябре 2025 года исследователь kr0d нашел критическую уязвимость в плагине, которая позволяет получить RCE.
На момент написания статьи существует две версии плагина: 1.1 и 1.2. Обе версии уязвимы к CVE-2025-8625.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем, нарушение тайны переписки, осуществление прослушивания и чтение переписки граждан без их согласия преследуется по закону.
CVE-2025-8625 объединяет две проблемы:
- в исходный код зашит секретный ключ для генерации JWT;
- загрузка файлов реализована небезопасно.
Любой желающий может сгенерировать валидный токен и загрузить любой файл на сервер веб‑приложения.

Скачиваем плагин
Просто скачать архив или установить плагин через мастер установки не получится. 26 сентября команда WP отключила доступ к плагину в каталоге. Скачать архив с других ресурсов тоже не выйдет, и даже Internet Archive не поможет.

Лазейка, через которую получится достать плагин, — это SVN. Под плагины WordPress развернута система контроля версий Apache Subversions. Она позволяет просматривать исходные файлы и отслеживать, какие изменения были от версии к версии.
На странице плагина на вкладке Development осталась ссылка на репозиторий copypress-rest-api. Вот команда, которая скачает последнюю версию плагина в папку plugin
на твою машину:
svn export https://plugins.svn.wordpress.org/copypress-rest-api/trunk/ plugin
Если хочешь выкачать весь репозиторий, используй такую команду:
svn checkout https://plugins.svn.wordpress.org/copypress-rest-api/ plugin_full
В подпапку plugin_full
попадут две папки: trunk
— актуальная версия плагина, tags
— все релизы плагина. Команда будет особенно актуальна, если на момент чтения статьи автор выпустит новую версию плагина.
Если у тебя не установлен SVN, выполни sudp
.
Теперь зайди в папку plugin
и упакуй выбранную версию в ZIP:
zip -r copypress-rest-api.zip .
Плагин готов к установке.
Изучаем исходники
Первое проблемное место ты найдешь в файле includes/
в конструкторе:
public function __construct() {// Use a secret key from wp-config.php if defined$this->secret_key = defined('COPYREAP_JWT_SECRET_KEY') ? COPYREAP_JWT_SECRET_KEY : '826657a98e396172f8aed51d110d529d';}
Если не определен секретный ключ, используется жестко вшитый 826657a98e396172f8aed51d110d529d
. Спойлер: чтобы зарегистрировать собственный секретный ключ и обезопасить приложение, нужно добавить объявление COPYREAP_JWT_SECRET_KEY
в wp-config.
. Но плагин нигде не сообщает об этом пользователю.
Зная секретный ключ, злоумышленник может подделать JWT-токены и выполнить запрос к API плагина от имени любого пользователя ресурса.
info
Уязвимости с жестко зашитыми ключами относятся к CWE-321 — Use of Hard-coded Cryptographic Key (жестко зашитый криптографический ключ).
Подделка токена не была бы критической без второй проблемы — возможности загрузить любой файл на сервер. Магия происходит в функции copyreap_handle_image
, которая совершенно не заботится о том, что именно загружает. Нет проверки MIME-типа, не проверяется расширение файла, полностью отсутствует фильтрация. Класс проверяет корректность URL и доступность для чтения функцией file_get_contents
:
if ( ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) { return new WP_Error( 'invalid_image_url', 'Provided image URL is invalid.' );}$image_data = file_get_contents( $image_url );if ( ! $image_data ) { return new WP_Error( 'image_download_failed', 'Failed to download image.' );}
Плагин сохраняет файл под тем же именем, которое было в ссылке. Зная структуру папок WP, путь к файлу легко угадать: wp-content/
, где YYYY — это текущий год, а MM — текущий месяц с ведущим нолем.
$filename = basename( $image_url );$upload_dir = wp_upload_dir();$upload_path = $upload_dir['path'] . '/' . $filename;file_put_contents( $upload_path, $image_data );
Собираем стенд
www
Все исходники ты можешь скачать с моего GitHub ret0x2A.
Для тестов удобно использовать официальный образ WordPress для Docker. Создай docker-compose.
с таким содержимым:
version: '3.9'services: wordpress: image: wordpress:latest container_name: wp ports: - "8080:80" environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress volumes: - ./wp_data:/var/www/html db: image: mysql:5.7 container_name: wp_db restart: always environment: MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - ./db_data:/var/lib/mysql
Чтобы запустить, выполни команду docker
.
В собранном проекте Docker нужно включить mod_rewrite
. Выполни docker
.
Проверь содержимое файла .
, в базовом варианте он выглядит так:
$
#
#
#
#
#
Тебе нужно прописать правила самостоятельно:
docker exec wp bash -c 'echo -e "# BEGIN WordPress\nSetEnvIf Authorization "\(.*\)" HTTP_AUTHORIZATION=\$1\n<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteBase /\nRewriteRule ^index\.php$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . /index.php [L]\n</IfModule>\n# END WordPress" > /var/www/html/.htaccess'
Теперь вывод должен выглядеть так:
$
#
SetEnvIf
<
RewriteEngine
RewriteBase
RewriteRule
RewriteCond
RewriteCond
RewriteRule . /
#
Перезапусти контейнер командой docker
, чтобы изменения вступили в силу.
После сборки и запуска мастер установки WordPress доступен по адресу http://
. Выполни установку, указав любые данные. В конце включи перманентные ссылки.
В админке WP перейди к разделу плагинов, добавь новый и выбери «Загрузить плагин». Укажи наш архив. Активируй плагин.
Атакуем стенд
Для начала посмотри на код функции copyreap_generate_token
, чтобы понимать, как правильно сгенерировать JWT:
public function copyreap_generate_token($user) { $issued_at = time(); $expiration_time = $issued_at + 7200; // Token expires in 2 hour $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); $payload = json_encode([ 'iss' => get_bloginfo('url'), 'iat' => $issued_at, 'exp' => $expiration_time, 'data' => [ 'user_id' => $user->ID, 'username' => $user->user_login, 'email' => $user->user_email ] ]); $base64_url_header = $this->copyreap_base64UrlEncode($header); $base64_url_payload = $this->copyreap_base64UrlEncode($payload); $signature = hash_hmac('sha256', $base64_url_header . '.' . $base64_url_payload, $this->secret_key, true); $base64_url_signature = $this->copyreap_base64UrlEncode($signature); return $base64_url_header . '.' . $base64_url_payload . '.' . $base64_url_signature;}
Тебе потребуется id
админа. На это указывает строка 46 в файле плагина includes/
:
$jwt = new COPYREAP_JWT_Token();$user_data = $jwt->copyreap_validate_token($token);if (!$user_data) { return new WP_Error('invalid_token', 'Invalid token or expired', ['status' => 403]);}wp_set_current_user($user_data['user_id']);
Получить данные пользователя можно, обратившись к сайту по пути /
. В большинстве случаев прием сработает. В ответ ты получишь JSON с основными данными по всем пользователям сайта.

Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее