本文来自i春秋作者:索马里的海贼

前言

看了版主 jing0102 的{代码审计思路 (通读+审计) Mlecms(中危漏洞/不简单), 感觉挺有意思 于是也回去下了一套代码看看 不得不说小众CMS的开发能力、安全意识跟大厂商还是有不少差距的 限于篇幅 不是关键部分就不贴代码了

一、发现隐患

拿到一套源码 首先得找到下手的地方,不管是不是新手 我都建议从index.php开始。 index.php做为固定主页面,里面肯定包含了整套系统的配置读取,初始化等内容,跟随这些包含的内容或者文件 就能大致了解整套系统的处理框架 流程等信息 这些信息在审计中都是非常重要的。经常有刚入门的审计同学不知道怎么才能加载某个文件,或者明明发现某个地方存在问题,却不知道如何访问去触发,这都是对流程不熟悉的结果。

感觉说的废话都能出一本书了,接下来直接来看看今天的主角mlecms index.php 中包含了一个 inc/include/ 目录下的header.php
而 header.php 又包含了 common.inc.php common.inc.php 又包含了 globals.php .

这3个文件都是用来初始化站点的数据,在看到globals.php的时候 发现有这么一段

foreach(array('_GET','_POST','_COOKIE') as $_request){  
        foreach($$_request as $i => &$n){
                ${$i} = daddslashes($n);
        }
}

接触过代码审计的人应该很熟悉,这是一段伪全局的代码。很多流行cms都会用,也出过不少问题,dz dedecms都在伪全局上吃过苦头。

这里并没有对变量名进行判断就直接用双$初始化了变量,还记得DEDECMS的覆盖$GBLOBAS超全局变量导致的getshell么

好在这套系统并没有用$GBLOABS来做什么文章,而且变量的值都经过了daddslashes做了转义。那如果是$_FILES呢~

二、从隐患开始

正常的流程中$_FILES变量是当产生用户上传动作时一个系统初始化的数组

$_FILES['userfile']['name'] //客户端机器文件的原名称。
$_FILES['userfile']['type'] //文件的 MIME 类型,如果浏览器提供此信息的话。一个例子是“image/gif”。不过此 MIME 类型在 PHP 端并不检查,因此不要想当然认为有这个值。
$_FILES['userfile']['size'] //已上传文件的大小,单位为字节。
$_FILES['userfile']['tmp_name'] //文件被上传后在服务端储存的临时文件名。
$_FILES['userfile']['error'] //和该文件上传相关的错误代码。此项目是在 PHP 4.2.0 版本中增加的。

当正常上传的时候 数组中的tmp_name值是不可控的,但因为上面的提到隐患,$_FILES变量可以通过GPC提交来覆盖了。 当然$_FILES可控在严谨的上传流程里也不一定能造成很大的危害 我们就来看看这套系统的上传流程 搜索$_FILES 有4个文件使用了这个变量 去掉后台功能和ckeditor 找到了 inc/class/avatar.class.php
看名字应该是跟头像上传有关,来看看具体内容 inc/class/avatar.class.php 行34

//这里的注释等下回来看
public function onuploadavatar() {  
                @header("Expires: 0");
                @header("Cache-Control: private, post-check=0, pre-check=0, max-age=0", FALSE);
                @header("Pragma: no-cache");
                $this->init_input($_GET['agent']);
                $uid = $this->input['uid'];  //uid来自input数组 input数组来自init_input()函数
                if(empty($uid)) {
                        return -1;
                }
                if(empty($_FILES['Filedata'])) {
                        return -3;
                }
                list($width, $height, $type, $attr) = getimagesize($_FILES['Filedata']['tmp_name']); //这里调用getimagesize函数来检查文件内容
                $imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
                $filetype = $imgtype[$type]; //限定了文件后缀来自$imgtype数组
                $tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //临时保存文件名 [固定]+uid+文件后缀 
                file_exists($tmpavatar) && @unlink($tmpavatar); // 如果已经存在 先删除
                if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) { 
                        @unlink($_FILES['Filedata']['tmp_name']); //如果移动成功 就删了原文件
                        list($width, $height, $type, $attr) = getimagesize($tmpavatar); //再次调用getimagesize函数检查移动后的文件
                        if($width < 10 || $height < 10 || $type == 4) { //如果长度宽度不符合要求 或者$type=4(我记得是swf文件好像)  就删掉目标文件
                                @unlink($tmpavatar);
                                return -2;
                        }
                } else {
                        @unlink($_FILES['Filedata']['tmp_name']); //移动失败  也删掉原文件
                        return -4;
                }
                global $config;
                $avatarurl = $config['url'].'inc/tmp/other/member_'.$uid.$filetype; 
                return $avatarurl;
        }

看到了这句

if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) {  

move_upload_file函数会在移动文件之前检查文件是否为合法的上传临时文件,如果想搞事,伪造的tmp_name是不会通过函数检查的

但copy就不一样了 不管你来源 不管你目的 直接给你怼过去。 再看看上面这句 两个函数都尝试了 所以如果我们伪造$_FILES['Filedata']['tmp_name']=/etc/passwd就是一个妥妥的任意文件读取了 更进一步 如果伪造$_FILES['Filedata']['tmp_name']=http://xxx.com/shell.txt 是不是就能getshell了呢

来看看程序的流程(减少篇幅 回去看上面代码段的注释) 可以看到 不管哪个流程 最终都会删掉原文件(也就是我们伪造的$_FILES['Filedata']['tmp_name'])你应该不想读了个数据库配置信息导致整个网站瘫痪吧。而且这里保存的文件后缀来自数组 并不能随意伪造来getshell。

到这里真的没办法了吗?

三、真的没办法了吗?

真的没办法了吗?当然不,代码审计就是要有死磕的精神,你拦我绕,你堵我怼。来看看怎么怼。

先总结一下目前的状况 由于伪全局未过滤,导致$_FILES变量覆盖 又由于使用了copy函数来进行文件移动 不会检查文件是否合法 可能导致任意文件读取和getshell的问题 问题是 不管走哪个流程 都会unlink原文件 读取关键配置会导致关键配置文件被删 站点直接瘫痪

一个一个来解决

关于文件读取 有没有办法 能让copy(src,dst)成功 而unlink(src)失败呢

答案是有的 就是神奇的php://filter 这里限于篇幅 不再细说这个schema 百度一下有几位前辈早已写过有关的文章

利用php://filter/resource=路径/文件名 就可以达到我们想要的效果 copy成功 unlink失败,虽然copy成功之后 第二个getimagesize检查后面的unlink没办法bypass,不过已经生成了,那我读不读就由不得你了。时间竞争大家应该不陌生, 我赶在你生成和删除中间的一瞬间读到不就行了,时间竞争的关键一点就是,目标要明确,如果我不知道你文件名 胡乱去猜的话 这个时间间隔肯定是不够的,但是文件名我们是已知的( [固定]+uid+文件后缀),所以多试几次 肯定能成功。

好了 现在任意文件读取这个漏洞已经拿下了,那贪心一点 能getshell么?

答案当然也是能。

来看看保存的文件名格式。

 //这里调用getimagesize函数来检查文件内容
                $imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
                $filetype = $imgtype[$type]; //限定了文件后缀来自$imgtype数组
                $tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //临时保存文件名 [固定]+uid+文件后缀[/size][/size][/color][/size][/font][/size][/font][/color]

为了getshell 这里需要让getimagesize()失败 这样$type就不会被初始化

$filetype=$imgtype[$type];

文件后缀就变成null了 接下来如果能控制$uid=xx.php的话 就能getshell了 $uid来自

$this->init_input($_GET['agent']);
$uid = $this->input['uid'];  //uid来自input数组 input数组来自init_input()函数

看看init_input函数

public function init_input($getagent = '') {  
                $input = $_GET['input'];
                if($input) {
                        $input = encryption($input,'DECODE',WEBKEY); 
                        parse_str($input,$this->input); 
                        $agent = $getagent ? $getagent : $this->input['agent'];
                        if(($getagent && $getagent != $this->input['agent']) || (!$getagent && md5($_SERVER['HTTP_USER_AGENT']) != $agent)) {
                                exit('Access denied for agent changed');
                        } elseif($this->time - $this->input['time'] > 3600) {
                                exit('Authorization has expired');
                        }
                }
                if(empty($this->input)) {
                        exit('Invalid input');
                }
        }

$_GET['input'];中解密并用parse_str赋值给了$this->input数组 这里用的加解密函数encryption()其实就是dz的 authcode 函数,还是比较安全的。密钥WEBKEY来自inc/config/version.config.php 如果我们能知道密钥WEBKEY 就能伪造uid=.php的input值来getshell

怎么得到这个WEBKEY值呢,别忘了上面的任意文件读取哦~

四、利用

毕竟还是个0day 危害也比较大 这里就不公开具体的getshell代码了 主要是分享一个从拿到cms开始发现安全隐患 到如何利用安全隐患 再遇到困难 解决困难最终成功利用的过程。

总结

写文章比看代码累多了。。。 代码审计 靠的其实就是对编程语言的理解。怎么去快速发现问题,怎么去绕坑,都需要不断的积累。 最后祝大家0day多多

原文地址:http://bbs.ichunqiu.com/thread-13703-1-1.html?from=seebug