[PHP防火墙]输入内容存在危险字符,安全起见,已被本站拦截

之前在很多的网站都看到了360webscan的攻击拦截脚本,正好分析并学习一下。

下载地址:http://webscan.360.cn/protect/down?domain=blog.dyboy.cn

最后一个 domain 参数改为自己的线上网站域名

为了本地测试:我下载http://webscan.360.cn/protect/down?domain=www.test.com

0x01 安装

将下载的 360webscan.zip 解压后,得到360safe文件夹,并上传至网站根目录

在全局加载的文件中(示例网站根目录下:index.php),加入如下代码:

if(is_file($_SERVER['DOCUMENT_ROOT'].'/360safe/360webscan.php')){
    require_once($_SERVER['DOCUMENT_ROOT'].'/360safe/360webscan.php');
} //注意文件路径

访问:http://www.test.com/360safe/360webscan.php post-data: webscan_act=ckinstall

但是并没有出现安装信息,原因是:http://safe.webscan.360.cn

该域名已经无法访问(后面涉及到这个网址的函数都不无法正常执行),因此着重分析拦截过滤的一个过程。

看到这个脚本文件的最后编辑时间为2014年…


0x02 结构分析

webscan_cache.php

webscan_cache.php

默认拦截,POST/GET/COOKIE/REFERER 这四个参数

同时还有白名单功能

//url白名单,可以自定义添加url白名单,默认是对phpcms的后台url放行
//写法:比如phpcms 后台操作url index.php?m=admin php168的文章提交链接post.php?job=postnew&step=post ,dedecms 空间设置edit_space_info.php
$webscan_white_url = array('index.php' => 'm=admin','post.php' => 'job=postnew&step=post','edit_space_info.php'=>'');

很清晰的解释了

再看 360webscan.php

360webscan.php

所有的过滤规则以及函数实现都在此文件

0x03 功能测试

在按照上述安装方法安装后,测试访问:http://www.test.com/index.php?test=<script>alert(1)</script>

XSS拦截显示:

XSS被拦截

比如注入等都会被拦截


0x04 拦截规则

//get拦截规则
$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//post拦截规则
$postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//cookie拦截规则
$cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//获取指令
$webscan_action  = isset($_POST['webscan_act'])&&webscan_cheack() ? trim($_POST['webscan_act']) : '';
//referer获取
$webscan_referer = empty($_SERVER['HTTP_REFERER']) ? array() : array('HTTP_REFERER'=>$_SERVER['HTTP_REFERER']);

0x05 运行分析

在程序的底部调用函数,过滤判断四种参数是否存在非法攻击字符串,如果是在白名单目录下(webscan_white()函数 ),就不会调用第二层的判断(四种拦截方式)

拦截四种

继续跟进:webscan_white()

/**
 *  拦截目录白名单
 */
function webscan_white($webscan_white_name,$webscan_white_url=array()) {
  $url_path=$_SERVER['SCRIPT_NAME'];  //修复之前是PHP_SELF
  $url_var=$_SERVER['QUERY_STRING'];
  if (preg_match("/".$webscan_white_name."/is",$url_path)==1&&!empty($webscan_white_name)) {
    return false;
  }
  foreach ($webscan_white_url as $key => $value) {
    if(!empty($url_var)&&!empty($value)){
      if (stristr($url_path,$key)&&stristr($url_var,$value)) {
        return false;
      }
    }
    elseif (empty($url_var)&&empty($value)) {
      if (stristr($url_path,$key)) {
        return false;
      }
    }
  }
  return true;
}

1.如果你输入 /test.php/123456 的话 $_SERVER['SCRIPT_NAME']结果是/test.php 。所以为了安全起见,为了指向自身,应该用$_SERVER['SCRIPT_NAME']

2.$_SERVER['QUERY_STRING']获取 ? 后面的字符串,例如:index.php?action=login&username=123&pass=123,那么获取的结果就是:action=login&username=123&pass=123

3.preg_mactch函数: 搜索subjectpattern给定的正则表达式的一个匹配.

reference: http://php.net/manual/zh/function.preg-match.php
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )

正则语法:http://php.net/manual/zh/reference.pcre.pattern.syntax.php

翻译了一下

Regex quick reference
[abc]     A single character: a, b or c      单独的字符
[^abc]     Any single character but a, b, or c      匹配字符除了abc
[a-z]     Any single character in the range a-z        匹配a到z的字符
[a-zA-Z]     Any single character in the range a-z or A-Z    匹配a到z或A到Z的字符
^     Start of line            一行的开始
$     End of line            一行的结束
\A     Start of string        字符串开头
\z     End of string        字符串结尾
.     Any single character        任何字符
\s     Any whitespace character            任何空白字符
\S     Any non-whitespace character        任何非空白字符
\d     Any digit        任何数字
\D     Any non-digit        任何非数字
\w     Any word character (letter, number, underscore)        任何的单词字符(字母,数字,下划线)
\W     Any non-word character            任何非单词字符
\b     Any word boundary character        任何单词边界字符
(...)     Capture everything enclosed    捕获所未包裹有内容
(a|b)     a or b        a或b
a?     Zero or one of a          有0个或1个字符a
a*     Zero or more of a        有0个或多个字符a
a+     One or more of a            有1个或多个字符a
a{3}     Exactly 3 of a            有3个字符a
a{3,}     3 or more of a        有3个或多个字符a
a{3,6}     Between 3 and 6 of a            有3到6个字符a

options: i case insensitive m make dot match newlines x ignore whitespace in regex o perform #{...} substitutions only once

可选设置:i不区分大小写,m使得.(点符号)匹配换行符,x忽略正则表达式中的空格,o只执行一次#{...}中内容替换

其中的\\等价于\

\\\\等价于\\等价于/

4.strsti()函数:返回 haystack 字符串从 needle 第一次出现的位置开始到结尾的字符串。

reference: http://php.net/manual/zh/function.stristr.php
string stristr ( string $haystack , mixed $needle [, bool $before_needle = FALSE ] )

在整个白名单判断函数中,如果匹配上了,那么就返回false,就不做拦截检测,针对白名单这一点其实是有漏洞可绕过的,传递的第一个参数$webscan_white_name是一个全局参数在webscan_cache.php文件中

//后台白名单,后台操作将不会拦截,添加"|"隔开白名单目录下面默认是网址带 `admin`  `/dede/` 放行
`$webscan_white_directory='admin|\/dede\/'`;

这样的话,那么我们只要在admin 或者 dede目录下的任何操作都不会被拦截。如果存在后台注入的话,同时在后台添加了白名单,那么拦截就不再有效果了。

同时提一点:如上代码,注释了一下 $url_path=$_SERVER['SCRIPT_NAME']; //修复之前是PHP_SELF,这里存在一个安全问题,直接引用一下离别歌大佬的博文:

然后再给大家说明一下$_SERVER['PHP_SELF']是什么:
PHP_SELF指当前的页面绝对地址,比如我们的网站:
https://www.leavesongs.com/hehe/index.php
那么PHP_SELF就是/hehe/index.php。
但有个小问题很多人没有注意到,当url是PATH_INFO的时候,比如
https://www.leavesongs.com/hehe/index.php/phithon
那么PHP_SELF就是/hehe/index.php/phithon
也就是说,其实PHP_SELF有一部分是我们可以控制的。

ok,那么如果目录不在白名单中,那么就会下一步匹配参数是否在白名单中,如果能够匹配上也返回false

进入过滤检测手中,比如xss过滤:

这样的:http://www.test.com/index.php?id=123%3Ciframe%20src=http://www.xxx.com/1.js%3E 是不会被过滤的

然后调用webscan_StopAttack()函数将拦截规则与当前的GET/POST/COOKIE/REFERER参数匹配!

那么直接看GET请求中的过滤规则吧!

//get拦截规则
$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

简单解读,只要规则中出现的单词或连续字符,那么在访问链接URL中就不能存在这些关键词,否则就会被拦截。

为什么要简单解读呐?因为这TM的规则太复杂了…

可以把 | 分割开的看成一个小规则,这样子来分别分析

在上面我们看到iframe关键词没被过滤,那么改为如下的:

//添加一个iframe关键词 iframe|

$getfilter = "iframe|\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

规则修改

这样就起到了拦截效果

其他的请求都是类似的,正则语法真难!真香!

如果匹配到了需要拦截过滤的关键词,就会调用webscan_pape()函数,及调用拦截结果显示页面,如上图所示。

0x06 总结

正则语法看得心力憔悴,更多的匹配规则得自己下来写一写,然后在本地环境输出查看!

脚本防火墙真方便!正则匹配就好了,在这个360webscan的过滤插件中,还是看到了函数封装的美感,Do you like these?

发表评论 / Comment

用心评论~

金玉良言 / Appraise
天凉好个秋LV 2
2019-04-30 23:51
分析得很好
MrxnLV 2
2018-09-29 22:35
你这多久的版本了。。。2014
你看看新版的:https://pastebin.com/g9TkMxbc
这个正则之全面。。。看吐了都,据我的水平,目前这个正则几乎绕不过。。。
头像
DYBOY站长已认证
2018-10-09 12:10
@Mrxn:O(∩_∩)O谢谢!

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/blog.dyboy.cn/content/templates/dyblog/footer.php:56) in /www/wwwroot/blog.dyboy.cn/include/lib/view.php on line 23