挖掘暗藏ThinkPHP中的反序列利用链

前言

在Blackhat2018,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,使用phar伪协议可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞,极大地扩展了PHP反序列化的攻击面并且开源了新工具PHPGGC,PHPGGC可以针对十数个PHP流行框架进行了反序列化利用链输出。

于是本文由此对中国最流行的PHP框架之一Thinkphp进行了反序列化利用链挖掘。

预备知识

1.PHP反序列化原理

PHP反序列化就是在读取一段字符串然后将字符串反序列化成php对象。

2.在PHP反序列化的过程中会自动执行一些魔术方法

方法名 调用条件
__call 调用不可访问或不存在的方法时被调用
__callStatic 调用不可访问或不存在的静态方法时被调用
__clone 进行对象clone时被调用,用来调整对象的克隆行为
__constuct 构建对象的时被调用;
__debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct 明确销毁对象或脚本结束时被调用;
__get 读取不可访问或不存在属性时被调用
__invoke 当以函数方式调用对象时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
__set 当给不可访问或不存在属性赋值时被调用
__set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString 当一个类被转换成字符串时被调用
__unset 对不可访问或不存在的属性进行unset时被调用
__wakeup 当使用unserialize时被调用,可用于做些对象的初始化操作

3.反序列化的常见起点

__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

4.反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this->$func();

5.反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

6.Phar反序列化原理以及特征

phar://伪协议会在多个函数中反序列化其metadata部分
受影响的函数包括不限于如下:

copy,file_exists,file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,
fileinode,filemtime,fileowner,fileperms,
fopen,is_dir,is_executable,is_file,is_link,is_readable,is_writable,
is_writeable,parse_ini_file,readfile,stat,unlink,exif_thumbnailexif_imagetype,
imageloadfontimagecreatefrom,hash_hmac_filehash_filehash_update_filemd5_filesha1_file,
get_meta_tagsget_headers,getimagesizegetimagesizefromstring,extractTo

(Thinkphp框架中暂未发现,略有遗憾)

漏洞挖掘

1.安装Thinkphp 5.1.37环境

首先去github下载Thinkphp的源码,现在Thinkphp已经分为2个部分,
https://github.com/top-think/framework/tags
https://github.com/top-think/thinkphp/tags
下载5.1.37(最新版)对应的版本号
将framework改名为为thinkphp放到think-5.1.37中

2.寻找反序列化的起始点

使用idea打开该文件夹,开启xdebug
直接Ctrl+Shift+F搜索 “__destruct(” 看到此处有其他方法调用,我们继续跟进

发现 Windows->removeFiles(); 中使用了 file_exists 方法,而且 $files 可控

class Windows extends Pipes
{

 /** @var array */
 private $files = [];
.....
public function __destruct()
{
 $this->close();
 $this->removeFiles();
}
/**
 * 删除临时文件
 */
private function removeFiles()
{
 foreach ($this->files as $filename) {
 if (file_exists($filename)) {
 @unlink($filename);
 }
 }
 $this->files = [];
}

查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用

3.寻找反序列化的中间跳板

下面就要求寻找一个实现了__toString()方法的对象来作为跳板

此处thinkphp\library\think\model\concern\Conversion.php存在跳板可能

trait Conversion
{
 protected $visible = [];
 protected $hidden = [];
 protected $append = [];
....

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

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

toArray() 函数中寻找一个满足条件的:
$可控变量->方法(参数可控)
这样可以去触发某个类的__call方法,
找到符合条件的一处,其中 “$relation” 和 “$name” 都是可控变量,$name需要为数组

$relation->visible($name);

4.寻找反序列化代码执行点

下面我们需要寻找一个类满足以下2个条件

1.该类中没有”visible”方法

2.实现了__call方法

直接查找 “public function __call”

一般PHP中的__call方法都是用来进行容错或者是动态调用,所以一般会在__call方法中使用

__call_user_func($method, $args)

__call_user_func_array([$obj,$method], $args)
但是 public function __call($method, $args) 我们只能控制 $args,所以很多类都不可以用
经过查找发现 think-5.1.37/thinkphp/library/think/Request.php 中的 __call 使用了一个array取值的

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

这里的 $hook是我们可控的,所以我们可以设计一个数组 $hook= {“visable”=>”任意method”}
但是这里有个 array_unshift($args, $this); 会把$this放到$arg数组的第一个元素这样我们只能

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

如下图

这种情况是很难执行命令的,但是Thinkphp作为一个web框架,
Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)
所以可以尝试覆盖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);

            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是一个数组(由于前面判断条件 is_array($name)),(string)$name 会报错终止程序,所以不能直接使用这个函数
继续查找调用input方法的的函数

这里发现一个函数 public function param($name = ”, $default = null, $filter = ”),如果能满足$name为字符串,就可以控制变量代码执行了
所以继续向上查找使用了param的方法

但是PHP有个特性,一个函数可以接收任意数量参数,超出的部分可以自动忽略
这里就发现isAjax/isPjax方法可以满足param的第一个参数为字符串,因为$this->config也是可控的

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

5.构造反序列化利用链

攻击链如下图所示

6.漏洞利用条件

使用的 ThinkPHP 5.1.X框架的程序中,满足以下任意条件:
1. 未经过滤直接使用反序列化操作
2. 可以文件上传且文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤

POC:
此漏洞仅影响 Thinkphp 5.1.X

漏洞还未修复,已提交至官方,poc暂不披露。

参考

https://github.com/ambionics/phpggc
https://www.cnblogs.com/iamstudy/articles/thinkphp_5_x_rce_1.html
https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html
https://p0sec.net/index.php/archives/114/
https://paper.seebug.org/680/

作者:斗象能力中心 TCC – 小胖虎

评论(4)

[…] https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用链/ […]

匿名

2019/09/08 18:26
php原生代码里面的异常处理类存在tostring调用点,实际使用时很好用

test

2019/08/15 15:00
在Conversion.php中,$relation = $this->getAttr($key); $relation->visible($name);这里$relation是怎么控制他的值的,看了下,他的值来源于$this->data,但是$this->data属于私有属性。作者方便解答下吗?

能力中心

2019/08/23 16:58
反序列化可以控制类属性,无论是private还是public PHP反序列化由浅入深 - 先知社区 https://xz.aliyun.com/t/3674

发表评论

captcha