ThinkPHP5之文件包含审计分析(五)

说明

该文章来源于同事lu2ker转载至此处,更多文章可参考:https://github.com/lu2ker/

参考链接:Mochazz/ThinkPHP-Vuln/

影响版本:5.0.0<=ThinkPHP5<=5.0.18 、5.1.0<=ThinkPHP<=5.1.10

测试环境:PHP7.3.4、Mysql5.7.26、TP5.0.18

0x00 环境准备

关于tp默认的模板调用路径,全局搜了一下后发现应该是在这里:
thinkphp\library\think\view\driver\Think.php#41

public function __construct($config = [])
{
	$this->config = array_merge($this->config, $config);
	if (empty($this->config['view_path'])) {
	$this->config['view_path'] = App::$modulePath . 'view' . DS;
}
	$this->template = new Template($this->config);
}

这里的构造函数生成的,默认位置就在application\index\view\。实际上按照习惯也应该是和模型(M),控制器(C)同级的。

之后呢,又在同类下的parseTemplate方法中132行,根据控制器名称使用默认规则(即同名)给$template赋值,然后又拼接上默认的模板文件后缀.html就好了。:

$template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action());
........
return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');

这就是为什么要创建创建 application/index/view/index/index.html文件

0x01 测试代码

<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller
{
    public function index()
    {
        $this->assign(request()->get());
        return $this->fetch(); // 当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html
    }
}

开始需要assign方法进行模板变量赋值,然后fetch方法渲染。应该是比较常见的写法。

正式开始之前,还要再public下放一张图片马,模拟已经上传好了。

访问:http://www.tp5018.qwe/index.php/index/index?cacheFile=1.jpg 即可

0x02 代码分析

request的get方法不是这次的重点,只是获取get传入的参数。所以直接忽略。首先会来到

thinkphp\library\think\Controller.php#144也就是Controller类的assign方法,

并直接调用thinkphp\library\think\View.php#92View类的的assign方法

public function assign($name, $value = '')
    {
        if (is_array($name)) {
            $this->data = array_merge($this->data, $name);
        } else {
            $this->data[$name] = $value;
        }
        return $this;
    }

首先判断是不是数组,因为tp的request->get()返回的就是一个数组($data变量),数组内每一个元素存放一个参数的键值对。所以会执行array_merge()方法,array_merge在手册中的解释是将一个或多个数组的单元合并起来,一个数组中的值附加在前一个数组的后面。返回作为结果的数组。

这些还只是正常的数据传递过程,和文件包含还没有什么关系,接着往下看fetch方法的渲染。

  • 在thinkphp\library\think\View.php#148

进入了View类的fetch方法:其中,我们从158行开始:

try {
            $method = $renderContent ? 'display' : 'fetch';
            // 允许用户自定义模板的字符串替换
            $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string'));
            $this->engine->config('tpl_replace_string', $replace);
            $this->engine->$method($template, $vars, $config);
        } catch (\Exception $e) {
            ob_end_clean();
            throw $e;
        }

这个try,首先给 m e t h o d 设置成了 f e t c h (因为 V i e w 类的 f e t c h 方法默认 method设置成了fetch(因为View类的fetch方法默认 method设置成了fetch(因为View类的fetch方法默认renderContent是个false),然后执行的代码的作用就是取来一些渲染视图需要的文件位置,比如:

在这里插入图片描述

重要的是,注意到$this->engine->$method($template, $vars, $config);这条语句,因为$method已经被赋值过了,所以它是会去调用tp模板引擎的fetch方法(在Think类),而Think类下的fetch在最后又调用了template的fetch方法。也就是最后会走到这里:

  • thinkphp\library\think\Template.php#160

到这里先停一下,回想一下传入的cacheFile=1.jpg现在是个什么状态,在哪个变量里

  1. 刚开始通过requets->get()获取,作为$name传入assign方法
  2. 两层assign处理后,由Controller下的assign返回了 t h i s ,参数在 this,参数在 this,参数在this->view->data数组里
  3. 然后在View类中的fetch中赋值给了$vars(这里的fetch是第二层,层层深入)
  4. v a r s 做为 T h i n k 类下 f e t c h 方法的 vars 做为 Think类下fetch方法的 vars做为Think类下fetch方法的data[]参数传入(这里的fetch是第三层)
  5. d a t a 做为 T e m p l a t e 类下 f e t c h 方法的 data 做为Template类下fetch方法的 data做为Template类下fetch方法的vars[]传入(这里是最后一层fetch,模板引擎的核心)

变量的传递过程理清后来看最重要的代码:

在这里插入图片描述

首先关注参数, t e m p l a t e 在最开始我们就知道它存放的是模板文件的位置, template在最开始我们就知道它存放的是模板文件的位置, template在最开始我们就知道它存放的是模板文件的位置,vars是我们的图片马文件参数,$config在调用过程中,一直是默认空的。

再经过了176行的模板文件解析之后进入了if代码块,首先把自动生存的缓存php文件路径给了 c a c h e F i l e 变量,接着就调用了 s t o r a g e − > r e a d ( ) ,并将 cacheFile变量,接着就调用了storage->read(),并将 cacheFile变量,接着就调用了storage>read(),并将data和$cacheFile一起传入了,跟入:

  • 来到thinkphp\library\think\template\driver\File.php#45
public function read($cacheFile, $vars = [])
    {
        if (!empty($vars) && is_array($vars)) {
            // 模板阵列变量分解成为独立变量
            extract($vars, EXTR_OVERWRITE);
        }
        //载入模版缓存文件
        include $cacheFile;
    }

注意到其中调用了extract函数来处理$vars,在官方手册中extract解释如下:

本函数用来将变量从数组中导入到当前的符号表中。

检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突。

意思就相当于全局是变量注册,可以看我这里的记录指出了对用户可控的输入使用该函数可能会造成风险。尤其是第二个参数:默认EXTR_OVERWRITE,如果有冲突,覆盖已有的变量。

了解了这个函数,就明白了在URL栏传入的?cacheFile=1.jpg实际上就是为了这里的变量覆盖!在变量覆盖之后,程序立马就include了$cacheFile,也就是1.jpg图片马。

至此完成文件包含。

0x03 总结

这个漏洞个人认为文件包含是小事,因为重点是在最后的变量覆盖。而且那条代码还是一个经典的变量覆盖漏洞案例。如果是从审计来看,定位到extract函数,注意到这里设置了EXTR_OVERWRITE,那么只要第一个参数可控就可以完成变量覆盖了,include只是完美利用了变量覆盖这个功能。全局搜索也是很容易搜到调用位置的,这里有两处,另外一处是display方法,试了下也是可以完美利用的,它在调用View类下的fetch方法的时候会传入$renderContent=true用来区分fetch方法:

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jeromeyoung666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值