骑士 cms 通读审计案例
我们已经介绍了代码审计中通读全文代码审计方式的思路,下面我们用案例来说明
这种通读方式。为了方便大家理解,笔者找了一款相对简单容易看懂的应用骑士cms来介绍,版本是3.5.1,具体的审计思路我们在上文中已经有过介绍。
1. 查看应用文件结构
先看一下骑士 cms 的大致文件目录结构,如下图:
首先需要看看有哪些文件和文件夹,寻找名称里有没有带有api、admin、manage、include 一类关键字的文件和文件夹,通常这些文件比较重要,在这个程序里,可以看到并没有什么PHP文件,就一个index.php,看到有一个名为include的文件夹,一般比较核心的文件都会放在这个文件夹中,我们先来看看大概有哪些文件,如下图所示。
2. 查看关键文件代码
在这个文件夹里面我们看到了多个数十K的PHP文件,比如common.fun.php就是本程序的核心文件(函数集文件),基础函数基本在这个文件中实现,我们来看看这个文件里有哪些关键函数,一打开这个文件,立马就看到一大堆过滤函数,这是我们最应该关心的地方,首先是一个SQL注入过滤函数:
function addslashes_deep($value)
{
if (empty($value))
{
return $value;
}
else
{
if (!get_magic_quotes_gpc()) // get_magic_quotes_gpc获取当前magic_quotes_gpc的配置选项设置
{
$value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags(addslashes($value)); // strip_tags() 函数剥去字符串中的 HTML、XML 以及 PHP 的标签。
// array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。
}
else
{
$value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags($value);
}
return $value;
}
}
该函数将传入的变量使用addslashes()函数进行过滤,也就过滤掉了单引号、双引号、NULL字符以及斜杠,现在我们要记住,在挖掘SQL注入等漏洞时,只要参数在拼接到SQL语句之前,使用了这个函数就不能注入了(除非有宽字节注人或者其他特殊情况)。
再往下走是一个过滤XSS的函数mystrip_tags(),代码如下:
function mystrip_tags($string)
{
$string = new_html_special_chars($string);
$string = remove_xss($string);
return $string;
}
这个函数调用了new_html_special_chars() 和remove_ xss() 函数来过滤XSS,就在该函数下方,代码如下:
function new_html_special_chars($string) {
$string = str_replace(array('&', '"', '<', '>'), array('&', '"', '<', '>'), $string);
$string = strip_tags($string);
return $string;
}
function remove_xss($string) {
$string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $string);
$parm1 = Array('javascript', 'union','vbscript', 'expression', 'applet', 'xml', 'blink', 'link', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');
$parm2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload','style','href','action','location','background','src','poster');
$parm3 = Array('alert','sleep','load_file','confirm','prompt','benchmark','select','update','insert','delete','alter','drop','truncate','script','eval');
$parm = array_merge($parm1, $parm2, $parm3); //array_merge函数把一个或多个数组合并为一个数组
for ($i = 0; $i < sizeof($parm); $i++) {
$pattern = '/';
for ($j = 0; $j < strlen($parm[$i]); $j++) {
if ($j > 0) {
$pattern .= '(';
$pattern .= '(&#[x|X]0([9][a][b]);?)?';
$pattern .= '|(�([9][10][13]);?)?';
$pattern .= ')?';
}
$pattern .= $parm[$i][$j];
}
$pattern .= '/i';
$string = preg_replace($pattern, '****', $string);
}
return $string;
}
在new_html_special_chars() 函数中可以看到,这个函数对&符号、双引号以及尖括号进行了html实体编码,并且使用strip_tags() 函数进行了二次过滤。而remove_xss()函数则是对一些标签关键字、事件关键字以及敏感函数关键字进行了替换。再往下走有一个获取IP地址的函数getip(),是可以伪造IP地址的:
getenv(参数)函数是一个用于获取环境变量的函数,根据提供不同的参数可以获取不同的环境变量。strcasecmp() 函数比较两个字符串,相等返回0,大于返回正数,小于返回负数。
很多应用都会由于在获取IP时没有验证IP格式,而存在注入漏洞,不过这里还只是可以伪造IP(用抓包)。
再往下看可以看到一个值得关注的地方,SQL查询统一操作函数inserttable()以及updatetable()函数,大多数SQL语句执行都会经过这里,所以我们要关注这个地方是否还有过滤等问题。
function inserttable($tablename, $insertsqlarr, $returnid=0, $replace = false, $silent=0) {
global $db; // 声明 $db 的作用范围为全局,度就可以在函数里面使用了
$insertkeysql = $insertvaluesql = $comma = '';
foreach ($insertsqlarr as $insert_key => $insert_value) {
$insertkeysql .= $comma.'`'.$insert_key.'`';
$insertvaluesql .= $comma.'\''.$insert_value.'\'';
$comma = ', ';
}
$method = $replace?'REPLACE':'INSERT';
// echo $method." INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)", $silent?'SILENT':'';die;
$state = $db->query($method." INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)", $silent?'SILENT':'');
if($returnid && !$replace) {
return $db->insert_id();
}else {
return $state;
}
}
function updatetable($tablename, $setsqlarr, $wheresqlarr, $silent=0) {
global $db;
$setsql = $comma = '';
foreach ($setsqlarr as $set_key => $set_value) {
if(is_array($set_value)) {
$setsql .= $comma.'`'.$set_key.'`'.'=\''.$set_value[0].'\'';
} else {
$setsql .= $comma.'`'.$set_key.'`'.'=\''.$set_value.'\'';
}
$comma = ', ';
}
$where = $comma = '';
if(empty($wheresqlarr)) {
$where = '1';
} elseif(is_array($wheresqlarr)) {
foreach ($wheresqlarr as $key => $value) {
$where .= $comma.'`'.$key.'`'.'=\''.$value.'\'';
$comma = ' AND ';
}
} else {
$where = $wheresqlarr;
}
return $db->query("UPDATE ".($tablename)." SET ".$setsql." WHERE ".$where, $silent?"SILENT":"");
}
再往下走则是wheresql()函数,是SQL语句查询的Where条件拼接的地方,我们可以看到参数都使用了单引号进行包裹,代码如下:
function wheresql($wherearr='')
{
$wheresql="";
if (is_array($wherearr))
{
$where_set=' WHERE ';
foreach ($wherearr as $key => $value)
{
$wheresql .=$where_set. $comma.$key.'="'.$value.'"';
$comma = ' AND ';
$where_set=' ';
}
}
return $wheresql;
}
还有一个访问令牌生成的函数asyn_userkey(), 拼接用户名、密码salt以及密码进行一次md5,访问的时候只要在GET参数key的值里面加上生成的这个key即可验证是否有权限,被用在注册、找回密码等验证过程中,也就是我们能看到的找回密码链接里面的key,代码如下:
同目录下的文件如下图所示:
图中是具体功能的实现代码,我们这时候还不需要看,先了解下程序的其他结构。
3. 查看配置文件
接下来我们找找配置文件,之前我们介绍到配置文件的文件名通常都带有 “config” 这样的关键字,我们只要搜索带有这个关键字的文件名即可,如下图所示。
在搜索结果中我们可以看到搜索出来多个文件,结合文件所在目录这个经验可以判断出data目录下面的config.php以及 cache_config.php 才是真正的配置文件,打开/data/config.php查看代码,如下所示: .
<?php
$dbhost = "localhost";
$dbname = "74cms";
$dbuser = "root";
$dbpass = "123456";
$pre = 'qs_ ";
$QS_cookiedomain = ' ' ;
$QS_cookiepath = "/74cms/";
$QS_pwdhash = "KOciF:RkE4xNhu@S";
define ('QISHI_CHARSET', 'gb2312') ;
define ('QISHI_DBCHARSET', 'GBK') ; // qishi_dbcharset
?>
很明显看到,很有可能存在我们之前说过的双引号解析代码执行的问题,通常这个配置是在安装系统的时候设置的,或者后台也有设置的地方。另外我们还应该记住的一个点是QISHI_DBCHARSET常量,这里配置的数据库编码是GBK,也就可能存在宽字节注入,不过需要看数据库连接时等设置的编码,不妨找找看,找到骑士cms连接MySQL的代码在include\mysql.class.php文件的connect()函数,代码如下:
这段代码里面有个关键的地方,见选中部分的代码,这里存在安全隐患。
代码首先判断MySQL版本是否大于4.1,如果是则执行如下代码:
mysql_query( "SET NAMES gbk");
执行这个语句之后再判断,如果大于 5 则执行如下代码:
mysql_query("SET character_set_connection=".$dbcharset.", character_set_results=".$dbcharset.", character_set_client=binary", $this->linkid);
character_set_client 客户端使用的编码,如GBK, UTF8 比如你写的sql语句是什么编码的。
character_set_results 查询返回的结果集的编码(从数据库读取的数据是什么编码的)。
character_set_connection 连接使用的编码
也就是说在MySQL版本小于 5 的情况下是不会执行这行代码的,但是执行了“set names gbk
",“set names gbk"其实等同于干了三件事:
SET character_set_connection=' gbk ', character_set_results=' gbk ', character_set_client=' gbk '
因此在MySQL版本大于 4.1 小于 5 的情况下,基本所有跟数据库有关的操作都存在宽字节注入。
跟读首页Index文件
通过对系统文件大概的了解,我们对这套程序的整体架构已经有了一定的了解,但是还不够,所以我们得跟读一下index.php文件,看看程序运行的时候会调用哪些文件和函数。
打开首页文件index.php就可以看到如下代码:
首先判断安装锁文件是否存在,如果不存在则跳转到install/index.php,接下来是包含/include/common.inc.php文件,跟进该文件查看:
发现/include/common.inc.php文件在开头包含了三个文件,data/config.php 为数据库配置文件,include/common.fun.php 文件为基础函数库集文件,include/74cms_version.php 为应用版本文件。接着往下看:
if (!empty($_GET))
{
$_GET = addslashes_deep($_GET);
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
$_COOKIE = addslashes_deep($_COOKIE);
$_REQUEST = addslashes_deep($_REQUEST);
这段代码调用了 include/common.fun.php 文件里面的 addslashes_deep() 函数对GET/POST/COOKIE参数进行了过滤,再往下走可以看到又有一个包含文件的操作:
require_once (QISHI_ROOT_PATH.' include/tpl.inc.php') ;
包含了 include/tpl.inc.php 文件,跟进看看这个文件做了什么:
首先看到包含了 include/template_lite/classtemplate.php 文件,这是一个映射程序模板的类,由Paul Lockaby paul 和Mark Dickenson编写,由于该文件较大,我们这里不再仔细分析,继续往下跟进,可以看到这段代码实例化了这个类对象赋值给$smarty变量,继续跟进则回转到index.php文件代码:
判断是否已经缓存,然后调用display()函数输出页面,审计到这里是否对整个程序的框架比较熟悉了?接下来像审计index.php文件一样跟进其他功能入口文件即可完成代码通读。