代码安全之参数安全过滤
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;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[] = '<;';
$searchs[] = '>';
$replaces[] = '>;';
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[] = "<;".$value.">;";
$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('";','"',$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实体编码,如&转换成&;,"转换成";,'转换成';,<转换成<;,>转换成>;这个函数简单粗暴但是却非常有效果,已经能干掉大多数的XSS攻击。
而strip_tags()函数则是用来去掉HTML及PHP标记,比如给这个函数传入“<h1>xxxxx</h1>”,经过它处理后返回的字符串为xxxxx。
3、命令执行过滤函数
通常我们进行系统命令注入的时候会使用到||以及&等字符,PHP为了防止系统命令注入的漏洞,提供了escapeshellcmd()和escapeshellarg()两个函数对参数进行过滤,escapeshellcmd()函数过滤的字符为'&'、';'、'`'、'|'、'*'、'?'、'~'、'<'、'>'、'^'、'('、')'、'['、']'、'{'、'}'、'$'、'\'、'\x0A'、'\xFF'、’%’以及单双引号,Windows下过滤方式则是在这些字符前面加了一个^符号,而在Linux下则是在这些字符前面加了反斜杠(\)。escapeshellarg()函数过滤方式比较简单,给所有参数加上一对双引号,强制为字符串。
微信公众号:计算机与网络安全
ID:Computer-network