(Vietnamese) PHP deserialize, all-in-one what we need to know
My all-in-one post about PHP deserialization vulnerability, which explains detailed information about serialize, deserialize in PHP - how it works under the hood and some type of challenges I met
What is serialization and deserialization
Trong bài viết này mình sẽ phân tích về lỗ hổng Insecure Deserialization, cụ thể hơn là trong ngôn ngữ PHP. Mình sẽ để bài tìm hiểu về lỗ hổng này trong 2 ngôn ngữ Java và Python bên dưới.
Tổng hợp lab và solution mình tự build: https://drive.google.com/drive/folders/1f09smOeNkmn5hgdqcJ1Ws368kfqTTi-N?usp=sharing
Context
Trong lập trình và phát triển phần mềm, chúng ta sử dụng nhiều ngôn ngữ lập trình khác nhau, trong mỗi ngôn ngữ lại tồn tại nhiều cấu trúc dữ liệu, đối tượng hay các thuộc tính khác nhau. Điều này gây ra khó khăn trong quá trình lưu trữ và truyền tải dữ liệu song song giữa những ngôn ngữ, khi đó Serialization ra đời
Serialization là quá trình chuyển đổi một đối tượng với các thuộc tính thành dạng dữ liệu có thể lưu trữ (có thể lưu vào file hoặc cơ sở dữ liệu), hoặc truyền đi trong mạng. Tùy thuộc vào định dạng mà đối tượng được chuyển đổi, dữ liệu sau khi chuyển đổi có thể ở dạng binary hoặc dữ liệu có cấu trúc (XML, JSON,..)
Ngược lại với serialization, deserialization là quá trình chuyển ngược các chuỗi byte đó trở lại thành đối tượng hoặc cấu trúc dữ liệu gốc
Trong thực tế, ví dụ điển hình nhất cho 2 hành động này đó là chức năng save game. Khi tiến hành save game là ta đang tiến hành serialize toàn bộ dữ liệu hiện tại của người dùng như vật phẩm đang có trong túi, máu, mana, … Và khi load game lên thì sẽ khôi phục lại những tiến trình đã được lưu đó bằng việc giải mã serialized data
Root-cause
Lỗ hổng Insecure Deserialization nằm ở vị trí thứ 8 trên bảng xếp hạng OWASP. Lỗ hổng xảy ra khi dữ liệu đầu vào không tin cậy được đưa vào quá trình deserialize và chuyển hóa thành các đối tượng nguy hiểm. Dữ liệu đầu vào này sau quá trình deserialize có thể lợi dụng để thay đổi luồng xử lý của ứng dụng. Tùy vào tường trường hợp cụ thể, việc khai thác lỗ hổng này mang lại những hậu quả khác nhau từ Path Traversal, Arbitrary file read, SQL Injection,.. hay có thể dẫn đến RCE
Insecure Deserialization in PHP
Trong ngôn ngữ PHP, quá trình Serialization và Deserialization được hỗ trợ qua 2 hàm serialize() và unserialize()
Lỗ hổng Deserialization trong PHP hay với một tên gọi khác là PHP Object Injection lợi dụng việc unserialize các đối tượng kết hợp với các magic function để có thể thực hiện nhiều hành vi trái phép. Để tiến hành khai thác lỗ hổng này, cần sử dụng đến kỹ thuật Property Oriented Programming (POP)
Kỹ thuật POP lợi dụng các đoạn mã nguồn khác có sẵn trong chương trình (thường là các đối tượng có sẵn trong mã nguồn hay thư viện đi kèm) được gọi là gadget và kết hợp chúng lại với nhau thành một payload hoàn chỉnh (gadget chains) để khai thác.
Serialized data in PHP
Trong PHP, một dữ liệu sau khi trải qua quá trình serialize được gọi là serialized data, đây là một chuỗi lưu các thuộc tính theo dạng datatype - data, chẳng hạn như:
1
2
3
4
5
6
7
8
serialize(18); // i:18;
serialize(12.50); // d:12.5;
serialize(null); // N;
serialize(true); // b:1;
serialize(false); // b:0;
serialize('John Smith'); // s:10:"John Smith";
serialize(['a','b']); // a:2:{i:0;s:1:"a";i:1;s:1:"b";}
serialize(new stdClass()); // O:8:"stdClass":0:{}
Lấy ví dụ với chuỗi
1
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
=> Ta có thể hiểu serialized data trên đại diện cho một đối tượng User, trong đối tượng đó bao gồm 2 thuộc tính là username với giá trị vickie và status not admin
Unserialize() under the hood
Giờ chúng ta sẽ tìm hiểu nguyên nhân chính dẫn tới lỗ hổng Insecure Deserialization trong PHP, hàm unserialize(). Cụ thể là hàm này hoạt động như này, và vì sao dẫn tới lỗ hổng
Đầu tiên, ta cần phải hiểu hàm này sẽ làm gì với những dữ liệu đã bị serialized, thì cụ thể cách thức hoạt động của unserialize() trong php có thể được minh hoạ qua hình ảnh sau
Để hoàn thành việc khởi tạo lại dữ liệu từ serialized data, hàm unserialize() nhận vào 2 tham số là chuỗi dữ liệu cần khởi tạo lại string $data và một array $options. Trong đó, options chỉ có 2 giá trị là max_depth - int và allowed_class - array|boolean. Ở đây chú trọng vào việc khai thác, vì vậy ta chỉ cần tập trung vào option thứ 2
allowed_class có thể được truyền vào dưới dạng array hoặc boolean, cụ thể:
- Nếu là array => Tất cả các class nằm trong array đó sẽ được chấp nhận
- Nếu là boolean => False = không chấp nhận bất kỳ class nào, True = chấp nhận tất cả các class
Step 0.1 - Prerequisites
Trước tiên, cần chú ý rằng serialize sẽ lưu lại toàn bộ thuộc tính (properties) trong đối tượng nhưng không lưu lại các phương thức (methods)
Điều này có nghĩa là để có thể unserialize() một serialize data chứa object, cái class của object đó phải được định nghĩa hoặc load vào trước đó trong code. Nếu class này mà chưa từng xuất hiện, đối tượng mà muốn được unserialize này sẽ được khởi tạo vào __PHP_Incomplete_Class, trong này sẽ không có một phương thức nào và object này tạm thời là useless vì chỉ có dữ liệu chứ không hề có method nào
Step 1 - Object instantiation
Đến với bước đầu tiên khi ta đã hiểu được điều kiện tiên quyết của unserialize(), khi này hàm này sẽ bắt đầu khởi tạo lại đối tượng trong bộ nhớ
- Nhận vào một serialized data, trong đó chứa class của object cần tái tạo lại và những properties cần gán giá trị
- Sử dụng những data trên để tạo một bản copy của đối tượng ban đầu (chưa bị serialize)
- Khi đã tìm được class của object cần tái tạo, gọi
__wakeup()nếu có và thực thi method này. Cụ thể hơn về magic method__wakeup()mình sẽ trình bày rõ hơn bên dưới
Step 2 - Use the object and object destruction
Sau khi đã tái tạo lại được dữ liệu từ serialized string, chương trình sẽ có thể sử dụng những dữ liệu đó để thực hiện những hành động khác
Cuối cùng, khi không còn tham chiếu nào trỏ tới deserialize object thì magic method __destruct() sẽ được gọi
Magic Methods in PHP
Trong PHP, người lập trình có thể định nghĩa một hàm được gọi tự động. Các hàm như vậy không cần một hàm khác gọi để thực thi. Với tính năng này, có một vài hàm trong PHP được gọi là “magic functions” hay”magic methods”, những hàm đó được tự động gọi trong một số ngữ cảnh khác nhau
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__construct(): Phương thức này được tự động gọi khi class đó được gọi
__destruct(): Phương thức này được tự động gọi khi không còn bất kỳ tham chiều nào đến class đó
__call($fun, $arg): Phương thức này được tự động gọi khi một phương thức không xác định hoặc không thể truy cập của class đó được gọi
__callStatic($fun, $arg): Phương thức này được gọi khi một phương thức không xác định hoặc không thể truy cập được gọi theo cách tĩnh
__get($property): Phương thức này được tự động gọi khi một thuộc tính không tồn tại hoặc không được phép truy cập được truy cập từ bên ngoài class
__set($property, $value): Phương thức này được sử dụng để set các giá trị cho các thuộc tính của class được khởi tạo động bởi overloaded property PHP
__isset($content): Phương thức này sẽ được tự động gọi trong khi gọi isset() hoặc empty()
__unset($content): Phương thức này sẽ được tự động gọi trong khi gọi reset()
__sleep(): Phương thức này được gọi đầu tiên trong khi thực thi serialize(). Nó trả về mảng thuộc tính của đối tượng sau khi đã xử lý các đối tượng class PHP trước khi serialize()
__wakeup(): Phương thức này được tự động gọi trong khi unserialize() được thực thi. Nó sẽ xử lý thuộc tính và tài nguyên của đối tượng trước khi gọi unserialize()
__toString(): Phương thức này sẽ được tự động gọi trong khi sử dụng phương thức echo,.. để in,.. một đối tượng trực tiếp. Dự kiến sẽ trả về một giá trị chuỗi trong khi sử dụng các instance của class với các câu lệnh in,... PHP
__invoke(): Phương thức này sẽ được tự động gọi trong khi cố gắng gọi một đối tượng theo cách gọi hàm
__set_state($array): Phương thức này được tự động gọi trong khi gọi var_export(). Nó trả về mảng các thuộc tính của đối tượng với biến là các giá trị tương ứng
__clone(): Phương thức này được tự động gọi khi đối tượng được sao chép
__debugInfo() Phương thức này được tự động gọi bởi var_dump() trong khi kết xuất một đối tượng để lấy các thuộc tính sẽ được hiển thị
Giờ mình sẽ phân tích một vài hàm điển hình là nguy hiểm nếu có user input ở trong, cụ thể như sau:
__construct()
Constructor sẽ giúp chúng ta khởi tạo các giá trị của properties ngay trong khi tạo object. Mặc định, constructor luôn được gọi khi khởi tạo một object
Trong PHP nó có syntax là __construct(), vẫn lấy ví dụ bên trên nhưng thay vì viết một hàm set_name rồi khi muốn gán lại phải call rườm rà, ta sẽ làm như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Trainer {
public $name;
public $bags;
public $ages;
function __construct($name, $bags, $ages){
$this->name = $name;
$this->bags = $bags;
$this->ages = $ages;
}
}
$testTrainer = new Trainer("anhcd", "bags are here", 20);
echo $testTrainer->name;
echo "\n";
echo $testTrainer->ages;
?>
__destruct()
Hàm huỷ được gọi ngay khi không còn một tham chiếu nào trỏ tới đối tượng hiện tại, hoặc là trong quá trình tắt script (shutdown sequence). Nghe hơi khó hiểu nhưng 2 case này có thể được minh hoạ trong ví dụ sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Trainer {
public $name;
function __construct($name){
$this->name = $name;
}
function __destruct(){
echo "Destruct was here";
}
}
# Case 1: Khi khong con reference toi doi tuong testTrainer
$testTrainer = new Trainer("anhcd1");
unset($testTrainer);
# Case 2: Sau khi hoan thanh xong, tu dong goi __destruct()
$testTrainer2 = new Trainer("anhcd2");
?>
__sleep() and __wakeup()
Khi sử dụng hàm serialize(), hàm này sẽ luôn kiểm tra xem trong class liệu có tồn tại hàm __sleep() không, nếu có thì hàm này sẽ được ưu tiên thực thi trước cả quá trình serialize
Trong thực tế, __sleep() được sử dụng khi lưu trữ (commit pending data), chẳng hạn như những dữ liệu đã được chỉnh sửa mà chưa được lưu lại trước khi serialize, ta có thể sử dụng magic method này để lưu lại những trạng thái đó trước khi tạo ra serialized data (điển hình như lưu trạng thái game)
Ngoài ra, khi dữ liệu cần serialize ít mà các properties lại quá nhiều, ta sẽ chỉ định rõ cần phải serialize cái gì bằng cách return về mảng đó trong __sleep()
Ngược lại với serialize(), khi sử dụng unserialize(), hàm này sẽ lại tìm kiếm __wakeup() trong class. Nếu có xuất hiện, phương thức __wakeup() sẽ có tác dụng là:
- Tái tạo lại mọi thứ mà
__sleep()đã đóng (chẳng hạn như tiến trình game trước đó đã lưu) - Khởi động và đảm bảo object sẽ ở trạng thái sẵn sàng ngay sau khi unserialize
Theo những gì mình hiểu, về bản chất
unserialize()sẽ luôn phải khởi tạo lại Object
__toString()
Khi một đối tượng được gọi hoặc sử dụng dưới vai trò là chuỗi (string), phương thức __toString() sẽ được thực thi. Lưu ý rằng method này luôn phải return một chuỗi.
Magic method này thường được invoke khi có những lệnh in ra như echo, print, …
Ngoài ra còn có thể gặp trong những trường hợp sử dụng so sánh, khi 2 thứ được so sánh với nhau mà một cái là string một cái là object chẳng hạn thì __toString() của class chứa object đó sẽ được gọi để thực thi sau đó mới tiến hành so sánh
=> Như vậy, ta có thể kết luận các magic method, đặc biệt hơn là 3 method __wakeup(), __toString(), __destruct() quan trọng trong việc khai thác lỗ hổng PHP Object Injection vì chúng được tự động gọi khi unserialize hoặc huỷ object, cho phép attacker thực thi code mà không cần gọi trực tiếp
Từ đó ta cũng có thể suy ra được điều kiện, muốn khai thác PHP Object Injection, ta cần:
- Đối tượng có thể kiểm soát có sử dụng magic function
- Tồn tại gadget chains trong mã nguồn để kiểm soát quá trình
unserialize()
Một số dạng khai thác
Sửa đổi serialized data
Theo mình thấy đây là dạng bài đơn giản nhất dành cho người mới tiếp cận lỗ hổng này, một số ví dụ điển hình như:
Ví dụ 1
Chương trình unserialize một chuỗi nhận giá trị từ người dùng, và sử dụng nó để cấp quyền cho họ. Dưới đây là một cách mình mô phỏng lại hành động kiểm tra đó, mặc dù mình cũng không nghĩ là việc này có tồn tại trong thực tế
1
2
3
4
5
6
7
8
9
10
11
12
13
class User{
public $isAdmin = false;
}
if (isset($_COOKIE["auth"])){
$data = unserialize($_COOKIE["auth"]);
if ($data->isAdmin === true){
include("admin.php");
}
else {
include("guest.php");
}
}
Ví dụ 2
Chương trình có một hàm với chức năng đọc file, và unserialize user input. Trong trường hợp này dẫn tới LFI
1
2
3
function read(){
echo file_get_contents($this->filename);
}
Tính tới hiện tại đó là 2 dạng bài sửa đổi serialized data mà mình gặp, đôi khi cũng sẽ có một vài biến thể phải kết hợp thêm với các lỗi khác như Loose Comparison để có thể khai thác thành công, mình cũng có trình bày về case đó tại đây. Còn giờ thì chúng ta sẽ đi tới dạng tiếp theo
POP Chain
Code Reuse Attack hay POP (Property Oriented Programming) chain 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 nhất trong dạng tấn công này là việc hiểu và tận dụng việc invoke các magic methods mà mình đã tìm hiểu ở các phần nội dung bên trên. Ngoài ra thì cũng cần chú ý tới các hàm builts-in nguy hiểm của PHP bao gồm:
- Command Execution:
execpassthrupopensystemcall_user_func_array
- File Access:
file_put_contentsfile_get_contentsunlink
Dưới đây là một lab đơn giản, chỉ sử dụng qua 1 lần gọi magic method để giải mình viết ra. Nhưng trong thực tế để xử lý những bài POP chain chắc không chỉ dừng lại ở 1 2 object như thế này
Source: Lab1 trong file zip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TmpClass{
public $tmp;
function __wakeup(){
$this->tmp->getFile();
}
}
class Read{
public $filename;
function getFile(){
echo file_get_contents($this->filename);
}
}
$data = unserialize($user_input);
Với mục tiêu là LFI, tư duy để sinh payload cho bài này là làm như nào đó để $this->filename trong getFile nhận giá trị mình mong muốn. Dễ thấy getFile được gọi bởi method __wakeup của TmpClass, và method này sẽ mặc định được gọi nếu có tồn tại khi sử dụng unserialize()
Như vậy, xâu chuỗi lại các vấn đề:
- LFI xảy ra khi
getfile()được thực thi, tức giả sử có 1 đối tượng$event = new Read()thì phải có$event->getfile() - Để có
$event->getfile(), ta nhận thấy ở trên có$this->tmp->getFile(). Tức giờ ta sẽ làm sao để giá trị biếntmptrong class TmpClass nhận giá trị là$eventvà__wakeup()phải được call (đã thoả mãn) => Done
Với ý tưởng như vậy, ta có thể viết lại code để sinh payload như sau:
1
// payload: O:8:"TmpClass":1:{s:3:"tmp";O:4:"Read":1:{s:8:"filename";s:11:"/etc/passwd";}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class TmpClass{
public $tmp;
function __construct($tmp){
$this->tmp = $tmp;
}
}
class Read{
public $filename;
function __construct($filename){
$this->filename = $filename;
}
}
$tmp = new Read("/etc/passwd");
$data = new TmpClass($tmp);
$solve = serialize($data);
echo $solve;
?>
Overflow
Mình gặp dạng này khi làm root-me, nhưng hiện tại mình vẫn chưa thể tự giải được bài này
PHAR Deserialization
https://sec.vnpt.vn/2019/08/ky-thuat-khai-thac-lo-hong-phar-deserialization
What is a PHAR file?
PHp ARchive - PHAR file trong PHP tương tự như Jar file trong Java, nó là một package format cho phép ta gói nhiều các thư viện, code, hình ảnh, … vào một tệp có cấu trúc gọi là PHAR file. Một PHAR file có thể bao gồm:
- Stub: Là một file PHP nhưng chứa đoạn code sau:
<?php __HALT_COMPILER(); - A manifest: Một đoạn miêu tả khái quát nội dung sẽ có trong file. Ngoài ra còn có thể có serialized data, vì vậy nên đây sẽ là vị trí quan trọng để nhát nữa chúng ta tìm hiểu và khai thác
- A source file: Nội dung chính của PHAR
- An optional signature: Sử dụng để kiểm tra tính toàn vẹn
Như mình đã đè cập, điểm đáng chú ý trong cấu trúc của file PHAR nằm ở mục manifest. Theo php.net thì phần manifest có thể chứa các thông tin sau:
Những điều thú vị khác là:
- Wrapper
phar://không kiểm tra phần mở rộng tệp khi khai báo luồng. Tức nếu bình thường chỉ được upload PNG lên bằnghttps://, thì nếu dùngphar://PHP sẽ không check extension của file nữa mà chỉ tập trung vào kiểm tra xem đây có phải 1 file PHAR hợp lệ hay không - Nếu một
filesystem functiongọi đến một PHAR file thì tất cả các serialized data trên sẽ tự động đượcunserialize
DƯới đây là những hàm trigger lỗi hổng đó:
Như vậy, để khai thác dạng bài PHAR Deser ta cần chú ý vào ba yếu tố:
- Tìm được POP chain trong trong source code cần khai thác
- Đưa được Phar file vào đối tượng cần khai thác
- Tìm được entry point, đó là những chỗ mà các filesystem function gọi tới các Phar file do người dùng kiểm soát
Mình cũng chưa gặp một challenge nào sử dụng dạng khai thác này khi tìm hiểu, nhưng cảm giác nó cũng phổ biến nên sẽ gặp sớm và viết lại
Xem thêm write-up của mình cho các challenge PHP Object Injection tại đây: https://hackmd.io/@cKZOTBozR4moJ4nC-OXLpQ/BJHuDl9Ilg








