阻止针对 NGINX 的恶意访问

为了对付爬虫以及各种攻击,起初我选择自己编写 Bash 脚本定时监控 Nginx 日志。
问题是对 Nginx 不管不顾可能导致恶意访问在被封禁之前已经得逞。
另外写正则并不是一个愉快的过程,我的目的是识别出恶意访问,然后阻止这些请求并封禁 IP。
经过一段时间的摸索,关键词就是 444 以及 Fail2ban

环境需求

背景知识

请求日志

首先启动人眼观察,从 Nginx 的 access_log 中摘取一次典型的恶意访问。

因为我已经完成了本文的工作,所以它的响应是 444,一般来说是 404。

1
118.42.xxx.xx - - [02/Feb/2019:08:23:40 +0800] "GET /phpMyAdmion/index.php HTTP/1.1" 444 0 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0"

每一行的格式都是这样的(没有自定义 log_format 的话)。

1
$remote_addr - - [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"

显然我们可以给 $remote_addr、$request、$http_referer、$http_user_agent 加上一层判定再做出设置好的响应,翻译过来就是根据远程 IP、请求、请求来源、用户代理做出相同(一网打尽)或不同(个性化)的处理。

我的方案是一网打尽,通过一系列的配置,最终表现为 access_log 中的恶意访问的响应为统一的 444,然后使用 Fail2ban 监控请求日志实现 IP 封禁。

错误日志

这里真正涉及的是 naxsilogs 也就是 Naxsi 日志,naxsilogs 写入至 Nginx 的 error_log。

我将 Naxsi 规则触发后的响应同样设置为 444,此时这一类请求并不会写入 Nginx 的 access_log 而只会写入 error_log,大概过程如下:

1
恶意访问触发 Naxsi ------> 444 响应 ------> 写入 error.log

参考上文,同样配合 Fail2ban 使用,只不过这一次监控的日志并不一样。

模块与指令

  • map - 涉及最多的模块,大多判定基于 map 指令。
  • return - 针对我们所定义的恶意访问返回设定的响应。
  • include - 用来分离并引用多个配置,方便维护。

Fail2ban

我对 Fail2ban 没什么深入体验但不妨碍使用,基本上能遇到的问题都可以搜索到答案。
之前对 Fail2ban 的印象停留在 SSH 防爆破,特别是我使用 sshguard 以后就更加没接触过它。
Fail2ban 可以监控各式各样的日志,依据你设定的规则控制 iptables 实现 IP 封禁。
安装过后更是有很多配置可以选用,涵盖 Apache、Nginx、HAProxy 等等。
就本文来说,新增两个 jail 以及 filter 就足够了。

Nginx 设置

主配置

nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...

http
{

# 恶意请求一律返回 444 状态码
# 后续配合 Fail2ban 实现 IP 封禁

# 用户代理规则,针对 User-Agent
include UA.rules;

# 国别规则,针对国家
include GeoIP2.rules;

# 额外规则,两个简单的 if 判断
include extra.rules;

# Naxsi 核心规则
include naxsi_core.rules;

# 默认主机/空主机
server
{
listen 80 default_server;
server_name _;
return 444;
}

# 引入站点配置
include vhost/*.conf;
}

用户代理规则

UA.rules

1
2
3
4
5
6
7
8
9
10
11
12
13
map $http_user_agent $UA_Policy {

# 默认设为 0,可以设为任意字符
default 0;

# 正则匹配(不区分大小写)User-Agent
# 这些仅仅是我个人判定的恶意 UA,不具有任何说服力
# 因为它们大多锲而不舍实在烦人,比如 masscan 就是个知名的扫描工具
~*(?i)(.Net|ipip|masscan|ZeMu|Morfeus|Netcraft|zgrab|Solstice|Python) 1;

# 匹配空白 UA
"" 1;
}

国别规则

GeoIP2.rules | GeoIP2 采用 ISO 3166-2 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 匹配国家,请调整为自己想要的配置或者干脆忽略
map $geoip2_data_country_code $Country_Policy {
default 0;
BY 1;
RU 1;
KR 1;
IR 1;
SG 1;
#CN 1;
}

# 此处往下段落修改自 https://github.com/leev/ngx_http_geoip2_module#example-usage
# 请注意你的文件路径,这是我的配置,这几段也可以直接写进 http 段落
geoip2 conf/GeoLite2-City.mmdb {
$geoip2_data_city_name default=Shanghai city names zh-CN;
}

geoip2 conf/GeoLite2-Country.mmdb {
auto_reload 5m;
$geoip2_metadata_country_build metadata build_epoch;
#$geoip2_data_country_code default=CN source=$variable_with_ip country iso_code;
$geoip2_data_country_code default=CN country iso_code;
$geoip2_data_country_name country names zh-CN;
}

fastcgi_param COUNTRY_CODE $geoip2_data_country_code;
fastcgi_param COUNTRY_NAME $geoip2_data_country_name;
fastcgi_param CITY_NAME $geoip2_data_city_name;

额外规则

extra.rules

1
2
3
4
5
6
7
8
9
10
# 非 GET HEAD POST 请求一律 444
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}

# 非许可 host 一律 444
# 一般来说你对外开放的站点都可以放进去
if ($host !~* ^(aaa.com|b.aaa.com|c.ddd.com)$) {
return 444;
}

Naxsi 规则

naxsi.rules | 不同于 Naxsi 核心规则 | 参见 官方说明

1
2
3
4
5
6
7
8
9
10
SecRulesEnabled;

# 此处对应下文站点配置中的 Naxsi 响应
DeniedUrl "/RequestDenied";

CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;

站点配置

vhost/*.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 无关配置不再赘述

server
{
# 80 转 443
listen 80;
return 301 https://$server_name$request_uri;
}

server
{
listen 443 ssl http2;

# 额外响应
include extra.rules;

# Naxsi 响应
location /RequestDenied {
return 444;
}

location / {
# Naxsi 的具体规则,定义了响应转向 /RequestDenied
include naxsi.rules;

# 国别响应
if ($Country_Policy = 1) {
return 444;

# 你也可以基于 $Country_Policy 来一个八国语言的问候
#add_header Content-Type "text/plain;charset=utf-8";
#return 200 "Your IP Has Been banned!";
}
}

# 用户代理响应
if ($UA_Policy = 1) {
return 444;
#return http://127.0.0.1;
}
}

Fail2ban 设置

警告

重要的警告放前面,因为我已经完成了各种测试,自我感觉方案成熟所以将封禁设为 72 小时。
如果你要测试(你肯定得测试),请务必将 bantime 设为短时长比如 1m(一分钟),否则你的测试 IP 可能一键去世。

监控 access_log

  • 新增 444 监狱
1
2
3
4
5
6
7
8
9
10
11
cat >> /etc/fail2ban/jail.d/nginx-444.conf << EOF
[nginx-444]
enabled = true
port = http,https
filter = nginx-444
logpath = /var/log/nginx/*access.log
maxretry = 1
findtime = 1m
bantime = 72h
action = iptables[name=nginx, port="0:65535", protocol=tcp]
EOF
  • 新增 444 匹配
1
2
3
4
5
6
7
cat >> /etc/fail2ban/filter.d/nginx-444.conf << EOF
[Definition]

failregex = ^<HOST> -.*444 0.*

ignoreregex =
EOF

监控 error_log

  • 新增 naxsi 监狱
1
2
3
4
5
6
7
8
cat >> /etc/fail2ban/jail.d/nginx-naxsi.conf << EOF
[nginx-naxsi]
enabled = true
port = http,https
filter = nginx-naxsi
logpath = /var/log/nginx/*error.log
maxretry = 6
EOF
  • 新增 naxsi 匹配
1
2
3
4
5
6
7
cat >> /etc/fail2ban/filter.d/nginx-naxsi.conf << EOF
[Definition]

failregex = NAXSI_FMT: ip=<HOST>.*&config=block

ignoreregex = NAXSI_FMT: ip=<HOST>.*&config=learning
EOF

重启/验证 Fail2ban

1
2
3
4
5
fail2ban-client --test
service fail2ban restart
fail2ban-client ping
fail2ban-client status
iptables -L | grep nginx

测试

假设你按部就班地布置好一切,可以通过 curl 触发封禁。
再次警告,请务必将 bantime 设置为短时长比如 1m。
另外,在确认测试结果良好后,你可能需要删除 Fail2ban 数据库并复原 bantime 设置最后再次重启 Fail2ban。

本地操作

1
2
3
4
5
6
7
8
9
10
11
# 正常访问你的网站,Nginx 正常响应
# 或者直接浏览器访问,都不会触发 444
curl https://你的网站

# 以空白 UA 访问,立刻触发 Nginx 用户代理响应
# Nginx 返回 444,Fail2ban 捕获到日志
# 此刻开始你的测试 IP 将会被封禁 1 分钟
curl -A "" https://你的网站

# 浏览器访问或者刷新你的网站,打不开就对了
# 一分钟过后 IP 解封

远程操作

  • 验证
1
2
3
$ tail -f /var/log/fail2ban.log
2019-02-10 00:59:13,689 fail2ban.actions [17110]: NOTICE [nginx-444] Ban 222.93.xxx.xx
2019-02-10 01:00:13,786 fail2ban.actions [17110]: NOTICE [nginx-444] Unban 222.93.xxx.xx
  • 恢复
1
2
3
service fail2ban stop
rm -rf /var/lib/fail2ban/fail2ban.sqlite3
service fail2ban start

小结

  • Nginx 本身支持 deny IP,我没有采用这种方式。
  • Fail2ban 单独使用也可以封禁恶意访问,它的本质就是读取日志然后进行防火墙操作。
  • iptables 也不是唯一选择,还支持 nftables、firewalld 等等。
  • 限流方面的配置暂时没有涉及,以后有机会再行补充。

参考资料