延时盲注的核心判断依据从来不是“有没有报错”,而是“响应时间是否出现可复现的偏差”。攻击者利用SLEEP()、BENCHMARK()、pg_sleep()或WAITFOR DELAY等函数,让数据库在条件成立时暂停执行,再通过HTTP响应时间差逐字符推断数据。防御要精准,就不能只盯着“请求里有没有sleep关键字”,因为真实攻击中函数名会被注释符、十六进制编码、大小写混写甚至数据库特有的等价表达式绕过。真正有效的防护需要从请求入口、执行层、响应层三个位置同时建立检测与阻断机制,并且让规则具备语义识别能力,而不是简单的字符串匹配。

先理解延时注入的三种时间信号特征

第一种是单次条件延时。攻击者提交的载荷通常形如 IF(ASCII(SUBSTR((SELECT password FROM users WHERE id=1),1,1))>100,SLEEP(5),0)。这条语句只有在条件成立时才会暂停5秒,响应时间会从正常业务的几十毫秒突然跃升到5秒以上。第二种是倍数延时,例如 SELECT SLEEP(ASCII(SUBSTR(database(),1,1))/10),让暂停时长与字符ASCII码成比例,攻击者通过精确计时反推字符值。第三种是重锤式延时,使用BENCHMARK(50000000,MD5('x'))这类CPU密集型操作制造时间差,它不依赖数据库的sleep函数,特征更隐蔽。

防御系统必须识别这三种时间信号,而不是只检测SLEEP。一个精准的防护策略应该把“时间”本身当作检测维度:任何单次查询响应时间超过业务基线阈值(例如正常请求P99响应时间的三倍以上),且该请求包含数据库查询语义特征,就应当进入二次验证或阻断流程。阈值不能拍脑袋定,需要从真实业务日志中统计每个接口的响应时间分布,动态生成基线。

入口层:参数语义检测与语法特征提取

请求到达应用服务器之前,WAF或网关层要做的不是简单匹配正则表达式,而是对参数进行“语义拆解”。将用户输入中的SQL关键字、函数名、运算符、注释符、编码片段分别提取出来,再判断它们是否构成“条件分支+延时函数”的组合模式。例如检测到IF、CASE WHEN、AND、OR与SLEEP、BENCHMARK、pg_sleep同时出现在同一参数中,无论它们之间插入多少注释、换行、十六进制编码,都应当判定为高危。

具体实现上,可以维护一个“延时函数等价词表”,包含MySQL的SLEEP、BENCHMARK,PostgreSQL的pg_sleep,SQL Server的WAITFOR DELAY,Oracle的DBMS_LOCK.SLEEP,SQLite的randomblob配合heavy运算等。同时对注释符、空白符、字符串连接符做归一化处理,把//、%09、%0a、%0d、+、||等统一替换为空格,再进行模式匹配。这一步能拦截大量依赖混淆绕过的攻击载荷。

还有一个常被忽视的入口特征:攻击者为了逐字符猜解,会反复提交结构相同、仅条件值不同的请求。例如同一个参数位置,连续出现SUBSTR(database(),1,1)、SUBSTR(database(),2,1)、SUBSTR(database(),3,1)这样的序列。防御系统应当对单IP或单会话在短时间内的参数结构做相似度聚类,如果发现同一参数位置的载荷结构相似度超过90%,且请求频率异常,即使单次载荷没有命中已知规则,也应当触发临时封禁或人机验证。

执行层:数据库侧的行为审计与资源限制

WAF层面的检测总有绕过可能,真正无法绕过的是数据库自己看到的执行行为。在数据库端开启慢查询日志或审计插件,对执行时间超过阈值的语句进行实时分析。MySQL可以使用performance_schema中的events_statements_history_long表,PostgreSQL可以使用pg_stat_statements扩展,SQL Server可以使用扩展事件。这些工具能记录语句的完整文本、执行时长、返回行数,防御系统通过订阅这些日志,可以识别出“执行时间长但返回行数极少”的异常语句——这正是延时盲注的典型特征。

更主动的做法是在数据库连接层设置语句执行超时。MySQL的max_execution_time(针对SELECT)、PostgreSQL的statement_timeout、SQL Server的QUERY_GOVERNOR_COST_LIMIT都能限制单条语句的最长执行时间。将超时值设置为业务正常查询最大耗时的1.5到2倍,可以大幅压缩攻击者的时间信号空间。例如正常业务查询最慢500毫秒,那么将超时设为800毫秒,攻击者想让数据库暂停5秒的企图就会直接触发超时中断,时间信号被截断,盲注无法进行。

但要注意,直接设置全局超时可能误伤偶发的复杂报表查询。更精细的做法是按数据库用户或应用模块区分超时策略。为Web应用使用的数据库账号设置严格超时,为后台分析任务使用独立账号并放宽限制。同时配合连接池的语句分类,对来自用户输入拼接的查询强制使用短超时,对预编译的固定语句使用正常超时。

响应层:时间序列分析与异常响应拦截

延时盲注最终要体现在HTTP响应时间上。应用服务器或反向代理层应当记录每个请求的完整耗时,并建立基于时间序列的异常检测模型。这个模型不需要复杂的机器学习,简单的统计学方法就能达到很好效果:对每个接口维护最近一小时响应时间的滑动窗口,计算中位数和四分位距,当新请求的响应时间超过上四分位数加3倍四分位距时,标记为时间异常。

关键是要把时间异常与请求内容关联起来。如果一个请求的时间异常,同时它的参数中出现了数据库对象名、系统函数名或条件表达式特征,那么这几乎可以确定是一次延时盲注尝试。此时应当立即阻断该请求,并将源IP、会话ID、完整载荷写入告警日志,供后续分析。

还有一种更隐蔽的时间信号利用方式:攻击者不直接使用延时函数,而是构造一个执行时间与数据相关的查询。例如 SELECT IF(条件, (SELECT count(*) FROM large_table), 0),通过返回大量行数来制造时间差。这类攻击在响应时间上同样会表现出异常,但载荷中没有明显的延时函数。防御这类攻击需要检测“条件分支+子查询”的组合,并且子查询的目标表行数巨大。数据库审计日志中如果发现同一语句模板反复执行且执行时间波动与输入参数强相关,就应当重点排查。

应用层:参数化查询与输入白名单的硬约束

所有检测手段都是补救措施,最根本的防护是让用户输入无法影响SQL语句结构。参数化查询(预编译语句)能做到这一点,但前提是所有SQL都走参数化,不能有任何拼接。实际项目中常见的问题是:排序字段、表名、列名这类无法参数化的位置,开发者往往直接拼接用户输入。攻击者恰恰会利用这些位置注入延时载荷。

对于这类无法参数化的输入,必须使用白名单校验。排序字段只允许出现固定的几个列名,表名只允许从预定义集合中选择,任何不在白名单内的输入直接拒绝。如果业务确实需要动态表名,应当使用映射关系,让用户输入的是编号或标识,由服务端映射到真实表名,而不是直接传入数据库。

另一个容易被忽视的入口是HTTP头。User-Agent、Referer、X-Forwarded-For等字段经常被应用记录到数据库,如果这些字段的写入操作存在拼接,同样会成为注入点。防御策略必须覆盖所有会进入SQL语句的输入源,包括GET参数、POST参数、Cookie、HTTP头、文件上传的文件名和内容片段。

实战中的检测规则示例

下面给出一组针对延时盲注的检测规则片段,适用于WAF或网关层的规则引擎。这些规则的核心思路是:先做载荷归一化,再匹配“条件表达式+时间函数”的组合模式,同时加入时间维度的辅助判断。

# 归一化处理:去除注释、空白、字符串连接符
normalize(input):
    input = replace_regex(input, r'/\*.*?\*/', ' ')
    input = replace_regex(input, r'--[^\n]*', ' ')
    input = replace_regex(input, r'#.*', ' ')
    input = replace_regex(input, r'[\s+]+', ' ')
    input = replace_regex(input, r'\|\|', ' ')
    return input

# 延时函数等价词表
time_funcs = [
    'sleep', 'benchmark', 'pg_sleep', 'waitfor', 'delay',
    'dbms_lock.sleep', 'randomblob', 'heavy', 'get_lock'
]

# 条件分支关键词
condition_keywords = ['if', 'case', 'when', 'and', 'or', 'substr', 'substring', 'ascii', 'ord', 'mid', 'left', 'right']

# 检测规则
def detect_time_blind(payload):
    normalized = normalize(payload.lower())
    has_time_func = any(func in normalized for func in time_funcs)
    has_condition = any(kw in normalized for kw in condition_keywords)
    if has_time_func and has_condition:
        return True
    # 检测倍数延时模式:sleep(ascii(...)/n) 或 benchmark(...,ascii(...))
    if re.search(r'(sleep|benchmark|pg_sleep)\s*\([^)]*(ascii|ord|substr|substring|mid|left|right)', normalized):
        return True
    # 检测重锤式延时:benchmark(大数字,md5(...))
    if re.search(r'benchmark\s*\(\s*\d{6,}', normalized):
        return True
    return False

这套规则的关键在于归一化处理,它把攻击者常用的混淆手段——注释插入、空白替换、字符串连接——全部消除,让后续的模式匹配能够看到载荷的真实语义。同时延时函数等价词表覆盖了主流数据库的不同实现,避免只检测SLEEP导致漏报。

响应时间基线的动态计算与联动阻断

单靠静态规则无法应对未知变种,时间维度必须纳入自动判定。在网关层为每个接口维护响应时间统计:记录最近200次请求的耗时,计算中位数M和绝对中位差MAD。当新请求耗时大于M+5*MAD时,触发时间异常标记。这个阈值比固定值更贴合业务实际,因为不同接口的正常耗时差异可能很大。

时间异常标记本身不足以判定攻击,因为网络抖动、后端偶发慢查询也会造成响应变慢。所以需要与载荷特征联动:如果时间异常请求的载荷同时命中“条件表达式”特征,或者该IP在短时间内出现了多个结构相似的载荷,则升级为攻击判定,执行阻断。阻断动作可以是临时封禁IP、强制人机验证、或者丢弃请求并返回统一错误页。推荐使用临时封禁加自动解封的策略,封禁时长从5分钟起步,重复触发则指数递增。

对于无法完全阻断的场景(例如业务连续性要求极高),可以采用“响应混淆”策略:当检测到疑似延时盲注时,在响应中加入随机延时,打乱攻击者的时间信号。具体做法是在应用层对可疑请求的响应前增加一个100到500毫秒的随机等待,这样攻击者无法区分真实延时和随机延时,盲注的可靠性被彻底破坏。这种方法对业务影响极小,但能有效提升攻击成本。

数据库审计与事后追踪

精准防护不能只靠实时阻断,还需要完整的事后追踪能力。数据库审计日志应当记录每条SQL的完整文本、执行时间、返回行数、发起用户、来源IP。对于延时盲注,审计日志中会出现大量执行时间异常但返回行数很少的SELECT语句,且这些语句的文本结构高度相似,只是条件值在逐位变化。通过分析这些日志,可以还原攻击者的完整猜解过程,确认被攻击的数据库对象,评估数据泄露范围。

建议将数据库审计日志与Web访问日志关联,使用统一的请求ID或会话ID贯穿整个链路。这样当发现某条SQL执行异常时,可以快速定位到对应的HTTP请求,看到完整的攻击载荷和来源信息。关联分析可以离线进行,但日志必须保留足够长的时间,至少覆盖一次完整攻击周期的分析需求。

延时盲注的防护难点在于时间信号本身是合法的——数据库执行慢查询是正常现象,攻击者只是利用了这一点。因此任何单一维度的检测都不够,必须把请求语义、执行行为、响应时间三者结合起来,用动态基线替代固定阈值,用参数化查询从源头消除注入可能。这套组合策略能在不显著影响业务性能的前提下,将延时盲注的成功率压到极低。