SQL注入攻击往往不是一次成型的,攻击者会先用扫描器向你的接口发送大量探测性请求,观察返回结果是否异常,再决定是否深入利用。这些探测请求只要到达了你的服务器,就一定会被Nginx记录下来。问题在于,默认的Nginx日志格式并不记录POST请求体,GET参数中的特殊字符也可能被忽略,导致大量攻击痕迹悄悄溜走。本文从日志配置、特征提取、自动化分析三个层面,讲解如何把SQL注入尝试完整地捕获下来。

一、调整日志格式,确保攻击参数被完整记录
默认的combined格式只记录URL路径和User-Agent,不包含请求参数的详细结构。要捕获SQL注入,第一步是自定义log_format,把查询字符串、请求体长度、引用页等信息都写进去。在http块中添加如下配置:
log_format injection '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'args=$args request_time=$request_time';
access_log /var/log/nginx/access.log injection;这里的关键是$args变量,它保存了完整的查询字符串原文。攻击者发送的id=1' or '1'='1这类payload会原封不动地出现在日志里。需要注意Nginx默认会对变量做转义处理,单引号会被记录成\x27,这不影响后续的正则匹配,反而让特征更统一。
如果需要捕获POST请求体,Nginx本身没有现成变量,需要借助Lua模块。通过access_by_lua_block读取ngx.req.read_body()后的内容,再用ngx.log输出到错误日志。对于大多数场景,只监控GET参数加上返回状态码的变化已经能覆盖八成以上的探测行为,因为自动化扫描工具为了效率,优先使用GET方式测试。
二、用特征规则从日志中筛出可疑请求
SQL注入的payload有很强的模式特征,即便经过URL编码,解码后也会呈现出固定的语法结构。常见的特征包括单引号配对、布尔表达式、注释符截断、时间延迟函数等。用grep配合扩展正则就能做第一轮筛查:
grep -E "union.+select|information_schema|sleep\(|benchmark\(|'(\s|\+)*(or|and)" /var/log/nginx/access.log
这条命令能匹配出联合查询注入、数据库元数据探测、时间盲注等典型痕迹。但要注意误报问题:正常业务中如果参数本身就包含or、and这类单词,会被误判。改进思路是匹配更长的组合特征,比如union\s*select必须连在一起,或者单引号后面紧跟逻辑运算符的结构。
更严谨的做法是写一个Python脚本,先对日志行做URL解码,再用一组加权规则打分。命中一条特征记一分,同一个IP在短时间内累计分数超过阈值才判定为攻击。这样能有效区分正常用户误触和扫描器的批量探测。脚本的核心逻辑大致如下:
import re
from urllib.parse import unquote
patterns = [
(r"union\s+(all\s+)?select", 5),
(r"information_schema", 5),
(r"sleep\s*\(\s*\d+", 4),
(r"('\s*(or|and)\s*')|('\s*=\s*')", 3),
(r"(--|#|/\*)\s*$", 1),
]
def score(line):
decoded = unquote(line)
total = 0
for pat, weight in patterns:
if re.search(pat, decoded, re.I):
total += weight
return total打分机制的好处是可以灵活调整灵敏度。内部系统的业务参数可能天然带特殊字符,把阈值调高一些;面向公网的接口则可以调低阈值,宁可错杀。另外建议把解码后的原始payload单独存档,方便后续交给安全团队复盘攻击手法。
三、联动fail2ban实现自动封禁
人工分析日志只能事后追溯,要形成实时防御,需要把检测规则交给fail2ban执行。它的工作原理是持续监控日志文件,一旦某个IP在指定时间窗口内触发的匹配次数超过上限,就调用iptables或firewalld将其封禁。先创建过滤规则文件/etc/fail2ban/filter.d/nginx-sqlinject.conf:
[Definition]
failregex = ^<HOST>.*"(GET|POST).*union.*select
^<HOST>.*"(GET|POST).*information_schema
^<HOST>.*"(GET|POST).*sleep\(
^<HOST>.*\x27.*(%20|\+| )*(or|and)(%20|\+| )*
ignoreregex =然后在jail配置中启用它,设置最大尝试次数为3次,封禁时长可以设得长一些,比如24小时。扫描器的探测请求动辄上百条,3次的门槛已经非常宽松,正常用户几乎不可能在几分钟内连续命中三条注入特征。
[nginx-sqlinject] enabled = true filter = nginx-sqlinject logpath = /var/log/nginx/access.log maxretry = 3 findtime = 600 bantime = 86400 action = iptables-multiport[name=sqlinject, port="http,https"]
部署之后建议先跑一段时间的观察模式,也就是只记录不封禁,确认规则没有误伤正常流量再切换到正式拦截。曾经有业务因为参数里合法包含and导致大量用户被误封,排查起来相当麻烦。
四、日志存储与运维层面的注意事项
完整的payload记录会让日志体积膨胀,攻击高峰期一天可能多出几个GB。务必配置logrotate做切割和压缩,保留周期根据合规要求设定,一般至少30天。同时要考虑日志本身的防篡改:攻击者拿到服务器权限后第一件事往往就是清日志,可以把日志实时转发到独立的日志服务器,用syslog或者filebeat推送到远端,本地删了也没用。
另外一点容易被忽视:日志分析发现注入尝试,不代表注入成功,但绝对值得跟进。如果某个IP的探测请求后面紧跟着返回状态码从404变成200、响应时间突然变长的情况,说明payload可能已经在应用层生效,这时候要立刻检查对应接口的参数校验逻辑,并对数据库做审计排查。日志捕获的意义不只是封IP,更是给上层应用提供了一个修复漏洞的线索来源。