每一个可以努力的日子,都是一份厚礼。
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() {
## 定义错误代码。这里使用了正则表达式匹配,你可以自行添加一些,
## 例如无端访问 .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 也可以发表评论或引用到你的网站。除特殊说明外文章均为本人原创,并遵从署名-非商业性使用-相同方式共享创作协议,转载或使用请注明作者和来源,尊重知识分享。 |

大约3年前
港ICP备?这是香港的备案号吗 😐
大约12年前
请问”使用 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,以上这些代码是否正确?
待博客的回复,谢谢。
大约12年前
iptables 不是代码,是直接在命令行运行的命令。
你上面的指令理论上是没问题的,只是我说过
“对数值的拿捏十分困难:配置太松,起不到屏蔽扫描的作用;配置太严格,又可能把正常访问也拒绝了,特别是某些搜索引擎过来抓页面的时候。网络环境差异,这个值该设多少,是没有一个标准答案的。”
大约12年前
后来我理解了,按教程设置了相应的iptables设置并且重启,但是事后用百度站长工具刷扫漏洞后马上出现主机内存占满,i/o数提高到5K左右,MYSQL停止运行,后来重启后才恢复正常。
对于这种情况有没有什么办法解决?针对于DOS、CC功击,有什么好的解决方案?
大约12年前
mod_security
fail2ban
试下这两个
大约13年前
楼主的免费VPS是哪家的呢? 😛 也想搞一个来做做实验,求分享 : )
大约13年前
Amazon EC2云计算,第一年免费。我选用的EC2日本节点,速度还行。
大约13年前
大谢楼主 😛