好久没看 PHP 的东西了,上个月爆了个 ThinkPHP3.2.x RCE漏洞 (本质其实是文件包含 ),刚好跟着分析一下。
环境
1 | git clone https://github.com/top-think/thinkphp.git |
放到 web 目录下访问,会直接在 Application
目录下面自动生成公共模块 Common
、默认模块 Home
和Runtime
运行时目录
修改 Index 控制器
Application/Home/Controller/IndexController.class.php
1 | <?php |
把 Debug 关了,Debug 会影响日志的路径
复现
漏洞主要是文件包含,通过构造错误请求,把 payload 的写入日志文件,接着去包含日志文件从而 RCE
构造一个错误的请求访问 http://host-web/index.php?m=--><?=phpinfo();?>
日志路径 Application/Runtime/Logs/Common/21_09_06.log,注意访问的时候用 bp 去访问,浏览器访问的话浏览器会自动 url 编码
访问 https://host-web/index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/21_09_06.log
分析
说一下 thinkphp 4个视图方法
1 | display(); |
- display
1 | display('[模板文件]'[,'字符编码'][,'输出类型']) |
模板文件的写法支持下面几种:
不带任何参数 | 自动定位当前操作的模板文件 |
---|---|
[模块@][控制器:][操作] |
常用写法,支持跨模块 模板主题可以和theme方法配合 |
完整的模板文件名 | 直接使用完整的模板文件名(包括模板后缀) |
fetch
fetch方法的用法除了不需要指定输出编码和类型外其它和display基本一致
模板文件的调用方法和 display 方法完全一样,区别就在于 fetch 方法渲染后不是直接输出
show
1
show('渲染内容'[,'字符编码'][,'输出类型'])
渲染内容,例如
1
2
3$this->show($content);
// 也可以指定编码和类型
$this->show($content, 'utf-8', 'text/xml');assign
如果要在模板中输出变量,必须在在控制器中把变量传递给模板,系统提供了 assign 方法对模板变量赋值,无论何种变量类型都统一使用assign赋值。
1
$this->assign('name',$value);
1 | $array['name'] = 'thinkphp'; |
再来看漏洞代码
1 |
|
$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
当 $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;
接着调用 $this->display() 渲染模版。
在 77 行进入 $this->fetch() 方法解析模版
$content 为空直接往下走,133 行判断了一下是否使用 php 原生的模版引擎,直接看 else 的部分
把一些参数放到了数组 $params 里,其中 ‘var’ => $this->tVar ,这里用到了前面 $this->tVar 也就是我们传入的 $value。
接着调用 Hook::listen(‘view_parse’, $params);
都是一写记录日志的代码,重点看 99 行 $result = self::exec($name, $tag, $params);
这个方法是用来执行插件的,这里会去调用 Behavior\ParseTemplateBehavior 了类的 run 方法,
传入的参数为 $params 其中包含了 $this->tVar
1 | $addon = new $name(); |
判断了一下是否采用 Think 的模版引擎,接着对一些参数进行判断,
最后会调用 $tpl->fetch($_content, $_data['var'], $_data['prefix']);
这里的 $_data 就是我们调用 run 方法时传入的 $params , $_data[‘var’] 其实就是 $this->tVar 也就是我们传入的 $value。
把我们传入的 $_data[‘var’] 赋值给 $this->tVar 接着去调用 Storage::load($templateCacheFile, $this->tVar, null, 'tpl');
接着就是最终触发漏洞的地方,第二个参数 $vars ,其实就是传入的 $this->tVar,也就是我们传入的 $value。
总结
所以这个漏洞本质上就是一个任意文件包含漏洞,利用的前提是 assign() 方法的第一个参数可控。但是好像很少有人会这么写代码,github 上翻了一些基于 thinphp3.2.x 的项目没找到能利用的。。。