返回介绍

骑士 cms 通读审计案例

发布于 2024-10-11 22:07:42 字数 12578 浏览 0 评论 0 收藏 0

我们已经介绍了代码审计中通读全文代码审计方式的思路,下面我们用案例来说明这种通读方式。

为了方便大家理解,笔者找了一款相对简单容易看懂的应用骑士 cms 来介绍,版本是 3.5.1,具体的审计思路我们在上文中已经有过介绍。

3.2.1.1 查看应用文件结构

首先来看一下骑士 cms 的大致文件目录结构,如图 3-8 所示。

图 3-8

首先需要看看有哪些文件和文件夹,寻找名称里有没有带有 api、admin、manage、include 一类关键字的文件和文件夹,通常这些文件比较重要,在这个程序里,可以看到并没有什么 PHP 文件,就一个 index.php,看到有一个名为 include 的文件夹,一般比较核心的文件都会放在这个文件夹中,我们先来看看大概有哪些文件,如图 3-9 所示。

图 3-9

3.2.1.2 查看关键文件代码

在这个文件夹里面我们看到了多个数十 K 的 PHP 文件,比如 common.fun.php 就是本程序的核心文件,基础函数基本在这个文件中实现,我们来看看这个文件里有哪些关键函数,一打开这个文件,立马就看到一大堆过滤函数,这是我们最应该关心的地方,首先是一个 SQL 注入过滤函数:

function addslashes_deep ( $value ) 

{

  if ( empty ( $value )) 

  {

    return $value ; 

  }

  else

  {

    if (! get_magic_quotes_gpc ()) 

    {

    $value=is_array ( $value )? array_map ( 'addslashes_deep' , $value ): mystrip_ tags ( addslashes ( $value )); 

    }

    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 ( '&amp ; ' , '&quot ; ' , '&lt ; ' , '&gt ; ' ), 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' , 'bench-mark' , 'select' , 'update' , 'insert' , 'delete' , 'alter' , 'drop' , 'truncate' , 'script' , 'eval' ); 

  $parm = array_merge ( $parm1 , $parm2 , $parm3 ); 

    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 .= '| ( &#0 ( [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 地址的:

function getip () 

{

    if ( getenv ( 'HTTP_CLIENT_IP' ) and strcasecmp ( getenv ( 'HTTP_CLIENT_IP' ), 'unknown' )) {

    $onlineip=getenv ( 'HTTP_CLIENT_IP' ); 

    }elseif ( getenv ( 'HTTP_X_FORWARDED_FOR' ) and strcasecmp ( getenv ( 'HTTP_X_FORWARDED_FOR' ), 'unknown' )) {

    $onlineip=getenv ( 'HTTP_X_FORWARDED_FOR' ); 

    }elseif ( getenv ( 'REMOTE_ADDR' ) and strcasecmp ( getenv ( 'REMOTE_ADDR' ), 'unknown' )) {

    $onlineip=getenv ( 'REMOTE_ADDR' ); 

    }elseif ( isset ( $_SERVER['REMOTE_ADDR'] ) and $_SERVER['REMOTE_ADDR'] and strcasecmp ( $_SERVER['REMOTE_ADDR'] , 'unknown' )) {

    $onlineip=$_SERVER['REMOTE_ADDR'] ; 

    }

    preg_match ( "/\d{1 , 3}\.\d{1 , 3}\.\d{1 , 3}\.\d{1 , 3}/" , $onlineip , $match ); 

    return $onlineip = $match[0]?$match[0] : 'unknown' ; 

}

很多应用都会由于在获取 IP 时没有验证 IP 格式,而存在注入漏洞,不过这里还只是可以伪造 IP。

再往下看可以看到一个值得关注的地方,SQL 查询统一操作函数 inserttable() 以及 updatetable() 函数,大多数 SQL 语句执行都会经过这里,所以我们要关注这个地方是否还有过滤等问题。

function inserttable ( $tablename , $insertsqlarr , $returnid=0 , $replace = false , $silent=0 ) 

{global $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 ; }

}

再往下走则是 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,代码如下:

function asyn_userkey ( $uid ) 

{

    global $db ; 

    $sql = "select * from ".table ( 'members' ) ." where uid = '".intval ( $uid ) ."' LIMIT 1" ; 

    $user=$db->getone ( $sql ); 

    return md5 ( $user['username'].$user['pwd_hash'].$user['password'] ); 

}

同目录下的文件如图 3-10 所示。

图 3-10

图中是具体功能的实现代码,我们这时候还不需要看,先了解下程序的其他结构。

3.2.1.3 查看配置文件

接下来我们找找配置文件,上面我们介绍到配置文件的文件名通常都带有“config”这样的关键字,我们只要搜索带有这个关键字的文件名即可,如图 3-11 所示。

在搜索结果中我们可以看到搜索出来多个文件,结合文件所在目录这个经验可以判断出 data 目录下面的 config.php 以及 cache_config.php 才是真正的配置文件,打开/data/config.php 查看代码,如下所示:

图 3-11

<?php $dbhost = "localhost" ; $dbname = "74cms" ; $dbuser = "root" ; $dbpass = "123456" ; $pre = "qs_" ; $QS_cookiedomain = '' ; $QS_cookiepath = "/74cms/" ; $QS_pwdhash = "K0ciF : RkE4xNhu@S" ; define ( 'QISHI_CHARSET' , 'gb2312' ); define ( 'QISHI_DBCHARSET' , 'GBK' );? >

很明显看到,很有可能存在我们之前说过的双引号解析代码执行的问题,通常这个配置是在安装系统的时候设置的,或者后台也有设置的地方。另外我们还应该记住的一个点是 QISHI_DBCHARSET 常量,这里配置的数据库编码是 GBK,也就可能存在宽字节注入,不过需要看数据库连接时设置的编码,不妨找找看,找到骑士 cms 连接 MySQL 的代码在 include\mysql.class.php 文件的 connect() 函数,代码如下:

function connect ( $dbhost , $dbuser , $dbpw , $dbname = '' , $dbcharset = 'gbk' , $connect=1 ) {

    $func = empty ( $connect )
 
? 'mysql_pconnect' : 'mysql_connect' ; 

    if (! $this->linkid = @$func ( $dbhost , $dbuser , $dbpw , true )) {

     $this->dbshow ( 'Can not connect to Mysql ! ' ); 

    } else {

     if ( $this->dbversion () > '4.1' ) {

    mysql_query ( "SET NAMES gbk" ); 

    if ( $this->dbversion () > '5.0.1' ) {

       mysql_query ( "SET sql_mode = ''" , $this->linkid ); 

       mysql_query ( "SET character_set_connection=".$dbcharset." , character_set_results=".$dbcharset." , character_set_client=binary" , $this-> linkid ); 

     }

     }

    }

    if ( $dbname ) {

     if ( mysql_select_db ( $dbname , $this->linkid ) ===false ) {

    $this->dbshow ( "Can't select MySQL database ( $dbname )! " ); 

     }

    }

}

这段代码里面有个关键的地方,见加粗代码,这里存在安全隐患。

代码首先判断 MySQL 版本是否大于 4.1,如果是则执行如下代码:

mysql_query ( "SET NAMES gbk" );

执行这个语句之后再判断,如果大于 5 则执行如下代码:

mysql_query ( "SET character_set_connection=".$dbcharset." , 

  haracter_set_results=".$dbcharset." , character_set_client=binary" , 

  $this->linkid );

也就是说在 MySQL 版本小于 5 的情况下是不会执行这行代码的,但是执行了“set names gbk”,我们在之前介绍过“set names gbk”其实干了三件事,等同于:

SET character_set_connection= ’ gbk ’, haracter_set_results= ’ gbk ’, 

  character_set_client= ’ gbk ’

因此在 MySQL 版本大于 4.1 小于 5 的情况下,基本所有跟数据库有关的操作都存在宽字节注入。

3.2.1.4 跟读首页文件

通过对系统文件大概的了解,我们对这套程序的整体架构已经有了一定的了解,但是还不够,所以我们得跟读一下 index.php 文件,看看程序运行的时候会调用哪些文件和函数。

打开首页文件 index.php 可以看到如下代码:

if (! file_exists ( dirname ( __FILE__ ) .'/data/install.lock' )) 

  header ( "Location : install/index.php" ); 

define ( 'IN_QISHI' , true ); 

$alias="QS_index" ; 

require_once ( dirname ( __FILE__ ) .'/include/common.inc.php' );

首先判断安装锁文件是否存在,如果不存在则跳转到 install/index.php,接下来是包含/include/common.inc.php 文件,跟进该文件查看:

require_once ( QISHI_ROOT_PATH.'data/config.php' ); 

header ( "Content-Type : text/html ; charset=".QISHI_CHARSET ); 

require_once ( QISHI_ROOT_PATH.'include/common.fun.php' ); 

require_once ( QISHI_ROOT_PATH.'include/74cms_version.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_once ( QISHI_ROOT_PATH.'include/template_lite/class.template.php' ); 

$smarty = new Template_Lite ; 

$smarty -> cache_dir = QISHI_ROOT_PATH.'temp/caches/'.$_CFG['template_dir'] ; 

$smarty -> compile_dir =  QISHI_ROOT_PATH.'temp/templates_c/'.$_CFG ['template_dir'] ; 

$smarty -> template_dir = QISHI_ROOT_PATH.'templates/'.$_CFG['template_dir'] ; 

$smarty -> reserved_template_varname = "smarty" ; 

$smarty -> left_delimiter = "{#" ; 

$smarty -> right_delimiter = "#}" ; 

$smarty -> force_compile = false ; 

$smarty -> assign ( '_PLUG' , $_PLUG ); 

$smarty -> assign ( 'QISHI' , $_CFG ); 

$smarty -> assign ( 'page_select' , $page_select );

首先看到包含了 include/template_lite/class.template.php 文件,这是一个映射程序模板的类,由 Paul Lockaby paul 和 Mark Dickenson 编写,由于该文件较大,我们这里不再仔细分析,继续往下跟进,可以看到这段代码实例化了这个类对象赋值给$smarty 变量,继续跟进则回转到 index.php 文件代码:

if (! $smarty->is_cached ( $mypage['tpl'] , $cached_id )) 

{

require_once ( QISHI_ROOT_PATH.'include/mysql.class.php' ); 

$db = new mysql ( $dbhost , $dbuser , $dbpass , $dbname ); 

unset ( $dbhost , $dbuser , $dbpass , $dbname ); 

$smarty->display ( $mypage['tpl'] , $cached_id ); 

}

else

{

$smarty->display ( $mypage['tpl'] , $cached_id ); 

}

判断是否已经缓存,然后调用 display() 函数输出页面,审计到这里是否对整个程序的框架比较熟悉了?接下来像审计 index.php 文件一样跟进其他功能入口文件即可完成代码通读。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文