网友提到,wordpress的某些php文件提交一些不太友好的请求,会让php崩掉。
用wp的应该都经常遇到了,我也是。。。
原因没什么好深究的,不止是wp,大概所有暴漏在外的web和非web服务应该都经常遇到这么个情况。
本文仅探讨web环境。
解决的思路无非就那么几种,要么苦修内功,自己把程序方面做扎实,对输入的内容进行严格过滤,这种被动挨揍的方式挺累,而且程序越大,越不可控。并且这方面还要反复协调php、nginx的接受参数范围,没多大意思的。
要么就是在网络层进行限制,比如拒绝所有国外IP,这种其实没多大用,因为扫漏洞的IP各地都有。
要么,就是像本文的思路这样,做一个主动点的防护网。
我自己的web环境是
Debian 11 / 12
nginx 1.22.x / 1.25.x
php 7.4 / 8.3
mariadb 版本忘了
首先,在Debian系统上,绝大多数服务程序是低权限运行的,而且数据库端口只有限定范围可以连接,所以这一块可以忽略,即便退一万步,数据库就算root密码丢出去也没多大事,毕竟我这库里没什么机密。
其他需要的软件,包括syslog-ng、iptables、ipset。
系统的处理流程有点复杂,做起来倒是很简单。
首先,我这里web的日志分为两部分,一部分发往后端的中央服务器进行统计、分析(这一块跟本文无关),另一部分发往本机,进行即时分析处理。
因为整套架构经过多年多次更新,有些与本文有关的关联设置我可能会忘掉,如果遗漏了什么就留言说一下我再补上。
nginx里面http部分增加的:
map $http_x_forwarded_for $clientRealIp { "" $remote_addr; ~^(?P<firstAddr>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+),?.*$ $firstAddr; } log_format badrequest '"$status","$clientRealIp"';
这是定义一个日志格式,仅包含http状态码和客户端IP,当然也可以包含更多东西,这个在后面补充说明。
然后在站点的server段增加一行:
access_log syslog:server=127.0.0.1:515,facility=local6,tag=tingtao,severity=info main;
上面两者配合,就可以让nginx把每一次请求的状态码和IP发给本机的日志服务。注意端口我用了非常规的515,这样可以和完整日志的514进行区分,因为后端服务器也有一些直接对外的服务,避免混乱。
然后来看日志部分,我用的是syslog-ng
这是我自用脚本的一部分,实际使用的比这个复杂的多,所以复制进来做一些更改,也许会有错漏,遇到问题再留言吧:
tee /etc/syslog-ng/conf.d/tingtao_org_slave.conf << EOF source s_network_slave { network( transport("udp") port(515) ); }; parser p_ngx_slave { csv-parser( columns("NGX.status","NGX.remote_addr") flags(escape-double-char,strip-whitespace) delimiters(",") quote-pairs('""[]') ); }; filter facility_6_slave { facility(local6); }; filter notmyip_slave { not in-list("/etc/syslog-ng/mysvrip.txt", value("NGX.remote_addr")); }; ########## 测试的 destination badip_slave { program("/badip.sh" template("\${NGX.status}|\${NGX.remote_addr}\n" ) ); }; log { source(s_network_slave); parser(p_ngx_slave); filter(facility_6_slave); filter(notmyip_slave); destination(badip_slave); }; EOF /etc/init.d/syslog-ng restart
这里面有几个过滤,一个notmyip是因为wp有些自动化任务是由服务器发起的,所以来自服务器的ip需要直通,同时我这里是前后端节点分离,所以服务器ip不是唯一的,需要单独跳过去。
下面看nginx里面站点的server配置,这里是相关的一部分:
geo $remote_addr $ipgeo { default 0; 服务器ip1 1; 服务器ip2 1; 服务器ip3 1; 服务器ip4 1; 127.0.0.1 1; 172.16.0.0/16 1; } geo $host $svripgeo { default 0; 服务器ip1 1; 服务器ip2 1; 服务器ip3 1; 服务器ip4 1; 127.0.0.1 1; 172.16.0.0/16 1; } #几个很明显的非法请求直接踢了 if ($server_protocol ~* "HTTP/1.0") { return 444; } if ($server_protocol = "") { return 444; } if ($http_user_agent = "") { return 444; } if ($host = "_") { return 444; } location ~ /\. { return 444; } if ($http_referer ~* "jokes.go2live.cn|simplesite.com" ) { return 404; } ####### 主机头为服务器IP的部分 set $badrequest ""; #主机头为服务器ip if ( $svripgeo = 1) { set $badrequest "fu"; } #客户端并不是服务器ip if ( $ipgeo = 0) { set $badrequest "${badrequest}ck"; } if ( $badrequest = fuck) { return 444; } ####### 主机头为服务器IP的部分结束 set $badrequest2 ""; #主机头在默认站点上 if ($host = "_") { set $badrequest2 "fu"; } if ($http_user_agent = "") { set $badrequest2 "${badrequest2}ck"; } if ( $badrequest2 = fuck) { return 444; } ######## 很明显的恶意请求地址,这个各位自行统计 location ~* ^/(-/|/1.php|/shell.php|wp-content/plugins/backup-backup/|wp-json/|wordpress/|dns-query) { return 444; } #禁止指定UA及UA为空的访问,这个各位自行统计 if ( $http_user_agent ~* "nvdorz|pdrlabs|Mozzila|xfa1|node-fetch|^$" ) { return 444; }
简单说明
这里面除了一个还在观察的返回404,其他全444,因为除了444其他的http状态实际上都会反馈内容给客户端,被人CC或者说DDOS的时候,其实很简单的操作就能把服务器带宽拖满,而用444则是直接在服务器端丢弃连接,不做任何后续处理,最大限度提升服务器方面的抗压能力。
两个geo来确定到底是不是服务器以及内网发来的请求,如果是,则是我日常测试什么的,跳过,否则就踢。这些做扫描工具的也真是花心思了,我观察到很多次主机头为127.0.0.1这种。
然后服务器上做了一个默认站点,主机头为"_",所有进这里的直接踢,因为自己的站点全都有明确的server段进行承接,而其他的诸如用ip的,用一些乱七八糟主机头的全都是恶意请求。
某些url是站点内不存在的,而一看路径就知道请求这个地址的一定是恶意客户端,也直接踢。
所有的请求都发日志给syslog-ng,然后syslog-ng会开启前面写明的/badip.sh来进行后续处理,下面是/badip.sh代码:
tee /badip.sh << EOF #!/bin/bash while read line ; do if [ -n "\$line" ]; then 判断状态码,然后分离IP出来,做后续处理 插数据库 提交给防火墙封锁IP else echo "空字符串" # >> /root/z/badsql.txt fi done EOF chmod +x /badip.sh
不知为何,syslog-ng经常会发一个空行给脚本,所以这个要判断下,走到这一步,插库和封IP也就是很简单的事了,根据自己情况写进去就可以了。
其实也可以在syslog-ng的判断里面加一个状态码的过滤,我以前也这样干过,这样会更简单而且shell接受的数据少,系统压力就会小,但是后续逻辑的处理能力会较低。
反正都很灵活了,根据实际情况去处理吧。
巧的是,syslog-ng的运行身份是root,所以不用考虑权限问题。
本文分几次写完,我也要赚钱养家,这才是主业……所以思路断断续续的,而且因为这玩意涉及到web、日志、防火墙、数据库、脚本等很多方面,我做这整套用了挺长时间的,所以可能会有些漏掉的部分,回头想到了再补充吧。
因为我自用的版本过于细致,所以抽取了一部分写成了这篇文章。
对是否是恶意请求的判断实际上可以发生在4个方面,nginx里面、nginx+lua、syslog-ng、shell,而这4个点都可以实现相同或者相似结果。前两个的话可以设置444,这样效果最好,但都已经能操作防火墙了,那其实这个判断放在哪里都差距不太大……但若遇到有针对性的全连接(俗称CC)攻击的时候,放在前两个会显著提升抗打击能力的。
我自己是把完整的日志记录以后,还单独记录了一份状态码大于399的,便于统计分析。周末百无聊赖的时候跟这些小崽子斗个法,也可以打发一部分时间。
本文只是一个思路,尽可能快的捕捉到恶意行为,然后丢给防火墙封了它,当然永远都会有新的恶意行为出现,根据情况再处理就好了。
完全按照本文写的去配置,应该是可以运行的,但shell部分我没写出来,所以封IP是做不到的,ua和url的部分我也删的差不多了,各位要根据自己情况去填充。
然后nginx需要找一下路径然后配置加载ipgeo什么的模块,这种事就自己搞吧。
其实聪明如你,再动一下脑子,会发现既然本文的做法行得通,那么做一下延伸的话,嗯。。。其实也可以实现ftp、数据库等大概所有服务的自动防御。动动脑子动动手,同样是挨揍,但你就是最靓的仔!
网上所有类似文章呢,告诉你的无非是怎样的行为返回403,以这个姿势挨揍不太疼,或者怎样的姿势挨揍的时候看起来不失风度,哈哈。
我的思路则不同,通过几个部分的衔接,让恶意请求的IP永远出不了招,这也就是文章开头所说的“主动一点”。虽然理论上不可能完全杜绝攻击行为,但可以大幅度提升攻击成本,这个还是可以做到的。
本文做法应该适用于所有vps、云机、独服,只是在原有web环境增加一个syslog-ng,而这个实际分配的内存不到15M,并且syslog-ng安装的时候会自动替换系统自带的rsyslog,所以实际上并没增加系统资源开销,我有一个512M内存的虚拟机都跑的顺溜。