每一个可以努力的日子,都是一份厚礼。
VPS 屏蔽扫描网站的 IP
前段时间博客经常性地无法访问,网站宕机。SSH 上去看进程,发现大量 php-fpm 占用系统资源,查看服务器的 Nginx 日志,就知道发生了什么事情。个别 IP “友情”为我的站点扫描漏洞,瞬时并发连接很大。我知道大家也没什么恶意,只是用黑客工具比较兴奋,拿 www.lovelucy.info 练练手而已嘛。但是博主很穷,小站搭建在一个配置并不高的免费 VPS 上,折腾不起,压力很大,结果一不小心让各位搞成 DoS 拒绝服务攻击了,真是惭愧。
有趣的是,除掉一些穷举后台密码的,扫描者一股脑地发请求,大部分却是在找 asp 的漏洞。可是这样好浪费时间啊,尘埃落定的博客实在是用的 wordpress 程序,是 php 平台啊……
GET /mirserver.rar 404
GET /save.asp 404
GET /wwwroot.rar 404
GET /upfile_flash.asp 404
GET /web.rar 404
GET /mirserver3.rar 404
GET /www.rar 404
GET /eWebEditor/admin_login.asp 404
GET /mirserver.zip 404
GET /wwwroot.zip 404
GET /mirserver4.rar 404
GET /newsadmin/ubb/admin_login.asp 404
GET /CmsEditor/admin_login.asp 404
GET /admin/webeditor/admin_login.asp 404
…
言归正传,扫描漏洞的人目的大多是想做黑链 SEO,给黑掉的站点加上隐藏链接,提高目标网站在搜索引擎中的排名。这背后已经形成产业链了,可惜这种手段收到的效果已经越来越差,Google 早就不给隐藏链接权重了,现在连百度都能检测出恶意外链,这样一来还有什么意义呢?
对于站长来讲,要避免扫描给站点带来的影响,最好是对有关 IP 进行屏蔽。在发现网站宕掉,手工用 iptables 封了几个 IP 后,网站立刻就恢复正常了。
iptables -I INPUT -s 124.115.0.199 -j DROP |
但是过了几天又有别的人来扫描,各种 IP 层出不穷,一个个地去封收效甚微。怎么办?
一、使用 Nginx 的 limit_conn 模块
Ngnix 服务器的 limit_conn 模块可以限制单个 IP 的并发连接数,刚了解到它的时候感觉这碉堡了,简直是应用层防火墙了。修改配置文件 nginx.conf
http { ... limit_zone ten $binary_remote_addr 10m; limit_conn ten 10; ... } |
这样限制使用 10M 内存来管理 session,同时限制每个 IP 可以同时发起最多 10 个请求。
二、使用 iptables 做连接数控制
iptables 也可以做到限制同一 IP 的瞬间连接数,如果使用 iptables 做并发控制,可以在网络层就把恶意数据包丢弃,理论上讲,效率比 Nginx (应用层)的方式更高一些。
iptables -I INPUT -p tcp --dport 80 -d SERVER_IP -m state --state NEW -m recent --name httpuser --set iptables -A INPUT -m recent --update --name httpuser --seconds 60 --hitcount 9 -j LOG --log-prefix 'HTTP attack: ' iptables -A INPUT -m recent --update --name httpuser --seconds 60 --hitcount 9 -j DROP |
SERVER_IP 为被攻击的服务器 IP。
- 第一行的意思是:-I,将本规则插入到 INPUT 链的最上头。即,所有目标端口是80、目标 IP 是我们机器的IP,的 TCP 连接,在连接建立时,我们就将其列入 httpuser 清单中。
- 第二行的意思是:-A,将本规则附在 INPUT 链的最尾端。只要是 60 秒内,同一个来源连续产生 9 个连接请求,我们就对此进行 Log 记录。记录行会以 HTTP attack 开头。 –update 表示规则匹配时会更新 httpuser 列表清单。
- 第三行的意思是:-A,将本规则附在 INPUT 链的最尾端。和第二行同样的比对条件,但是本次的动作则是将此连接直接丢弃。
所以,这三行规则表示,我们允许一个客户端 IP,每一分钟内可以向服务器发出 8 个连接请求。
三、使用 Nginx 过滤恶意请求
上面两个方法最大的缺陷在于对数值的拿捏十分困难:配置太松,起不到屏蔽扫描的作用;配置太严格,又可能把正常访问也拒绝了,特别是某些搜索引擎过来抓页面的时候。网络环境差异,这个值该设多少,是没有一个标准答案的。
于是,有外国友人写了一套 Nginx 规则,仅仅对恶意请求进行屏蔽。何为恶意呢?
server { [...] ## Block SQL injections set $block_sql_injections 0; if ($query_string ~ "union.*select.*\(") { set $block_sql_injections 1; } if ($query_string ~ "union.*all.*select.*") { set $block_sql_injections 1; } if ($query_string ~ "concat.*\(") { set $block_sql_injections 1; } if ($block_sql_injections = 1) { return 403; } ## Block file injections set $block_file_injections 0; if ($query_string ~ "[a-zA-Z0-9_]=http://") { set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { set $block_file_injections 1; } if ($block_file_injections = 1) { return 403; } ## Block common exploits set $block_common_exploits 0; if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { set $block_common_exploits 1; } if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { set $block_common_exploits 1; } if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { set $block_common_exploits 1; } if ($query_string ~ "proc/self/environ") { set $block_common_exploits 1; } if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { set $block_common_exploits 1; } if ($query_string ~ "base64_(en|de)code\(.*\)") { set $block_common_exploits 1; } if ($block_common_exploits = 1) { return 403; } ## Block spam set $block_spam 0; if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") { set $block_spam 1; } if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") { set $block_spam 1; } if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") { set $block_spam 1; } if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") { set $block_spam 1; } if ($block_spam = 1) { return 403; } ## Block user agents set $block_user_agents 0; # Don't disable wget if you need it to run cron jobs! #if ($http_user_agent ~ "Wget") { # set $block_user_agents 1; #} # Disable Akeeba Remote Control 2.5 and earlier if ($http_user_agent ~ "Indy Library") { set $block_user_agents 1; } # Common bandwidth hoggers and hacking tools. if ($http_user_agent ~ "libwww-perl") { set $block_user_agents 1; } if ($http_user_agent ~ "GetRight") { set $block_user_agents 1; } if ($http_user_agent ~ "GetWeb!") { set $block_user_agents 1; } if ($http_user_agent ~ "Go!Zilla") { set $block_user_agents 1; } if ($http_user_agent ~ "Download Demon") { set $block_user_agents 1; } if ($http_user_agent ~ "Go-Ahead-Got-It") { set $block_user_agents 1; } if ($http_user_agent ~ "TurnitinBot") { set $block_user_agents 1; } if ($http_user_agent ~ "GrabNet") { set $block_user_agents 1; } if ($block_user_agents = 1) { return 403; } [...] } |
我们来看上面这个配置文件,它将所有包含类似 union.*select.*\(
URL 参数的请求都拒绝,这样来阻止 SQL 注入攻击。又拒绝所有 URL 参数后面有 =http://
的请求,来防止文件包含漏洞。类似地,屏蔽掉所有有 <|%3C).*script.*(>|%3E
的请求,阻止跨站脚本。还有就是屏蔽一些预定义的 User Agent,拒绝恶意抓站。
回过头来看,这个配置其实不太符合我国国情,比如屏蔽的那些浏览器 UA,并不是扫描中文网站常见的 UA。另外,要在配置文件中定义和枚举所有的恶意行为,是很困难的一件事情。
四、使用脚本定时检测日志
所有对服务器的访问请求都会在 Nginx 日志中记录,这其中也包括那些造成出错的请求。我们能否通过分析日志来确认恶意的访问请求呢?受第二种方法的启发,我们可以监控服务器日志,将请求出错的访问者 IP 放入一个 List 特别观察,在一段时间内如果没有太多的出错,我们就将其从列表中移除,否则,错误太多达到警戒值就调用 iptable 将其禁封。
通过一个脚本就可以完成这项工作。这个脚本与方法二的区别在于,方法二仅仅记录并发连接,只要并发过高,就可以触发禁封,这不够科学。这里我们通过对日志的监控,禁封的只会是给我们造成出错和麻烦的 IP(例如频繁的 404 错误,明显是扫描),搜索引擎过来就不会有问题了。
脚本代码
#!/usr/bin/perl use strict; use warnings; ## 本脚本将会监控 Web 服务器的 log 记录,(例如 Apache 或者 Nginx) ## 并统计同一个 IP 所引发的 HTTP 错误数目。该数值达到用户配置的数量, ## 则使用防火墙对该 IP 进行屏蔽,拒绝其访问。 ## log 文件路径 my $log = "/var/log/nginx/access.log"; ## 一个 IP 触发了多少次错误,我们就将其屏蔽? my $errors_block = 10; ## 过期时间,超过多少秒没有再见到该 IP 则将其从观察列表中移除? my $expire_time = 7200; ## 将 IP 从观察列表中移除时,清理多少个错误日志行数? my $cleanup_time = 10; ## 调试模式 on=1 off=0 my $debug_mode = 1; ## 声明一些内部变量 my ( $ip, $errors, $time, $newtime, $newerrors ); my $trigger_count=1; my %abusive_ips = (); ## 打开日志文件。使用系统的 tail 命令,有效轮询 open(LOG,"tail --follow=$log |") || die "Failed!\n"; ## For Linux (Ubuntu) systems # open(LOG,"tail -f $log |") || die "Failed!\n"; ## For OpenBSD, FreeBSD or Linux systems while(<LOG>) { ## 定义错误代码。这里使用了正则表达式匹配,你可以自行添加一些, ## 例如无端访问 .vbs 后缀文件请求,列入屏蔽条件 if ($_ =~ m/( 401 | 402 | 403 | 404 | 405 | 406 | 407 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 444 | 500 | 501 | 502 | 503 | 504 | 505 )/) { ## 自定义: 白名单 IP。 不论这些 IP 做了什么,均不屏蔽。 ## Google 爬虫 IP 段 66.249/16 就是一个好例子。 ## 为了方便程序员开发测试,内部子网 192.168/16 也不屏蔽。 if ($_ !~ m/(^66\.249\.\d{1-3}\.d{1-3}|^192\.168\.\d{1-3}\.d{1-3})/) { ## 从日志行中解析出 IP $time = time(); $ip = (split ' ')[0]; ## 若 IP 之前从未出现过,我们需要初始化,以避免出现警告消息 $abusive_ips{ $ip }{ 'errors' } = 0 if not defined $abusive_ips{ $ip }{ 'errors' }; ## 给这个 IP 增加出错计数,更新时间戳 $abusive_ips{ $ip }{ 'errors' } = $abusive_ips{ $ip }->{ 'errors' } + 1; $abusive_ips{ $ip }{ 'time' } = $time; ## DEBUG: 输出详细信息 if ( $debug_mode == 1 ) { $newerrors = $abusive_ips{ $ip }->{ 'errors' }; $newtime = $abusive_ips{ $ip }->{ 'time' }; print "unix_time: $newtime, errors: $newerrors, ip: $ip, cleanup_time: $trigger_count\n"; } ## 如果该 IP 已经触发 $errors_block 出错数量,调用 system() 函数屏蔽之 if ($abusive_ips{ $ip }->{ 'errors' } >= $errors_block ) { ## DEBUG: 输出详细信息 if ( $debug_mode == 1 ) { print "ABUSIVE IP! unix_time: $newtime, errors: $newerrors, ip: $ip, cleanup_time: $trigger_count\n"; } ## 自定义: 这里是屏蔽 IP 的 system() 系统调用 ## 你可以对这个 IP 添加更多的执行操作。例如,我们使用 logger 打印记录到 /var/log/messages ## 注释掉的是 OpenBSD 系统 Pf 防火墙 system("logger '$ip blocked by calomel abuse detection'; iptables -I INPUT -s $ip -j DROP"); # system("logger '$ip blocked by calomel abuse detection'; pfctl -t BLOCKTEMP -T add $ip"); ## 当 IP 已经被屏蔽,它就没必要继续留在观察列表中了 delete($abusive_ips{ $ip }); } ## 为后面的清理函数增加触发计数 $trigger_count++; ## 清理函数:当触发计数达到 $cleanup_time 我们将所有已经过期的条目从 $abusive_ips 列表中删除 if ($trigger_count >= $cleanup_time) { my $time_current = time(); ## DEBUG: 输出详细信息 if ( $debug_mode == 1 ) { print " Clean up... pre-size of hash: " . keys( %abusive_ips ) . ".\n"; } ## 清理我们已经很久没再见到的 IP while (($ip, $time) = each(%abusive_ips)){ ## DEBUG: 输出详细信息 if ( $debug_mode == 1 ) { my $total_time = $time_current - $abusive_ips{ $ip }->{ 'time' }; print " ip: $ip, seconds_last_seen: $total_time, errors: $newerrors\n"; } ## 如果 IP 未出现的时间已经超过我们设定的过期时间,则将其从列表中移除 if ( ($time_current - $abusive_ips{ $ip }->{ 'time' } ) >= $expire_time) { delete($abusive_ips{ $ip }); } } ## DEBUG: 输出详细信息 if ( $debug_mode == 1 ) { print " Clean up.... post-size of hash: " . keys( %abusive_ips ) . ".\n"; } ## 重置清理触发计数 $trigger_count = 1; } } } } #### EOF #### |
保存以上内容到 web_server_abuse_detection.pl,增加可执行权限
chmod +x web_server_abuse_detection.pl |
变量解释:
my $log 是要监控的日志路径。日志文件格式是标准的 Apache “common” 或 “combined”。这个脚本可以处理 Apache, Nginx, Lighttpd 甚至 thttpd 的日志,它将会在日志中寻找第一个字符串,即远程连接过来的 IP 地址。例如,这个是 Google 爬虫访问我网站的一条日志 “66.249.72.6 www.lovelucy.info – …”
my $errors_block 是一个客户端所能触发的最大错误数量,超过此值 IP 将被屏蔽。 Web 服务器的错误代码 400-417(如果你用 Nginx 则还包括 444),以及 500-505 都是触发条件。我们默认设置 errors_block 为 10,即如果一个 IP 在 7200 秒 ($expire_time) 以内触发了 10 个错误 ($errors_block),它将被屏蔽。
my $expire_time 是一个 IP 从观察列表中移除的过期时间。默认我们设 7200 秒(2 小时)。请注意,一个 IP 必须在 7200 秒以内没有触发任何错误,我们才会将它从列表中移除。这意味着一个恶意用户用很慢的速率扫描,我们仍可能会将其屏蔽。例如,一个 IP 每一小时访问一次来检测漏洞,第一个小时它就被列入观察列表,在第二个小时出错计数被增加,同时“最后一次见到这个IP” 的时间戳也被更新。普通的入侵检测系统 (IDS) 可能会漏报这样的行为,但这个脚本会在一个较长的时间(2小时)持续跟踪监测一个 IP。在这个 IP 达到触发 10 次错误时,也就是10小时后,我们仍会将其屏蔽。
my $cleanup_time 是触发清理观察列表的出错行数量。清理工作是一个循环,很耗费 CPU 所以没必要每一个错误日志行都去执行。请保证你的清理计数值足够低,从而让旧 IP 能以合适的速率从列表中移除。但是太低又会耗费 CPU,一个恰当的值应该是你的服务器5分钟内所产生的错误日志行数。
my $debug_mode 调试模式,会打印出一些有用的信息。
自定义白名单:注释中已经说过,Google 机器人 IP段 66.249/16 就应该放到白名单里,因为任何别人的网站链接到我们的一个错误的 URL,都会导致搜索引擎抓取失败。开发人员的 IP 也应该放入白名单,因为程序测试也会经常产生失败错误。
自定义系统调用:检测到恶意 IP 后,我们通过系统调用屏蔽之。默认的调用包括 logger 打印消息到 /var/log/messages,并执行 iptables 屏蔽命令。我们也可以添加更多操作,例如触发一个 Nagios 监控警告,给运维人员发 Email,等等。
运行:
设置 my $debug_mode = 0;
脚本即会静默运行。要让它在后台运行,不占用终端,则在命令后加一个 & 符号
./web_server_abuse_detection.pl & |
总结
月光博客写过一篇《防止CC攻击的方法》,他说现在 CC 攻击的技术含量低,利用工具和一些 IP 代理,搞个几百个肉鸡,一个初、中级的电脑水平的用户就能够实施攻击。门槛还真低啊。
做人还是要低调一点。
参考链接:
Web Server Abuse Detection
iptables 限制同一 IP 连接数
Nginx: How To Block Exploits, SQL Injections, File Injections, Spam, User Agents, Etc.
这篇文章由lovelucy于2012-08-17 18:35发表在信息安全。你可以订阅RSS 2.0 也可以发表评论或引用到你的网站。除特殊说明外文章均为本人原创,并遵从署名-非商业性使用-相同方式共享创作协议,转载或使用请注明作者和来源,尊重知识分享。 |
Google Chrome 32.0.1700.76 Windows 7 大约10年前
请问”使用 iptables 做连接数控制”提供的那些代码具体放在那个文件具体那个地方?
同时请问一下写入以下代码是否正确:
iptables -I INPUT -p tcp –dport 80 -d 192.168.1.1 -m state –state NEW -m recent –name httpuser –set
iptables -A INPUT -m recent –update –name httpuser –seconds 60 –hitcount 21 -j LOG –log-prefix ‘HTTP attack: ‘
iptables -A INPUT -m recent –update –name httpuser –seconds 60 –hitcount 21 -j DROP
想每60秒只许发出20个请求,同时IP地址那里用一个内容的来表示,具本到时会换上服务器的真实IP,以上这些代码是否正确?
待博客的回复,谢谢。
Google Chrome 33.0.1750.154 Windows 7 大约10年前
iptables 不是代码,是直接在命令行运行的命令。
你上面的指令理论上是没问题的,只是我说过
“对数值的拿捏十分困难:配置太松,起不到屏蔽扫描的作用;配置太严格,又可能把正常访问也拒绝了,特别是某些搜索引擎过来抓页面的时候。网络环境差异,这个值该设多少,是没有一个标准答案的。”
Google Chrome 33.0.1750.154 Windows 7 大约10年前
后来我理解了,按教程设置了相应的iptables设置并且重启,但是事后用百度站长工具刷扫漏洞后马上出现主机内存占满,i/o数提高到5K左右,MYSQL停止运行,后来重启后才恢复正常。
对于这种情况有没有什么办法解决?针对于DOS、CC功击,有什么好的解决方案?
Google Chrome 33.0.1750.154 Windows 7 大约10年前
mod_security
fail2ban
试下这两个
Google Chrome 21.0.1180.79 Windows 7 大约12年前
楼主的免费VPS是哪家的呢? 😛 也想搞一个来做做实验,求分享 : )
Google Chrome 21.0.1180.79 Windows 7 大约12年前
Amazon EC2云计算,第一年免费。我选用的EC2日本节点,速度还行。
Google Chrome 21.0.1180.79 Windows 7 大约12年前
大谢楼主 😛