前言
在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部分
受影响的函数包括不限于如下:
(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 可控
查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用
3.寻找反序列化的中间跳板
下面就要求寻找一个实现了__toString()方法的对象来作为跳板
此处thinkphp\library\think\model\concern\Conversion.php存在跳板可能
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取值的
这里的 $hook是我们可控的,所以我们可以设计一个数组 $hook= {“visable”=>”任意method”}
但是这里有个 array_unshift($args, $this); 会把$this放到$arg数组的第一个元素这样我们只能
如下图
这种情况是很难执行命令的,但是Thinkphp作为一个web框架,
Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)
所以可以尝试覆盖filter的方法去执行代码
寻找使用了过滤器的所有方法
发现input()函数满足条件
但是这个方法不能直接使用,$name是一个数组(由于前面判断条件 is_array($name)),(string)$name 会报错终止程序,所以不能直接使用这个函数
继续查找调用input方法的的函数
这里发现一个函数 public function param($name = ”, $default = null, $filter = ”),如果能满足$name为字符串,就可以控制变量代码执行了
所以继续向上查找使用了param的方法
但是PHP有个特性,一个函数可以接收任意数量参数,超出的部分可以自动忽略
这里就发现isAjax/isPjax方法可以满足param的第一个参数为字符串,因为$this->config也是可控的
5.构造反序列化利用链
攻击链如下图所示
6.漏洞利用条件
使用的 ThinkPHP 5.1.X框架的程序中,满足以下任意条件:
1. 未经过滤直接使用反序列化操作
2. 可以文件上传且文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
POC:
此漏洞仅影响 Thinkphp 5.1.X
参考
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 – 小胖虎