根本原因是十六进制编码绕过关键词过滤,因其作为数据库原生字面量不经过字符串解析阶段,waf仅做请求体扫描而未还原语义导致失效。

简单的关键词过滤挡不住十六进制编码的 SQL 注入,根本原因不是“漏了某个关键字”,而是它完全跳出了字符串匹配的逻辑边界——0x61646D696E 不是 "admin",它甚至不是字符串,而是数据库原生支持的字面量类型。
MySQL 中 0x 前缀字面量不经过字符串解析阶段
当 MySQL 解析 WHERE name = 0x61646D696E 时,0x61646D696E 在语法分析早期就被识别为 BINARY 字面量,直接转成字节序列 b'admin',后续不再走引号包裹字符串的路径。这意味着:
- 所有基于“检测
'admin'”或“拦截select”的正则规则,对0x...完全失效 - WAF 若只做请求体关键词扫描,而没做数据库语义层还原,就等于在解析前一层拦了个寂寞
-
UNHEX('61646D696E')、CONV('61646D696E',16,10)等函数调用同理,它们属于函数调用节点,不是字符串字面量
检测必须分两层:先抓模式,再验语义
只靠正则扫 0x[0-9a-fA-F]{2,} 还远远不够。真实攻击中常见混淆手段包括大小写混用、中间插入空格(0x 61 64 6D)、换行、甚至 URL 编码后的 0x253735(即 %75 的 HEX)。所以检测需拆解为:
- 第一层(语法提取):用正则捕获所有可疑 HEX 模式,包括
0x[0-9a-fA-F\s\n\r\t]{2,}、0X[0-9a-fA-F\s\n\r\t]{2,}、UNHEX\([^)]+\)、CONV\([^)]+\) - 第二层(语义校验):对每个提取出的 HEX 内容做清洗(去空格/换行/制表符),再检查:
- 长度是否为偶数
- 是否只含
0-9a-fA-F - 能否通过
bytes.fromhex()成功解码(不抛ValueError) - 解码后首字节是否为控制字符(如
0x27单引号、0x3B分号、0x20空格)
URL 解码时机错位导致检测失效
很多防护逻辑默认“原始请求体 = 最终输入”,但实际链路中:id=0x61646D696E 经 Nginx 或 Spring WebMvc 处理后,可能被提前解码为字节流,后端收到的是二进制 b'\x61\x64\x6d\x69\x6e',而非字符串 "0x61646D696E"。此时:
- 日志里看到的是已解码内容,但 WAF 规则还在原始请求体上跑正则,自然漏掉
- 若业务代码用
String.getBytes(StandardCharsets.UTF_8)再拼 SQL,就可能把二进制误当作 UTF-8 字符串,产生乱码或截断,反而掩盖了攻击痕迹 - 正确做法是:在请求进入业务逻辑前,统一做一次“反向 HEX 还原”——即把所有疑似 HEX 字面量按数据库规则尝试还原,并记录还原结果供后续策略判断
真正难处理的不是 0x61646D696E 本身,而是它和 UNHEX、CHAR、大小写混淆、多字节空格混用在一起时,检测点会分散在协议层、Web 容器层、框架层、SQL 解析层多个位置——任何一层脱节,整个防线就形同虚设。











