Skip to content

三个白帽代码审计之条条大路通罗马1

Posted in 每刻,知识分享

代码审计好难做,这几天做这些做的异常压抑,好在最后在v牛帮助下总算解决了,最终知道了自己不是常规思路。。。。mdzz,问了超威蓝猫,orz!!!

index.php

define ('PATH_WEB', dirname(__FILE__).'/');
include_once 'init.php';

if($_M['form']['class']){
    include PATH_WEB . $_M['form']['class'].'.php';
}
if($_M['form']['formname'] || $_FILES['file']['name']){
        $upfile = new upfile();
        $upfile->set('savepath', '');
        $upfile->set('is_rename', $_M['form']['is_rename']);
        $back = $upfile->upload($_M['form']['formname']);
    }

    $gb_array = [
        "name" => htmlspecialchars(filter($_M['form']['name'])),
        "message" => htmlspecialchars(filter($_M['form']['message'])),
        "filename" => $back,
    ];
    $content = jsonencode($gb_array);
    $sql = "insert into guestbook(`content`) values('".$content."');";
    $result = mysql_query($sql);
    if($result)
    {
        echo "<script>alert('thx for your feedback~')</script>";
    }

可以控制的部分,name,message,file,这里还把变量带入了函数,把相关的几个函数提取出来看一下有什么可以钻空子的地方。 显而易见,这里有任意php文件读取,因为参数经过过滤,所以截断是失效的,意味着这里只能读取php文件。

include.php

function daddslashes($string, $force = 0) {
    !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
    if(!MAGIC_QUOTES_GPC || $force) {
        if(is_array($string)) {
            foreach($string as $key => $val) {
                $string[$key] = daddslashes($val, $force);
            }
        } else {
            $string = trim(addslashes($string));
        }
    }
    return $string;
}
/*
获取GET,POST,COOKIE,存放在$_M['form'],系统表单提交变量数组
*/
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}

有戏!这里说明$_M[‘form’]所有变量均可控,为之后的操作带来了很大的便利,我们可以借此控制不属于用户域内的变量,然而这里输入都经过了daddslashes的过滤,这里的过滤比较严密,去除了空格,并且对输入做了转义,需要考虑php多字节输入带来的安全问题或者通过二次覆盖来绕过,这里暂时没什么头绪

<?php
//define ('PATH_WEB', dirname(__FILE__));

class upfile {
    public    $savepath;
    public    $is_rename;

    public function __construct() {
        global $_M;
        $this->set_upfile();
    }

    public function set($name, $value) {
        if ($value === NULL) {
            return false;
        }
        switch ($name) {
            case 'savepath':
                $this->savepath = PATH_WEB.'upload/'.$value;
                break;
            case 'is_rename':
                $this->is_rename = $value;
                break;
        }   
    }

    public function set_upfile() {
        global $_M;
        $this->set('savepath', '');
        $this->set('is_rename', 1);
    }

    public function upload($form = '') {
        global $_M;
        if (is_array($form)) {
            $filear = $form;
        }else{
            $filear = $_FILES[$form];
        }
        if(!$filear){
            foreach($_FILES as $key => $val){
                $filear = $_FILES[$key];
                break;
            }
        }
        //是否能正常上传
        if(!is_array($filear))$filear['error'] = 4;
        var_dump($filear);
        //文件后缀是否为合法后缀
        $this->getext($filear["name"]); //获取允许的后缀
        if (strtolower($this->ext)=='php'||strtolower($this->ext)=='php3'||strtolower($this->ext)=='php4'||strtolower($this->ext)=='php5') {
            return false;
        }

        //文件名重命名
        $this->set_savename($filear["name"], $this->is_rename);

        //复制文件
        $upfileok=0;
        $file_tmp=$filear["tmp_name"];
        $file_name=$this->savepath.$this->savename;

        if (function_exists("move_uploaded_file")) {
            if (move_uploaded_file($file_tmp, $file_name)) {
                $upfileok=1;
            } else if (copy($file_tmp, $file_name)) {
                $upfileok=1;
            }
        } elseif (copy($file_tmp, $file_name)) {
            $upfileok=1;
        }
        if ($upfileok) {
            @unlink($filear['tmp_name']); //任意文件删除,指哪儿打哪儿
        } 

        $back = str_replace(PATH_WEB, '', $this->savepath).$this->savename;
        return $back;
    }

    protected function set_savename($filename, $is_rename) {
        if ($is_rename) {
            srand((double)microtime() * 1000000);
            $rnd = rand(100, 999);
            $filename = date('U') + $rnd;
            $filename = $filename.".".$this->ext;   
        } else {
            $name_verification = explode('.',$filename);
            $verification_mun = count($name_verification);
            if($verification_mun>2){
                $verification_mun1 = $verification_mun-1;
                $name_verification1 = $name_verification[0];
                for($i=0;$i<$verification_mun1;$i++){
                    $name_verification1 .= '_'.$name_verification[$i];
                }
                $name_verification1 .= '.'.$name_verification[$verification_mun1];
                $filename = $name_verification1;
            }
            $filename = str_replace(array(":", "*", "?", "|", "/" , "\\" , "\"" , "<" , ">" , "——" , " " ),'_',$filename);
            $filename_temp = $filename;
            $i=0;
            $savename_temp=str_replace('.'.$this->ext,'',$filename_temp);
            while (file_exists($this->savepath.$filename_temp)) {
                $i++;
                $filename_temp = $savename_temp.'('.$i.')'.'.'.$this->ext;  
            }
            if ($i != 0) {
                $filename = str_replace('.'.$this->ext,'',$filename).'('.$i.')'.'.'.$this->ext; 
            }
        }
        return $this->savename = $filename;
    }

    protected function getext($filename) {
        if ($filename == "") {//弱类型判断 可以用 数组 直接 返回null
            return ;
        }
        $ext = explode(".", $filename);
        return $this->ext = $ext[count($ext)-1];
    }

}
?>

太长了,说实话,别人的代码超过了一页根本不想看啊。。。然而还是需要慢慢审,逻辑基本上是这样的,首先在index.php调用这个类时,就会初始化savepath为’/’和is_rename为1 然后把$form传入upload(),根据代码逻辑,$form如果不是数组是没办法成功上传的,所以$form必须是数组,而且是符合格式的数组。这里已经暴露了一个漏洞,不过这个漏洞没啥用…就在unlink这里,通过控制formname可以修改缓存文件路径,如果权限没有设置的话就能做到指哪儿打哪儿。

文件重命名,判断if($is_rename),初始化时被设置为1,如何绕过呢?经过几次本地测试,发现当$is_rename=”;时,可以绕过这里的判断,再结合之前的变量覆盖,拍脑袋想这里覆盖后可直接绕过文件重命名。 结果绕过后还仅仅是开始,之后对文件名是否合法有其他判断,以’.’为单位分割文件名为一个数组,取数组最后一个元素为扩展名,扩展名做了白名单判断,fuzz后倍感无力,其他格式都执行不了,愣是让我测了一夜。。 如果出现了两个以上的小数点,会对文件名做特殊处理,这样.php.jpg这种形式也无法绕过了。理论上已经堵死了php的上传,但是有个黑科技没有堵,就是包含php脚本的html文档,扩展名为pht,这样经过一次变量覆盖之后绕过重命名,就能访问到我们的shell了,菜刀连上得flag。

然而这道题如果堵死了pht的上传……

继续看这段的逻辑。他会把文件从php临时文件的文件夹中复制过来,并且重命名,发现临时文件的文件夹可以通过get参数控制,又是一次变量覆盖,这样就能造成任意文件读取,指哪儿打哪儿。

总结一下以上发现的洞:

1.php文件包含
2.任意文件删除(权限之内)
3.任意文件读取(通过复制的方式)
4.phtml格式未过滤

题目描述是需要找到后台,所以第四个做法是意外之喜….找到了洞然而并不会用!!!!应不是出题者本义,我们需要获取后台地址,也就是目录信息,询问了Cr的做法,下面就是几个常见的敏感目录泄漏的重灾区

/etc/init.d/nginx

/etc/apache2/apache

/usr/local/nginx/conf/nginx.conf

/homw/wwwlogs/access.log

需要的时候多google一下吧。这里读取到了/etc/apache2/apache

截取关键信息

<VirtualHost *:80>

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmwww

</VirtualHost>
<VirtualHost *:8080>

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmadmin

</VirtualHost>

我们现在在的目录是sgbmwww,我们下一步应当是sgbadmin,8080端口不允许外部访问。新一轮代码审计~,把后台的index.php读回来

<?php

include_once 'init.php';

$sql = "select id,content from guestbook where content like '%".$_M['form']['search']."%' order by id desc limit 0,100;";
$result = mysql_query($sql);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <title>Animated Form Switching with jQuery</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <meta name="description" content="Expand, contract, animate forms with jQuery wihtout leaving the page" />
        <meta name="keywords" content="expand, form, css3, jquery, animate, width, height, adapt, unobtrusive javascript"/>
        <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"/>
        <link rel="stylesheet" type="text/css" href="css/style.css" />
        <script src="js/cufon-yui.js" type="text/javascript"></script>
        <script src="js/ChunkFive_400.font.js" type="text/javascript"></script>
        <script type="text/javascript">
            Cufon.replace('h1',{ textShadow: '1px 1px #fff'});
            Cufon.replace('h2',{ textShadow: '1px 1px #fff'});
            Cufon.replace('h3',{ textShadow: '1px 1px #000'});
            Cufon.replace('.back');
        </script>
    </head>
    <body>
        <div class="wrapper">
            <h1>SanGeBaiMao HouTai</h1><h2 style="text-align:right;"></h2>
            <div class="content" style="text-align:center;padding-left:200px;width:500px">
            <div id="form_wrapper" class="form_wrapper"></div>
            <br/>
            <form class="MessageBox" action="index.php" method="post">
                    <div>
                            <h3>Guestbook Search<h3>
                            <input id="fd-name" class="text" type="text" name="search"/>
                            <input id="fd-button" class="input-submit" type="submit" name="submit" value="Search" /></p> 
                    </div>  
            </form>
            <div style="text-align:center;width" >
<?php
while($row = @mysql_fetch_array($result))
{   
    $content = jsondecode($row['content']);
    echo "<div style=\"text-align:left\"><h4>"."第".$row['id']."条留言:</h4></div><br/>";
    echo "<div>".$content['name']."|".$content['message']."|".$content['filename']."</div><br/>";
    echo "<div><hr></div><br/>";

}
?>

                </div>
            </div>
        </div>
    </body>
</html>

这简陋的后台就是管理员查询留言的地方,从这里似乎看不出什么,经过处理的参数注入的可能性也比较小,这里的sql查询用的是like,匹配查询,可能是没有直接的注入漏洞,数据库中的数据取出来之后才造成了危害。 继续审,init.php取回来

<?php

include 'config.php';
include 'include.php';

?>

继续读取include.php

<?php
function filter($input)
{
    $input = str_replace('<','',$input);
    $input = str_replace('>','',$input);
    $input = str_replace('0x','',$input);
    return $input;
}
function errorBox($message)
{
    echo $message;
}
function jsonencode($arr){
    $parts = array();
    $is_type = false;         //false 鍏宠仈鏁扮粍         true 绱㈠紩鏁扮粍
    $keys = array_keys($arr);
    $length = count($arr)-1;
    if($keys[0] === 0 && $keys[$length] == $length){//鍒ゆ柇鏄储寮曟暟缁勮繕鏄叧鑱旀暟缁�
        $is_type = true;
        for($i=0; $i<count($keys); $i++){
            if($i != $keys[$i]){
                $is_type = false;
                break;
            }
        }
    }
    foreach($arr as $key=>$val){
        if(is_array($val)){
            if($is_type){
                $parts[] = jsonencode($val);
            }else{
                $parts[] = '"' . $key . '":' . jsonencode($val);
            }
        }else{
            $str = '';
            if(!$is_type){
                $str = '"' . $key . '":';
            }
            if($val === false){
                $str .= 'false';
            }else if($val === true){
                $str .= 'true';
            }else{
                $str .= '"' . str_replace(array('\\' ,'/', '"') , array('\\\\' ,'\\/', '\"'),$val) . '"';
            }
            $parts[] = $str;
        }
    }
    $json = implode(',', $parts);
    $json = str_replace(array("\r", "\n", "\t"), '', $json);
    if($is_type)return '[' . $json . ']';
    return '{' . $json . '}';
}
function jsondecode($json){
    if($json){
        $convert = false;
        $str = '$arr=';
        for ($i=0; $i<strlen($json); $i++){
            if (!$convert){
                if (($json[$i] == '{') || ($json[$i] == '[')){
                    $str .= ' array(';
                }else if (($json[$i] == '}') || ($json[$i] == ']')){
                    $str .= ')';
                }else if ($json[$i] == ':'){
                    $str .= '=>';
                }else{
                    $str .= $json[$i];
                }                                    
            }else{
                $str .= $json[$i];
            }         
            if ($json[$i] == '"' && $json[($i-1)]!="\\"){
                $convert = !$convert;
            }
        }
        $str = str_replace(array('\\\\' ,'\\/'), array('\\' ,'/'), $str);
        @eval($str . ';');
    }else{
        $arr = array();
    }
    return $arr;
}

function daddslashes($string, $force = 0) {
    !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
    if(!MAGIC_QUOTES_GPC || $force) {
        if(is_array($string)) {
            foreach($string as $key => $val) {
                $string[$key] = daddslashes($val, $force);
            }
        } else {
            $string = trim(addslashes($string));
        }
    }
    return $string;
}
/*** 
鑾峰彇GET,POST,COOKIE锛屽瓨鏀惧湪$_M['form']锛岀郴缁熻〃鍗曟彁浜ゅ彉閲忔暟缁�
*/
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
    $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}

其他基本和前台处理函数一致,不过这里jsondecode函数中return之前留了个莫名其妙的执行函数@eval($str . ‘;’); 的确和之前的猜想一样,我们要构造一个畸形的留言,在被jsdecode之后能够完整的代入eval中,从而造成了任意命令执行。

再联想到index.php的任意文件包含,很清楚了,通过index.php的任意php文件包含向后台的index.php通过get方式取出数据库中的数据,jsondecode后可以造成任意命令执行

3 Comments

  1. 根本看不懂QAQ M菊苣好厉害

    2016年5月17日
    |Reply
  2. jaye
    jaye

    楼主,你的那个变量覆盖漏洞哪里找到的?

    2016年10月20日
    |Reply
    • Melody
      Melody

      时间也比较长了,应该是include.php里面的那个吧

      2016年10月20日
      |Reply

Leave a Reply

Your email address will not be published. Required fields are marked *