前言
在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 – 小胖虎
来源:freebuf.com 2019-07-22 15:05:01 by: 斗象智能安全平台
请登录后发表评论
注册