# Insecure Deserialization trong PHP Bài này mình sẽ viết về những thứ mà mình học được về lỗ hổng **Insecure Deserialization** trong **PHP** nhằm ôn lại và củng cố thêm kiến thức. Nếu có sai sót ở đâu mong mọi người góp ý và bỏ qua (^_\^) ## Khái niệm Trước hết tìm hiểu về khái niệm **serialization** và **deserialization**. - **Serialization** là quá trình chuyển đối của một đối tượng thành định dạng như chuỗi byte, JSON, YAML,... Mục đích chính của quá trình này để dễ dàng lưu trữ và truyền dữ liệu giữa các ứng dụng. - **Deserialization** là quá trình ngược lại với **serialization** để chuyển từ những định dạng dữ liệu trên thành đối tượng ban đầu. ![](https://hackmd.io/_uploads/ryoQ1Ci4n.png) Như vậy lỗ hổng **Insecure Deserialization** xảy ra khi thực hiện quá trình **deserialization**. Nguyên nhân chính là do việc ứng dụng web để người dùng có thể kiểm soát được các dữ liệu sau khi **serialize** đối tượng nào đó (gọi là **serialized data**), khi thay đổi chúng ứng dụng sẽ thực hiện quá trình **deserialization** để khôi phục lại đối tượng ban đầu và đương nhiên đối tượng đó sẽ bị thay đổi và có thể gây ra ảnh hưởng tới ứng dụng, thậm chí là máy chủ chứ không chỉ đối tượng đó. Trong **PHP**, thực hiện quá trình **serialization** bằng hàm **serialize()**, và quá trình **deserialization** bằng hàm **unserialize()**. Nói 1 chút về định dạng của **serialized data**: Ví dụ: có 1 đối tượng là ``$user`` có 3 thuộc tính: ``` $user->name = "chuong"; $user->age = 18; $user->isAdmin=false ``` Sau khi thực hiện **serialize** bằng hàm `serialize($user)` thu được kết quả: ``` O:4:"User":3:{s:4:"name";s:6:"chuong";s:3:"age";i:18;s:7:"isAdmin";b:0;} ``` - `O:4:"User":3`: `O` là object có class tên là `User` và độ dài tên class là `4`, object này có `3` thuộc tính. - `s:4:"name";s:6:"chuong"`: đây là thuộc tính đầu tiên có kiểu dữ liệu là string (`s`), có tên là `name` với độ dài là `4`, giá trị là string (`s`) có giá trị là `chuong` với độ dài là `6`. - `s:3:"age";i:18`: đây là thuộc tính thứ 2 có dạng string (`s`), tên thuộc tính là `age` có `3` ký tự và giá trị là `18` với kiểu dữ liệu int (`i`). - `s:7:"isAdmin";b:0`: đây là thuộc tính cuối cùng tương tự trên và có giá trị boolen (`b`) với giá trị false (`0`).<br> Tham khảo thêm: https://www.php.net/manual/en/function.serialize.php ## Một số dạng tấn công ### Sửa đổi đối tượng Đây là dạng bài đơn giản nhất. #### Ví dụ 1: Giả sử ứng dụng web kiểm tra quyền admin bằng cách kiểm tra giá trị `isAdmin` (ví dụ trên). Nếu giá trị là **true** sẽ được cấp quyền và ngược lại không nếu là **false**. Vậy khi người dùng có thể kiểm soát **serialized data** sẽ chỉnh sửa được giá trị **false** về **true** bằng cách thay đổi `s:7:"isAdmin";b:0` thành `s:7:"isAdmin";b:1`. Khi ứng dụng `deserialize` lại nhận được đối tượng giá giá trị thuộc tính `isAdmin` là **true** => thành công. #### Ví dụ 2: Giả sử ứng dụng có chức năng đọc file bằng hàm `file_get_contents()`: ``` function read(){ echo file_get_contents($this->filename); } ``` Như đã nói ở dạng trên, kẻ tấn công có thể thay đổi các thuộc tính và khi đó `$this->name` sẽ bị thay đổi thành các file nhạy cảm như `/etc/passwd`,... dẫn đến việc kẻ tấn công có thể đọc được chúng. #### Ví dụ 3: Khi các ứng dụng sử dụng cách xác thực tài khoản không an toàn: ``` if ($user['username'] == 'admin' && $user['password'] == $adminPassword) { $admin = true; } else { $admin = false; } ``` Ở đây thứ mà ta kiểm soát được là `username` và `password`, thứ chưa biết là `$adminPassword`. Vậy làm sao để bypass cái này?. Đó là dạng **type juggling** khi ứng dụng sử dụng so sánh loose (`==`) mà không phải `===`. Khi so sánh chuỗi (ở đây là `$adminPassword`) với boolean mang giá trị `true` sẽ trả về kết quả đúng. Vậy chỉ việc thay đổi giá trị `password` thành kiểu boolean mang giá trị `true` là được. Tham khảo thêm về **type juggling**: https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf ### POP chain **POP chain** còn được gọi là **Code Reuse Attack**, đây là 1 kỹ thuật liên quan đến việc sử dụng lại các đoạn code của chương trình (gọi là các **gadget**) để liên kết chúng lại thành 1 chuỗi thực thi (**chain**) đồng thời kết hợp với việc thay đổi các thuộc tính của các đối tượng tạo ra **một luồng hoạt động** với mục đích tấn công ứng dụng. Quan trọng của kỹ thuật này là việc sử dụng các **magic method** (các method bắt đầu bằng `__`), đại khái là nó sẽ được thực thi tự động khi thỏa mãn điều kiện, cụ thể với một số như sau: - `__construct()`: method này sẽ thực thi khi khởi tạo đối tượng từ class, ví dụ: `$user= new User('chuong');`. - `__destruct()`: method này sẽ thực thi khi đối tượng bị hủy ví dụ như dùng hàm `unset($user)` hoặc khi chương trình kết thúc (ngoài trừ việc chương trình kết thúc bằng 1 số hàm như `die()`). - `__wakeup()`: method này sẽ thực thi khi thực hiện quá trình **deserialization**, ví dụ: `unserialize($user);`. - `__toString()`: method này sẽ thực thi khi đối tượng được sử dụng như chuỗi , ví dụ như sử dụng các hàm in ra chuỗi như `echo`, `print()`,... hoặc sử dụng so sánh loose (`==`), hoặc trong các hàm như `preg_match()`. - `__invoke()`: method này sẽ thực thi khi đối tượng được gọi như một hàm, ví dụ: `$user();`. - ... Xem thêm: https://www.php.net/manual/en/language.oop5.magic.php Tiếp theo thì phải chú ý đến việc ứng dụng sử dụng các **function** đặc biệt được sử dụng như: `file_get_contents()`, `include()`, `system()`, `unlink()`,.. Đó có thể là các **filesystem functions** để thao tác với các file hệ thống cũng như hệ thống gây nên các cuộc tấn công. Ví dụ như chương trình sử dụng `file_get_contents()` để đọc file nào đó, khi kẻ tấn công thay đổi các thuộc tính truyền vào vào hàm đó như `/etc/passwd` nghĩa là có thể đọc được nó cũng như các file nhạy cảm khác. Và tất nhiên để ứng dụng làm được vậy có thể phải nhờ đến việc sử dụng các đoạn code khác trong chương trình (**gadgets**). Để xem các **gadgets** liên kết với nhau ra sao thì mình sẽ lấy một ví dụ đơn giản: Có file `index.php` có 2 class `A`, `B` và nhận vào biến `hix` từ GET để **unserialize()** nó. ``` <?php //the flag in flag.php class A { public $filename; public function __construct() { $this->filename = "hello.txt"; } public function read() { $res = ""; if (isset($this->filename)) { $res = file_get_contents($this->filename); } echo $res; } } class B { public $name; public function __construct($name) { $this->name = $name; } public function __wakeup() { return $this->name->read(); } } if (isset($_GET['hix'])) { @unserialize($_GET['hix']); } else { highlight_file(__FILE__); } ``` Nhìn sơ qua biết được class `A` có function `read()` để đọc file `hello.txt` nhưng chúng ta muốn đọc file `flag.txt`. Đơn giản thôi chỉ cần thay đổi thuộc tính của đối tượng tạo từ class `A` là `filename` có giá trị là `flag.txt`. Nhưng làm sao để `read()` được thực thi thì chú ý trong class `B` có gọi `read()`: ``` public function __wakeup() { return $this->name->read(); } ``` Giả sử tạo đối tượng `$a` từ class `A`: `$a=new A('flag.txt');` Vậy `read()` thực thi khi xảy ra `$a->read()` vậy `$this->name` trong class `B` là đối tượng tạo từ class `A` (ở đây là `$a`). Vậy chỉ cần tạo đối tượng `$b` từ class `B` có thuộc tính `name` là `$a`: `$b=new B($a);` Muốn `read()` thực thi thì `__wakeup()` cũng thực thi. Đây là **magic method** thực thi khi thực hiện quá trình **deserialization** (hàm **unserialize()**). Vậy chỉ cần đưa chỉ cần đưa **serialized data** của `$b` vào biến `hix` từ GET. Bây giờ chỉ cần viết script tạo nó : ``` <?php class A { public $filename; public function __construct($filename) { $this->file$filename = $filename; } } class B { public $name; public function __construct($name) { $this->name = $name; } } $a = new A("flag.txt"); $b = new B($a); echo serialize($b); ``` ... Đó là 1 bài đơn giản và trong các bài CTF hay trong thực tế có thể có nhiều **gadgets** đòi hỏi chúng ta phải phân tích tìm con đường phù hợp để đạt được mục tiêu. Mình đã làm 1 bài để giải các bài dạng này ở <a href="https://hackmd.io/@chuong/php-pop-chain">đây</a> Ngoài ra có thể dùng tool <a href="https://github.com/ambionics/phpggc">PHPGGC</a> với POP chain có sẵn của nhiều thư viện. ### Overflow ### Phar Deserialization Phar (PHP Archive) là một định dạng gói cho phép phân phối các ứng dụng và thư viện bằng cách gói các file PHP và một số file khác. Nó giống như một file được nén lại từ nhiều file và thành phần khác. Cấu trúc của file Phar: - Stub: phần đầu của archive. Nó là một file PHP đơn giản. Phần này bắt buộc có và kết thúc bởi: `__HALT_COMPILER();` - manifest: chưuá thông tin về danh sách file, thư mục được lưu trữ trong phar và các thông tin như sau: ![](https://hackmd.io/_uploads/r1xn0q_P2.png) Ở phần cuối, nó lưu Meta-data dưới dạng serialized, vì vậy khi gọi đến file phar (`phar://`) thì các data này sẽ unserialize => vector tấn công. - File contents: nôi dung chính của file. - Signature (optional): kiểm tra tính toàn vẹn. Như vậy là nó cũng thuộc dạng POP chain. Tạo 1 file phar: - Tạo file `create_phar.php`, ví dụ như: ``` <?php class Example { // code exploit gadgets } $data = new Example(); $phar = new Phar("test.phar"); $phar->startBuffering(); $phar->addFromString('test.txt', 'text'); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->setMetadata($data); $phar->stopBuffering(); ?> ``` Dùng command để thực thi: `php -d "phar.readonly=0" create_phar.php` (Trong một số bài uploads file, nếu server kiểm tra các magic bytes ta có thể thêm nó phần đầu của `Stub` ) Sau khi được file `test.phar`, ta sử dụng nó vào input khai thác (các **filesytem functions**): `phar://path/to/test.phar` Tham khảo thêm: - https://sec.vnpt.vn/2019/08/ky-thuat-khai-thac-lo-hong-phar-deserialization/ - https://pentest-tools.com/blog/exploit-phar-deserialization-vulnerability