Плагин 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 с основными данными по всем пользователям сайта.

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