0x00 结构浏览
按照代码审计的惯例,拿到这款cms之后首先浏览了一下目录结构,在基本了解之后,首先进入/index.php,这里包含了两个文件:/admini/config/qd-config.php和/loader/load.php,简单看了一下代码,没什么用,程序入口文件没有太大价值。转到后台/admini目录下,发现还有个/admin/index.php,打开阅读一下源码,没什么用,但是明确了这是一款比较硬核的cms,注释极少,程序可读性不是很高。/admin目录下还有一个/admin/controllers,从命名上就能看出这是一个比较重要的业务逻辑包,打开之后里面还有一个/admin/controllers/index.php,里面只有两个函数,还是没什么用。在这个目录下,还有一个文件夹为system,打开之后居然还有一个/admin/controllers/system/index.php....里面就俩函数,也没有有价值的信息。
0x01 任意文件下载
虽然这一大堆index都没有用到,但是也帮助我们了解了这款cms的结构,所以我们直接从后台业务逻辑核心部分,也就是/admini/controllers/system入手,首先读一下bakup.php的源码,这个文件主要作用是管理数据库,里面有一个download函数疑似存在任意文件下载:
1 function download() 2 { 3 global $request; 4 if(!empty($request['filename'])) 5 { 6 file_down(ABSPATH.'/temp/data/'.$request['filename']); 7 } 8 else 9 { 10 echo '<script>alert("文件名不能为空!");window.history.go(-1);</script>'; 11 } 12 }
为了进一步确认可以下载的范围,跟读file_down函数:
1 function file_down($file,$filename='') 2 { 3 if(is_file($file)) 4 { 5 $filename = $filename ? $filename : basename($file); 6 $filetype = fileext($filename); 7 $filesize = filesize($file); 8 header('Cache-control: max-age=31536000'); 9 header('Expires: '.gmdate('D, d M Y H:i:s', time() + 31536000).' GMT'); 10 header('Content-Encoding: none'); 11 //header('Content-Length: '.$filesize); 12 header('Content-Disposition: attachment; filename='.$filename); 13 header('Content-Type: '.$filetype); 14 readfile($file); 15 } 16 else 17 { 18 echo '<script>alert("文件不存在!");window.history.go(-1);</script>'; 19 } 20 exit; 21 }
可以看出这里没有做任何的过滤,构造payload:
http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=download&filename=index.php
0x02 CSRF
在这个文件里还有一个export函数:
1 function export() 2 { 3 global $db,$request,$sizelimit,$startrow; 4 $tables=$request['tables']; 5 $sizelimit=$request['sizelimit']; 6 if($request['dosubmit']) 7 { 8 $fileid = isset($request['fileid']) ? $request['fileid'] : 1; 9 if($fileid==1 && $tables) 10 { 11 if(!isset($tables) || !is_array($tables)) 12 echo "<script>alert('请选择要备份的数据表!');window.history.go(0);</script>"; 13 $random = mt_rand(100000, 999999); 14 cache_write('bakup_tables.php', $tables); 15 } 16 else 17 { 18 if(!$tables = cache_read('bakup_tables.php')) 19 echo "<script>alert('请选择要备份的数据表!');window.history.go(-1);</script>"; 20 } 21 $sqldump = ''; 22 $tableid = isset($request['tableid']) ? $request['tableid'] - 1 : 0; 23 $startfrom = isset($request['startfrom']) ? intval($request['startfrom']) : 0; 24 $tablenumber = count($tables); 25 for($i = $tableid; $i < $tablenumber && strlen($sqldump) < $sizelimit * 1024; $i++) 26 { 27 $sqldump .= sql_dumptable($tables[$i], $startfrom, strlen($sqldump)); 28 $startfrom = 0; 29 } 30 31 if(trim($sqldump)) 32 { 33 $sqldump = "#Realure.cn Created # -------------------------------------------------------- ".$sqldump; 34 $tableid = $i; 35 $random = isset($request['random']) ? $request['random'] : $random; 36 $filename = DB_DBNAME.'_'.date('Ymd').'_'.$random.'_'.$fileid.'.sql'; 37 $fileid++; 38 39 $bakfile = '../temp/data/'.$filename; 40 if(!is_writable('../temp/data/')) 41 message('数据无法备份到服务器!请检查 ./data 目录是否可写。', $forward); 42 file_put_contents($bakfile, $sqldump); 43 //echo 'wer'; 44 //exit; 45 echo '<script>alert("备份文件'.$filename.'写入成功!");window.location.href="?m=system&s=bakup&a=export&sizelimit='.$sizelimit.'&tableid='.$tableid.'&fileid='.$fileid.'&startfrom='.$startrow.'&random='.$random.'&dosubmit=1";</script>'; 46 } 47 else 48 { 49 cache_delete('bakup_tables.php'); 50 echo '<script>alert("数据库备份完毕!");window.location.href="?m=system&s=bakup&a=export";</script>'; 51 //message('数据库备份完毕!'); 52 } 53 exit; 54 } 55 }
不用看代码,光看输出就能看出来这是备份数据库的函数,但是代码审计总还是要看代码的,这个函数接收了用户提交的两个参数:tables、sizelimit,依据这两个参数导出数据库备份文件,没有验证referer和token,因此会存在CSRF漏洞,诱导管理员备份数据,结合之前的任意文件下载漏洞就可以轻松拿到数据库信息。payload:
http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=export&tables[]=doc_user&sizelimit=2048&dosubmit=开始备份数据
0x03 可执行sql文件的上传
这个文件的问题很多,几乎每个函数都可以利用,后面还有一个上传sql文件的函数:
1 function uploadsql() 2 { 3 global $request; 4 $uploadfile=basename($_FILES['uploadfile']['name']); 5 if($_FILES['userfile']['size']>$request['max_file_size']) 6 echo '<script>alert("您上传的文件超出了2M的限制!");window.history.go(-1);</script>'; 7 if(fileext($uploadfile)!='sql') 8 echo '<script>alert("只允许上传sql格式文件!");window.history.go(-1);</script>'; 9 $savepath = ABSPATH.'/temp/data/'.$uploadfile; 10 if(move_uploaded_file($_FILES['uploadfile']['tmp_name'], $savepath)) 11 { 12 echo '<script>alert("数据库SQL脚本文件上传成功!");window.history.go(-1);</script>'; 13 } 14 else 15 { 16 echo '<script>alert("数据库SQL脚本文件上传失败!");window.history.go(-1);</script>'; 17 } 18 }
这里有一个代码逻辑漏洞,在代码的第7行和fileext()函数中希望验证后缀名,只有.sql文件才能上传,但是后面并没有exit,而是弹窗提示只允许上传sql格式文件,程序继续运行,这样我们就可以上传任意文件了,新建一个phpinfo,在数据恢复页面上传。
除此之外,这里还有第二种利用方式,有一个数据库导入函数import:
1 function import() 2 { 3 global $db,$request; 4 $pre=$request['pre']; 5 if($request['dosubmit']) 6 { 7 if($request['filename'] && fileext($request['filename'])=='sql') 8 { 9 $filepath = ABSPATH.'/temp/data/'.$filename; 10 if(!is_file($filepath)) 11 echo '<script>alert("文件不存在!");window.history.go(-1);</script>'; 12 $sql = file_get_contents($filepath); 13 sql_execute($sql); 14 echo '<script>alert("'.$filename.'中的数据已经成功导入到数据库!");window.history.go(-1);</script>'; 15 } 16 else 17 { 18 $fileid = isset($request['fileid']) ? $request['fileid'] : 1; 19 $filename = $request['pre'].$fileid.'.sql'; 20 $filepath = ABSPATH.'/temp/data/'.$filename; 21 if(is_file($filepath)) 22 { 23 $sql = file_get_contents($filepath);//将整个文件读入一个字符串 24 sql_execute($sql); 25 $fileid++; 26 echo '<script>alert("数据文件'.$filename.'导入成功!");window.location.href="?m=system&s=bakup&a=import&pre='.$pre.'&fileid='.$fileid.'&dosubmit=1";</script>'; 27 28 } 29 else 30 { 31 echo '<script>alert("数据库恢复成功!");window.location.href="?m=system&s=bakup&a=import";</script>'; 32 } 33 } 34 } 35 }
代码的18-28行读取.sql文件并且执行,如果我们自己上传的sql脚本带有一句话木马,在权限足够的情况下可以直接拿shell。
0x04 同目录任意文件删除
这个文件搞完了,下一个文件是changeskin.php,里面有一个deleteFile函数:
1 function deleteFile(){ 2 global $request; 3 $dirPath = get_abs_skin_root().filter_submitpath( $request['dirPath'] ); 4 if(is_file($dirPath)){ 5 @unlink($dirPath); 6 exit('1::delete ok'); 7 }else{ 8 exit('0::Forbidden'); 9 } 10 }
里面调用了两个函数,继续跟读:
1 function filter_submitpath($path) 2 { 3 $path= preg_replace('/[.]{2,}/', '', $path);//去除 .. 禁止提交访问上级目录的路径 4 return preg_replace('/[/]{2,}/', '/', $path);//校正路径 5 } 6 7 8 function get_abs_skin_root() 9 { 10 return ABSPATH.'/'.SKINROOT.'/'.STYLENAME.'/'; 11 }
可以看出这里没有进行任何的过滤,可以删除同目录下的所有文件。