查看原文
其他

代码安全之参数安全过滤

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:16004488


微信公众号:计算机与网络安全

ID:Computer-network

所有对Web应用的攻击都要传入有害的参数,因此代码安全的基础就是对传入的参数进行有效的过滤,比如像SQL注入漏洞,只要过滤到单引号,就能防御住大部分的string类型的SQL注入,只要过滤掉尖括号和单双引号也能过滤掉不少XSS漏洞,这种简单的过滤跟完全不过滤带来的效果是天壤之别,我们做的就是要细化这些过滤规则,通过横向扩展防御策略来拦截更多的攻击,不少第三方提供了这样的过滤函数和类,我们可以直接引用,另外PHP自身提供了不少过滤的函数,好好使用这些内置的函数也能达到非常好的效果。


一、第三方过滤函数与类


在一些中小型的Web应用程序中,由于大多数开发者是不怎么懂安全的,所以都会选择一些第三方的过滤函数或者类,直接拿过去套着用,并不知道效果到底怎么样。在PHP安全过滤的类里面,比较出名的有出自80sec团队给出的一个SQL注入过滤的类,在国内大大小小的程序像discuz、dedecms、phpmywind等都使用过。

目前大多数应用都有一个参数过滤的统一入口,类似于dedecms的代码,如下所示:


foreach(Array('_GET','_POST','_COOKIE')as $_request)

{

foreach($$_request as $_k => $_v)

{

if($_k == 'nvarname')${$_k} = $_v;

else ${$_k} = _RunMagicQuotes($_v);

}

}


跟进_RunMagicQuotes()函数之后的代码如下:


function _RunMagicQuotes(&$svar)

{

if(!get_magic_quotes_gpc())

{

if(is_array($svar))

{

foreach($svar as $_k => $_v)$svar[$_k] = _RunMagicQuotes($_v);

}

else

{

if(strlen($svar)>0&& preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$svar))

{

exit('Request var not allow!');

}

$svar = addslashes($svar);

}

}

return $svar;

}


而这里仅仅是使用addslashes()函数过滤,确实能防御住一部分漏洞,但是对特定的场景和漏洞就不那么好使了。所以除了总入口,在具体的功能点也需要进行一些过滤。


(一)discuz SQL安全过滤类分析


discuz全称Crossday Discuz!Board,是一套开源通用的社区论坛软件系统,使用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),,3));

if(isset(self::$checkcmd[$cmd]))

{

$check = self::_do_query_safe($sql);

elseif(substr($cmd,,2)=== '/*')

{

$check = -1;

}

if($check < 1)

{

throw new DbException('It is not safe to do this query',,$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目前做得相对较好。


(二)discuz XSS标签过滤函数分析


目前大多数XSS过滤都是基于黑名单的形式,编程语言和编码结合起来千变万化,基于黑名单的过滤总给人不靠谱的感觉,事实确实是这样,目前好像还没有看到基于黑名单过滤的规则一直没有被绕过,其实在XSS的防御上,只要过滤掉尖括号以及单、双引号就能干掉绝大部分的payload。下面我们来看看discuz的HTML标签过滤代码,如下所示:


function checkhtml($html)

{

if(!checkperm('allowhtml'))

{

preg_match_all("/\<([^\<]+)\>/is",$html,$ms);

$searchs[] = '<';

$replaces[] = '&lt;';

$searchs[] = '>';

$replaces[] = '&gt;';

if($ms[1])

{

$allowtags = 'img|a|font|div|table|tbody|caption|tr|td|th|br|p|b|strong|i|u|em|span|ol|ul|li|blockquote|object|param';

$ms[1] = array_unique($ms[1]);

foreach($ms[1] as $value)

{

$searchs[] = "&lt;".$value."&gt;";

$value = str_replace('&','_uch_tmp_str_',$value);

$value = dhtmlspecialchars($value);

$value = str_replace('_uch_tmp_str_','&',$value);

$value = str_replace(array('\\','/*'),array('.','/.'),$value);

$skipkeys = 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','javascript','script','eval','behaviour','expression','style','class');

$skipstr = implode('|',$skipkeys);

$value = preg_replace(array("/($skipstr)/i"),'.',$value);

if(!preg_match("/^[\/|\s]?($allowtags)(\s+|$)/is",$value))

{

$value = '';

}

$replaces[] = empty($value)?'':"<".str_replace('&quot;','"',$value).">";

}

}

$html = str_replace($searchs,$replaces,$html);

}

return $html;

}


从代码中可以看到,这里首先定义了一条正则取出来尖括号中间的内容:


preg_match_all("/\<([^\<]+)\>/is",$html,$ms);


然后在if($ms[1])这个if条件里对这些标签中的关键字进行筛选,$skipkeys变量定义了很多on事件的敏感字符,如下代码中可以看到,最后拼接正则将这些字符串替换掉:


$skipstr = implode('|',$skipkeys);

value = preg_replace(array("/($skipstr)/i"),'.',$value);


二、内置过滤函数


PHP本身内置了很多参数过滤的函数,以方便开发者简单有效且统一地进行安全防护,而这些函数可以分为多种类型,如SQL注入过滤函数、XSS过滤函数、命令执行过滤函数、代码执行过滤函数,等等,下面我们来看看这些函数的用法。


1、SQL注入过滤函数


SQL注入过滤函数有addslashes()、mysql_real_escape_string()以及mysql_escape_string(),它们的作用都是给字符串添加反斜杠(\)来转义掉单引号(')、双引号(")、反斜线(\)以及空字符NULL。addslashes()和mysql_escape_string()函数都是直接在敏感字符串前加反斜杠,这里可能会存在绕过宽字节注入绕过的问题,而mysql_real_escape_string()函数会考虑当前连接数据库的字符集编码,安全性更好,推荐使用。


2、XSS过滤函数


XSS过滤函数有htmlspecialchars()和strip_tags(),这两个函数的功能大不一样,htmlspecialchars()函数的作用是将字符串中的特殊字符转换成HTML实体编码,如&转换成&amp;,"转换成&quot;,'转换成&#039;,<转换成&lt;,>转换成&gt;这个函数简单粗暴但是却非常有效果,已经能干掉大多数的XSS攻击。


而strip_tags()函数则是用来去掉HTML及PHP标记,比如给这个函数传入“<h1>xxxxx</h1>”,经过它处理后返回的字符串为xxxxx。


3、命令执行过滤函数


通常我们进行系统命令注入的时候会使用到||以及&等字符,PHP为了防止系统命令注入的漏洞,提供了escapeshellcmd()和escapeshellarg()两个函数对参数进行过滤,escapeshellcmd()函数过滤的字符为'&'、';'、'`'、'|'、'*'、'?'、'~'、'<'、'>'、'^'、'('、')'、'['、']'、'{'、'}'、'$'、'\'、'\x0A'、'\xFF'、’%’以及单双引号,Windows下过滤方式则是在这些字符前面加了一个^符号,而在Linux下则是在这些字符前面加了反斜杠(\)。escapeshellarg()函数过滤方式比较简单,给所有参数加上一对双引号,强制为字符串。

    微信公众号:计算机与网络安全

    ID:Computer-network

    【推荐书籍】

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存