之前在很多的网站都看到了360webscan的攻击拦截脚本,正好分析并学习一下。
最后一个 domain 参数改为自己的线上网站域名
0x01 安装
将下载的 360webscan.zip 解压后,得到360safe文件夹,并上传至网站根目录
在全局加载的文件中(示例网站根目录下:index.php),加入如下代码:
if(is_file($_SERVER['DOCUMENT_ROOT'].'/360safe/360webscan.php')){
require_once($_SERVER['DOCUMENT_ROOT'].'/360safe/360webscan.php');
} //注意文件路径
该域名已经无法访问(后面涉及到这个网址的函数都不无法正常执行),因此着重分析拦截过滤的一个过程。
看到这个脚本文件的最后编辑时间为2014年…
0x02 结构分析
在 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
所有的过滤规则以及函数实现都在此文件
0x03 功能测试
在按照上述安装方法安装后,测试访问:http://www.test.com/index.php?test=
XSS拦截显示:
比如注入等都会被拦截
0x04 拦截规则//get拦截规则
$getfilter = "\\<.>|<.>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|
//post拦截规则
$postfilter = "<.>|<.>|\\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]+?|>|
//cookie拦截规则
$cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|
//获取指令
$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函数: 搜索subject与pattern给定的正则表达式的一个匹配.
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 ]]] )
翻译了一下
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过滤:
然后调用webscan_StopAttack()函数将拦截规则与当前的GET/POST/COOKIE/REFERER参数匹配!
那么直接看GET请求中的过滤规则吧!
//get拦截规则
$getfilter = "\\<.>|<.>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|
简单解读,只要规则中出现的单词或连续字符,那么在访问链接URL中就不能存在这些关键词,否则就会被拦截。
为什么要简单解读呐?因为这TM的规则太复杂了…
可以把 | 分割开的看成一个小规则,这样子来分别分析
在上面我们看到iframe关键词没被过滤,那么改为如下的:
//添加一个iframe关键词 iframe|
$getfilter = "iframe|\\<.>|<.>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|
这样就起到了拦截效果
其他的请求都是类似的,正则语法真难!真香!
如果匹配到了需要拦截过滤的关键词,就会调用webscan_pape()函数,及调用拦截结果显示页面,如上图所示。
0x06 总结
正则语法看得心力憔悴,更多的匹配规则得自己下来写一写,然后在本地环境输出查看!
脚本防火墙真方便!正则匹配就好了,在这个360webscan的过滤插件中,还是看到了函数封装的美感,Do you like these?