返回介绍

9.1.1 discuz SQL 安全过滤类分析

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

discuz 全称 Crossday Discuz!Board,是康盛创想(北京)科技有限公司(英文简称 Comsenz)推出的一套开源通用的社区论坛软件系统,使用 PHP+MySQL 开发,现已被腾讯收购,由于用户量巨大,discuz 一直是众多安全爱好者重点研究的对象,所以也被公布过不少的安全漏洞。经过数年的沉淀,如今的 discuz 主程序在代码安全方面已经做得比较成熟。

discuz 在专门有一个 SQL 注入过滤类来过滤 SQL 注入请求,不过也出现了多次绕过的情况,下面我们来分析它的这个 SQL 注入过滤的类。

首先我们先看到 discuz 的配置文件/config/config_global.php 中的“CONFIG SECURITY”部分内容,如下:

// -------------------------  CONFIG SECURITY  -------------------------- //

$_config['security']['authkey'] = '3ca530i1uCe7lRke' ; 

$_config['security']['urlxssdefend'] = 1 ; 

$_config['security']['attackevasive'] = '0' ; 

$_config['security']['querysafe']['status'] = 1 ; // 是否开启 SQL 注入防御 

// 以下是过滤规则 

$_config['security']['querysafe']['dfunction']['0'] = 'load_file' ; 

$_config['security']['querysafe']['dfunction']['1'] = 'hex' ; 

$_config['security']['querysafe']['dfunction']['2'] = 'substring' ; 

$_config['security']['querysafe']['dfunction']['3'] = 'if' ; 

$_config['security']['querysafe']['dfunction']['4'] = 'ord' ; 

$_config['security']['querysafe']['dfunction']['5'] = 'char' ; 

$_config['security']['querysafe']['daction']['0'] = '@' ; 

$_config['security']['querysafe']['daction']['1'] = 'intooutfile' ; 

$_config['security']['querysafe']['daction']['2'] = 'intodumpfile' ; 

$_config['security']['querysafe']['daction']['3'] = 'unionselect' ; 

$_config['security']['querysafe']['daction']['4'] = ' ( select' ; 

$_config['security']['querysafe']['daction']['5'] = 'unionall' ; 

$_config['security']['querysafe']['daction']['6'] = 'uniondistinct' ; 

$_config['security']['querysafe']['dnote']['0'] = '/*' ; 

$_config['security']['querysafe']['dnote']['1'] = '*/' ; 

$_config['security']['querysafe']['dnote']['2'] = '#' ; 

$_config['security']['querysafe']['dnote']['3'] = '--' ; 

$_config['security']['querysafe']['dnote']['4'] = '"' ; 

$_config['security']['querysafe']['dlikehex'] = 1 ; 

$_config['security']['querysafe']['afullnote'] = '0' ;

根据笔者的标注(上面加粗代码),我们可以看到 discuz 配置文件中可以设置是否开启 SQL 注入防御,这个选项默认开启,一般不会有管理员去关闭,再往下的内容:

$_config['security']['querysafe']['daction'] 以及 $_config['security']['querysafe']['dnote']

都是 SQL 注入过滤类的过滤规则,规则里包含了常见的注入关键字。

Discuz 执行 SQL 语句之前会调用\source\class\discuz\discuz_database.php 文件 discuz_database_safecheck 类下面的 checkquery($sql)函数进行过滤,我们来跟进这个函数看看,代码如下:

public static function checkquery ( $sql ) {if ( self :: $config === null ) { self :: $config = getglobal ( 'config/security/querysafe' ); }if ( self :: $config['status'] ) { $check = 1 ; $cmd = strtoupper ( substr ( trim ( $sql ), 0 , 3 )); if ( isset ( self :: $checkcmd[$cmd] )) {   $check = self :: _do_query_safe ( $sql ); } elseif ( substr ( $cmd , 0 , 2 ) === '/*' ) { $check = -1 ; } if ( $check < 1 ) {   throw new DbException ( 'It is not safe to do this query' , 0 , $sql ); }}return true ; 

}

从代码中可以看到,程序首先加载配置文件中的 config/security/querysafe,根据$config['status']判断 SQL 注入防御是否开启,再到$check=self::_do_query_safe($sql);可以看到该函数又调用了同类下的_do_query_safe() 函数对 SQL 语句进行过滤,我们继续跟进_do_query_safe() 函数,代码如下:

private static function _do_query_safe ( $sql ) {

  $sql = str_replace ( array ( '\\\\' , '\\\'' , '\\"' , '\'\'' ), '' , $sql ); 

  $mark = $clean = '' ; 

  if ( strpos ( $sql , '/' ) === false && strpos ( $sql , '#' ) === false && strpos ( $sql , '-- ' ) === false && strpos ( $sql , '@' ) === false && strpos ( $sql , '`' ) === false ) {   $clean = preg_replace ( "/' ( .+ ?) '/s" , '' , $sql ); 

  } else {   $len = strlen ( $sql );  $mark = $clean = '' ; for ( $i = 0 ; $i < $len ; $i++ ) { $str = $sql[$i] ; switch ( $str ) {   case '`' :   if (! $mark ) {    $mark = '`' ;     $clean .= $str ;   } elseif ( $mark == '`' ) {    $mark = '' ;   }  break ;    case '\'' :   if (! $mark ) {    $mark = '\'' ;     $clean .= $str ;   } elseif ( $mark == '\'' ) {   $mark = '' ;   }    break ;   case '/' :     if ( empty ( $mark ) && $sql[$i + 1] == '*' ) {    $mark = '/*' ;     $clean .= $mark ;     $i++ ;     } elseif ( $mark == '/*' && $sql[$i - 1] == '*' ) {    $mark = '' ;     $clean .= '*' ;     }    break ;     case '#' :     if ( empty ( $mark )) {      $mark = $str ;       $clean .= $str ;     }    break ;     case "\n" :     if ( $mark == '#' || $mark == '--' ) {      $mark = '' ;     }    break ;     case '-' :     if ( empty ( $mark ) && substr ( $sql , $i , 3 ) == '-- ' ) {      $mark = '-- ' ;       $clean .= $mark ;     }    break ;     default :     break ; }$clean .= $mark?'' : $str ; }}

    if ( strpos ( $clean , '@' )
 
! == false ) {

    return '-3' ; 

    }

    $clean = preg_replace ( "/[^a-z0-9_\-\ ( \ ) #\*\/\"]+/is" , "" , strtolower ( $clean )); 

    if ( self :: $config['afullnote'] ) {

    $clean = str_replace ( '/**/' , '' , $clean ); 

    }

    if ( is_array ( self :: $config['dfunction'] )) {

    foreach ( self :: $config['dfunction'] as $fun ) {

     if ( strpos ( $clean , $fun . ' ( ' )
 
! == false ) 

        return '-1' ; 

    }

    }

    if ( is_array ( self :: $config['daction'] )) {

    foreach ( self :: $config['daction'] as $action ) {

     if ( strpos ( $clean , $action )
 
! == false ) 

    return '-3' ; 

    }

}

    if ( self :: $config['dlikehex'] && strpos ( $clean , 'like0x' )) {

    return '-2' ; 

    }

    if ( is_array ( self :: $config['dnote'] )) {

    foreach ( self :: $config['dnote'] as $note ) {

     if ( strpos ( $clean , $note )
 
! == false ) 

    return '-4' ; 

    }

}

    return 1 ; 

}

从如上代码我们可以看到,该函数首先使用:

$sql = str_replace ( array ( '\\\\' , '\\\'' , '\\"' , '\'\'' ), '' , $sql );

将 SQL 语句中的\\、\'、\"以及''替换为空,紧接着是一个 if else 判断逻辑来选择过滤的方式:

if ( strpos ( $sql , '/' ) === false && strpos ( $sql , '#' ) === false && strpos ( $sql , '-- ' ) === false && strpos ( $sql , '@' ) === false && strpos ( $sql , '`' ) === false ) {

     $clean = preg_replace ( "/' ( .+ ?) '/s" , '' , $sql ); 

     } else {

这段代码表示当 SQL 语句里存在'/'、#'、'--'、'@'、'`'这些字符时,则直接调用 preg_replace() 函数将单引号(')中间的内容替换为空,这里之前存在一个绕过,只要把 SQL 注入的语句放到单引号中间,则会被替换为空,进行下面再判断的时候已经检测不到 SQL 注入的关键字,导致绕过的出现,在 MySQL 中使用 @`'`代表 null,SQL 语句可以正常执行。

else 条件中是对整段 SQL 语句进行逐个字符进行判断,比如

case '/' : 

     if ( empty ( $mark ) && $sql[$i + 1] == '*' ) {

     $mark = '/*' ; 

     $clean .= $mark ; 

     $i++ ; 

     } elseif ( $mark == '/*' && $sql[$i - 1] == '*' ) {

     $mark = '' ; 

     $clean .= '*' ; 

     }

     break ;

这段代码的逻辑是,当检查到 SQL 语句中存在斜杠(/)时,则去判断下一个字符是不是星号(*),如果是星号(*)就把这两个字符拼接起来,即/*,然后继续判断下一个字符是不是星号(*),如果是星号则再继续拼接起来,得到/**,最后在如下代码中判断是否存在原来拦截规则里面定义的字符,如果存在则拦截 SQL 语句执行:

if ( is_array ( self :: $config['dnote'] )) {

     foreach ( self :: $config['dnote'] as $note ) {

     if ( strpos ( $clean , $note )
 
! == false ) 

    return '-4' ; 

     }

}

国内知名的多款 cms 应用如 dedecms 等,都有使用类似这个过滤类,另外由于应用的基础架构不一样,这个过滤类应用起来的实际效果也各不太一样,discuz 目前做得相对较好。

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

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

发布评论

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