前言
ubuntu19
mysql + php5.6
乐尚商城系统v1.5
写在前面
复现这个漏洞之前,我觉得比较重要的是了解MVC的设计模式,因为这个cms基于brophp开发的,是典型的使用mvc设计模式的框架。
MVC设计模式介绍
参考文章:MVC架构模式详细说明
在未有面对对象设计模式之前,很多开发人员都是把代码糅杂在一块儿,使得后期维护,和二次开发十分困难,于是有了MVC(Models-Views-Controller)
MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
模型(Model):负责存储系统的中心数据。
视图(View):将信息显示给用户(可以定义多个视图)。
控制器(Controller):处理用户输入的信息。负责从视图读取数据,控制用户输入,并向模型发送数据,是应用程序中处理用户交互的部分。负责管理与用户交互交互控制。
漏洞分析
MVC介绍
我们可以看到,在admin目录中有如下目录,然后我们可以想到,这个cms的设计模式就是MVC
分析一下,先打开任意一个文件试一下,定位admin/controls/acate.class.php::add()
这是一个控制器,用于处理用户输入的信息,从views获取数据,控制用户输入,并向models发送数据,具体分析一下add()函数1
2
3
4
5
6
7
8
9
10function add(){
$this->validate();
$acate=D("acate");
$result=$acate->add();
if(false !== $result){
$this->success("填加成功!", 1, "acate/index");
} else {
$this->error("填加失败!", 1);
}
}
跟进validate()函数1
2
3
4
5
6
7
8
9
10private function validate(){
validate::notnull($_POST['name'],"名称不能为空");
validate::notnull($_POST['sort'],"排序不能为空");
validate::number($_POST['sort'],"排序必须为数字");
if(!validate::$flag){
$msg=implode("<br>",validate::getMsg());
$this->error($msg, 3);
}
}
从这儿获取信息,通过add()函数,调用D(),跟进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/**
* 创建Models中的数据库操作对象
* @param string $className 类名或表名
* @param string $app 应用名,访问其他应用的Model
* @return object 数据库连接对象
*/
function D($className=null,$app=""){
$db=null;
//如果没有传表名或类名,则直接创建DB对象,但不能对表进行操作
if(is_null($className)){
$class="D".DRIVER;
$db=new $class;
}else{
$className=strtolower($className);
$model=Structure::model($className, $app);
$model=new $model();
//如果表结构不存在,则获取表结构
$model->setTable($className);
$db=$model;
}
if($app=="")
$db->path=APP_PATH;
else
$db->path=PROJECT_PATH.strtolower($app).'/';
return $db;
}
那么可以看到,我们创建的models中的acate类,并调用其add()函数
定位./admin/models/acate.class.php::add()1
2
3function add(){
return $this->insert();
}
通过该方法,对数据库进行处理,这样子也同样地验证了models的作用,负责存储系统的中心数据。
路由分析
定位./admin.php1
2
3
4
5
6
7
8
9
10
11
12
/**
* 单一入口文件
*/
include("./temp.inc.php");
define("TPLSTYLE", "new"); //默认模板存放的目录
define("BROPHP", "./brophp"); //框架源文件的位置
define("APP", "./admin"); //设置当前应用的目录
define("PAGENUM",$temp['admin_page_num']); //默认每页显示记录数
require(BROPHP.'/brophp.php'); //加载框架的入口文件
跟进brophp.php1
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
/*********************************************************************************
* brophp2.0 框架入口文件,所有脚本都是从这个文件开始执行,主要是一些全局设置。 *
* *******************************************************************************
*/
... ....
这里主要是加载一下配置文件,省略掉了
... ....
//控制器类所在的路径
$srccontrolerfile=APP_PATH."controls/".strtolower($_GET["m"]).".class.php";
//从admin.php处获取我们需要调用的模块
Debug::addmsg("当前访问的控制器类在项目应用目录下的: <b>$srccontrolerfile</b> 文件!");
//控制器类的创建
if(file_exists($srccontrolerfile)){
Structure::commoncontroler(APP_PATH."controls/",$controlerpath);
Structure::controler($srccontrolerfile, $controlerpath, $_GET["m"]);
$className=ucfirst($_GET["m"])."Action";
$controler=new $className();
$controler->run();
}else{
Debug::addmsg("<font color='red'>对不起!你访问的模块不存在,应该在".APP_PATH."controls目录下创建文件名为".strtolower($_GET["m"]).".class.php的文件,声明一个类名为".ucfirst($_GET["m"])."的类!</font>");
}
跟进run()函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 该方法用来运行框架中的操制器,在brophp.php入口文件中调用
*/
function run(){
if($this->left_delimiter!="<{")
parent::__construct();
//如果有子类Common,调用这个类的init()方法 做权限控制
if(method_exists($this, "init")){
call_user_func(array($this, "init"));
}
//根据动作去找对应的方法
$method=$_GET["a"];
if(method_exists($this, $method)){
call_user_func(array($this, $method));
}else{
Debug::addmsg("<font color='red'>没有{$_GET["a"]}这个操作!</font>");
}
}
那么也就是说我们想要调用某控制器的某动作,url如下
漏洞分析
定位:./admin/controls/backup.class.php::restore()1
2
3
4
5
6
7
8
9
10function restore(){
$bu=D("Backup");
$file=trim($_GET['file']);
$d=$bu->restore($file);
if($d['res']){
$this->success("还原成功!", 2, "backup/index");
} else {
$this->error("还原失败!".$d['info'], 2);
}
}
发现这里的file是用户可控的。
根据以上的分析的分析,我们这次定位到./admin/models/backup.class.php::restore()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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84public function restore($file){
$filename=PROJECT_PATH."backup/".$file;
//获取文件路径
$handle=@fopen($filename,"rb");
//打开文件
$head=@fread($handle,70);
//读取文件的前70字节长度的内容
fclose($handle);
//关闭文件
$arr=$this->getSqlInfo($head);
//调用getSqlInfo()
--------------------------------------------------------------------
--------------------------------------------------------------------
private function getSqlInfo($head){
$file_info = array('cms_ver'=>'', 'mysql_ver'=> '', 'add_time'=>'');
$head=str_replace("--","",$head);
$arr = explode("\n", $head);
foreach($arr as $var){
$temp = explode(":", $var);
switch($temp[0]){
case 'Version':
$file_info['cms_ver']=trim($temp[1]);
break;
case 'Mysql Ver':
$file_info['mysql_ver']=trim($temp[1]);
break;
case 'Create time':
$file_info['add_time']=trim($temp[1]);
break;
}
}
return $file_info;
}
//获取版本信息,mysql版本信息,创建时间
-------------------------------------
-------------------------------------
$status="";
if(strpos($arr['cms_ver'],"update")){
$status="update";
} elseif($arr['cms_ver']!=VERSION){
$data['info']="版本不统一,不能还原!";
$data['res']=false;
return $data;
}
//获取版本信息,这里我们就可以直接--Version:1.5.0,或者--Version:update,就可以绕过这个版本验证
$sql=$this->removeComments(file_get_contents($filename));
//调用removeComments()函数
----------------------------------------------------------
----------------------------------------------------------
private function removeComments($sql){
/* 删除SQL行注释,行注释不匹配换行符 */
$sql = preg_replace('/^\s*(?:--|#).*/m', '', $sql);
/* 删除SQL块注释,匹配换行符,且为非贪婪匹配 */
//$sql = preg_replace('/^\s*\/\*(?:.|\n)*\*\//m', '', $sql);
$sql = preg_replace('/^\s*\/\*.*?\*\//ms', '', $sql);
return $sql;
}
//这里主要过滤掉一些符号,关键词并没有被过滤掉
----------------------------------------------------------
----------------------------------------------------------
$sql = trim($sql);
$sql = str_replace("\r", '', $sql);
$segmentSql = explode(";\n", $sql);
foreach($segmentSql as $var){
if($var!=''){
if($status=="update"){
$var = str_replace('update_',TABPREFIX, $var);
}
$result=$this->query(trim($var));
}
if(!$result){
$data['info']=$var;
$data['res']=false;
return $data;
}
}
这里并没有严格的过滤,file可以使用目录穿越,我们想个法子写入一个数据库文件,然后通过目录穿越漏洞,然后代码读取其中的内容,并代入数据库进行处理。
POC
写一个文件,并改成jpg
然后在用户修改信息处上传
然后查看源码,查看文件位置
上传成功
查看数据库验证一下,成功