# SQL injection trong Wordpress core năm 2022 (CVE-2022-21661) ###### tags: `CVE` # CVE-2022-21661 - SQLi WP_Query ## Giới thiệu CVE này là về lỗi SQLi xảy ra ở bản 5.8.2 của Wordpress, đã được vá sau đó tại bản 5.8.3. Lỗi này cho phép dù là user guest thông thường hay là user admin đều có thể thực thi được câu lệnh SQL. Nó xuất hiện trong Wordpress core tuy nhiên để có thể trigger được lỗi thì ta cần phải có một plugin sử dụng đến class WP_Query. ## Phân tích lỗi Đầu tiên ta diff 2 bản patch để xem sự khác biệt ![](https://i.imgur.com/zbVaXGr.png) Có thể thấy đoạn code xử lý biến `$query['terms']` đã được thay đổi. Vì sao lại như vậy ? Đoạn code trên nằm ở dòng 559 của file `/wp-admin/class-wp-tax-query.php` cụ thể hơn nữa là trong hàm `clean_query`. Đào sâu vào hàm này ta sẽ thấy sau khi khử những `$query['terms']` trùng lặp bằng array_unique, sau đó gọi đến `$this->transform_query( $query, 'term_taxonomy_id' );`, tuy nhiên để giá trị của `$query['terms']` nhập vào không bị thay đổi thì `$query['taxonomy']` phải rỗng và `is_taxonomy_hierarchical` return False, điều này có thể đạt được dễ dàng bằng cách không nhập vào `$query['taxonomy']` :vvv ![](https://i.imgur.com/vK18SLi.png) Tại hàm `transform_query`: ![](https://i.imgur.com/ffo7RmM.png) Sẽ check giá trị của `$query['field'] == $resulting_field` mà `$resulting_field` khi hàm được gọi tới là `'term_taxonomy_id'`, hay nói cách khác là sẽ check `$query['field'] == 'term_taxonomy_id'`. Tuy nhiên `$query['field']` ta hoàn toàn có thể kiếm soát từ đó có thể khiến hàm return, luồng code sẽ được đưa về `clean_query` Biến `$query['term']` ta hoàn toàn có thể kiểm soát do đó ta có thể tainting câu query SQL dẫn đến SQLi. Tuy nhiên ta vẫn chưa biết câu query được execute ở đâu và untrusted data được đưa vào như thế nào?. Để biết được ta hãy cùng nhau truy ngược lại cách mà hàm `clean_query` được gọi. Đầu tiên hàm `clean_query` sẽ được gọi đến trong hàm `get_sql_for_clause` ![](https://i.imgur.com/2O5i44X.png) >Nói sơ qua về hàm `get_sql_for_clause()` sẽ có nhiệm vụ xử lý dữ liệu nhận được và tạo thành các câu query có điều kiện để lấy dữ liệu như các câu dạng `SELECT ... WHERE ...` Tiếp tục traceback ta biết được `get_sql_for_clause` được gọi đến trong `get_sql_for_query()` và `get_sql_for_query` được gọi đến trong `get_sql_clauses()`. Hàm `get_sql_clauses` tiếp tục được gọi đến bởi hàm `get_sql()` > Các hàm trên có thể hiểu đơn giản giúp ta gen ra câu sql query theo điều kiện ta nhập vào Tiếp tục hàm `get_sql()` sẽ được gọi đến bởi hàm `get_posts()` trong class WP_Query tại file `/wp-admin/class-wp-query.php` Và tại đây sẽ có một hàm execute câu query là `$wpdb->get_col()` ![](https://i.imgur.com/Mh0p8jh.png) Đến đây ta chỉ tìm được nơi execute được SQL query nhưng chưa tìm được nơi thật sự có thể đưa untrusted data vào. Tiếp tục trace ta thấy `get_posts()` được `query()` gọi tới. ![](https://i.imgur.com/VnOc8ID.png) Và `query()` sẽ được magic methods `__construct` của class WP_Query gọi tới ![](https://i.imgur.com/9F3hnbT.png) Vậy thì bây giờ chỉ cần tìm cách khởi tạo object của class `WP_Query`, magic method `__construct` được thực thi thì chain phía trên cũng sẽ được thực thi, và nếu ta kiểm soát được giá tị `$query` đưa vào `WP_Query` khi khởi tạo thì ta có thể thực thi được SQL injection. Tuy nhiên trong Wordpress core thì class `WP_Query` không được dùng tới. Class này thường được dùng bởi các plugin. Vì thế mình sẽ import vào một costum plugin để demo exploit. Code của plugin được imort (nguồn code mình tham khảo cũng ở trong code): ```php <?php /* Plugin Name: CVE-2022-21661-test-plugin Plugin URL: https://www.lsablog.com/networksec/penetration/cve-2022-21661-wordpress-core-sqli-analysis Description: This plugin was made in order to test CVE-2022-21661 (wordpress core sql injection) Version: v1.0 Author: LSA Author's Blog: https://www.lsablog.com/ License: MIT */ function testSQLiCVE202221661(){ echo 'test-cve-2022-21661-plugin'; $inputData = stripslashes($_POST['data']); $jsonDecodeInputData = json_decode($inputData,true); $wpTest = new WP_Query($jsonDecodeInputData); wp_die(); } add_action('wp_ajax_nopriv_testcve202221661','testSQLiCVE202221661'); ``` Ngoài ra trong Wordpress core để các plugin có thể sử dụng được `WP_Query` thì các plugin này phải thực hiện một việc là gọi là add_action. Sau khi add_action xong, nếu ajax của Wordpress gọi đến action của plugin, call back function được định nghĩa chung với action sẽ được thực thi từ đó có thể gọi đến và sử dụng `WP_Query`. Plugin demo sẽ thêm action `wp_ajax_nopriv_testcve202221661` cho phép khởi tạo `WP_Query`. Ajax wordpress xử lý tác vụ này nằm ở file `/wp-admin/admin-ajax.php`. Để xử lý được action ta phải gửi post request đến `/wp-admin/admin-ajax.php` và cho dù ta có login hay không thì `/wp-admin/admin-ajax.php` vẫn có thể thực hiện được action, hay có nghĩa là dù ta có login hay không thì `WP_Query` vẫn sẽ được khởi tạo, ta có thể exploit được SQLi ![](https://i.imgur.com/124BX82.png) Vậy tóm lại flow exploit sẽ như sau: ``` POST request có payload -> /wp-admin/admin-ajax.php /wp-admin/admin-ajax.php -> do_action("wp_ajax_nopriv_testcve202221661" ) WP_Query()->__contruct() WP_Query()->query() WP_Query()->get_posts() (tại đây thì $wpdb->get_col() sẽ execute câu truy vấn) WP_Tax_Query()->get_sql() WP_Tax_Query()->get_sql_clauses() WP_Tax_Query()->get_sql_for_query() WP_Tax_Query()->get_sql_for_clause() WP_Tax_Query()->clean_query() ``` >NOTE: từ phần `WP_Tax_Query()->get_sql()` trở xuống sẽ xử lý gen câu query sau đó truyền cho `wpdb->get_col()` trong `WP_Query()->get_posts()` để execute Và điều kiện để exploit được là: - Plguin có thể khởi tạo được `WP_Query` - Biến `$query['field']` == `'term_taxonomy_id'` - `$query['taxonomy']` trống hoặc `is_taxonomy_hierarchical($query['taxonomy']) === false` ## Khai thác Tiếp theo ta sẽ cùng phân tích cách các hàm trong chain trên gọi tới nhau và untrusted data thay đổi như thế nào qua từng hàm Đầu tiên ta gửi một POST request đến endpoint `/wp-admin/admin-ajax.php` với action có thể kích hoạt được `WP_Query` trong plugin (cụ thế plugin của ta là `action= testcve202221661`), và thêm vào đó là param `data`, param này sẽ được dùng để khởi tạo `WP_Query` trong plugin ![](https://i.imgur.com/LqxQzr1.png) >Note: Param `data` sẽ có dạng JSON với giá trị như sau: `{"tax_query":{"0":{"field":"term_taxonomy_id","terms":["<SQL Inject Code>"]}}}`. Tại sao lại như vậy thì cùng mình phân tích tiếp nhé. ![](https://i.imgur.com/70fABdk.png) Đầu tiên `admin-ajax.php` sẽ thực hiện action `wp_ajax_nopriv_"testcve202221661"` ![](https://i.imgur.com/r77LdIS.png) action `wp_ajax_nopriv_"testcve202221661"` thực hiện khởi tạo object của class `WP_Query` và json `data` được parse sang chuỗi ![](https://i.imgur.com/PeiBAio.png) param `data` do ta truyền vào được truyền vào hàm `query()` thông qua biến `query` ![](https://i.imgur.com/t7luP80.png) Hàm `query` gán `query_vars` bằng `query` mà ta truyền vào. Và biến `query_vars` có kiểu dữ liệu là mảng vì thế nên `query` ta truyền vào phải được parse từ json thành mảng. Sau đó thực hiện `$this->get_posts()` Trước khi execute câu query thì `get_posts()` thực hiện 2 thứ mà ta cần lưu ý, đầu tiên là thực hiện `$this->parse_query()` và `$this->parse_tax_query()`. Thứ 2 là gọi `get_sql()` trong `WP_Tax_Query` ![](https://i.imgur.com/lcsEkkT.png) ![](https://i.imgur.com/JdrbS4P.png) Trong đó `parse_query()` sẽ parse `query` đầu vào thành một chuỗi to chung với các thuộc tính khác theo logic của chương trình ![](https://i.imgur.com/jVh2h3C.png) Sau đó `parse_tax_query()` sẽ lấy ra giá trị `tax_query` từ mảng siêu to này để chiết xuất 2 giá trị `fields` và `terms` Sau đó khởi tạo một object của class `WP_Tex_Query` lần lượt gọi đến `get_sql()`, `get_sql()` gọi đến `get_sql_clauses()` ![](https://i.imgur.com/Qg96NWL.png) ![](https://i.imgur.com/9XNVQMA.png) `queries` được đưa vào `get_sql_for_query` ![](https://i.imgur.com/0Vyjtnh.png) `queries` được tách ra thành `clause` và đưa vào `get_sql_for_clause($clause, $query)`. Mà vì ta chỉ đưa vào một query nên khi foreach xong 2 giá trị này là như nhau :v ![](https://i.imgur.com/wGi8gDZ.png) `get_sql_for_clause()` gọi đến `clean_query($clasue)` và cuối cùng `clean_query()` gọi đến `$this->transform_query( $query, 'term_taxonomy_id' );` ![](https://i.imgur.com/JtimsGB.png) Ta thấy được `$query['field']` chính bằng `term_taxonomy_id` là giá trị ta truyền vào thông qua param `data` Sau quá trình gen request thực hiện xong, tức là tại `get_posts()` sau khi thực thi `get_sql()` xong, chương trình sẽ đưa câu SQL đã được gen vào `$wpdb->get_col` và thực hiện execute ![](https://i.imgur.com/TvEWqtD.png) Tại Burp ta cũng thấy được cấu trúc câu query ![](https://i.imgur.com/UNSnReP.png) Giờ chỉ cần thay payload Time-based SQLi ![](https://i.imgur.com/ZTPp7a1.png) >Do mạng chậm nên respone time hơi lâu :v ## Kết luận Tuy sink xuất hiện tại Wordpress core nhưng điều kiện đủ để trigger được lỗi thì ta cần target phải đang sử dụng một plugin có thể khởi tạo class `WP_Query`. Nghe có vẻ khá khó exploit tuy nhiên theo tác giả CVE thì số lượng plugin đáp ứng đủ yêu cầu cũng rất nhiều. Trong đó có thể kể đến `Ele custom skin`, một plugin cho phép custome theme với hơn 100k lượt tải Fact về tác giả đã report CVE này là ngocnb và khuyenn đến từ GiaoHangTietKiem JSC ## Refer: https://www.sonarsource.com/blog/wordpress-core-unauthenticated-blind-ssrf/ https://medium.com/@cognn/sql-injection-in-wordpress-core-zdi-can-15541-a451c492897 https://github.com/WordPress/WordPress/commit/6f7032dcf423b67f90381d4f29a90d16f4829070 https://www.zerodayinitiative.com/blog/2022/1/18/cve-2021-21661-exposing-database-info-via-wordpress-sql-injection https://confidentialteam.github.io/posts/cve-2022-21661/