白盒审计SQL注入,核心不是拿着扫描器跑一遍,而是从代码上下文里找出“参数从入口到SQL语句之间,哪一步的过滤被绕过了”。多数情况下,漏洞不在完全没有防护的地方,而在那些以为做了防护、实际上拼接逻辑有缺陷的位置。下面直接讲实操中真正有效的几个判断点和代码模式。

先定位拼接点,而不是先找参数

打开一个项目,不要从用户输入入口开始追,那样容易陷进业务逻辑里。直接全局搜索字符串拼接进入SQL语句的写法。常见特征是引号闭合、加号连接、String.format、MyBatis的${}、以及PHP里的双引号变量展开。搜索这些模式:

"select * from user where id=" + id
"select * from user where name='" + name + "'"
String.format("select * from user where id=%s", id)
select * from user where id=${id}
"select * from user where name='$name'"

把这些点全部列出来,再逐个判断参数是否来自外部可控输入。这个顺序能让你在十分钟内摸清一个中型项目的主要风险面。很多审计人员从入口追到SQL,追到一半就断了,因为中间经过的封装层太多。反向从SQL语句往回追,路径短得多。

判断参数是否真的可控

找到拼接点后,下一步是确认参数来源。直接来自request.getParameter、$_GET、$_POST、@RequestParam、@PathVariable的,可控性最高。但要注意,有些参数虽然来自内部方法,但方法入参最终追溯到Controller层时仍然可控。判断标准只有一个:攻击者能否通过HTTP请求影响这个变量的值。如果能,就进入下一步分析过滤逻辑。

过滤逻辑的常见绕过点

很多代码不是没有过滤,而是过滤函数用错了场景。以下几种情况在实际审计中出现频率极高:

第一,用addslashes或mysql_real_escape_string处理数字型参数。如果参数最终被拼进不带引号的SQL位置,比如where id=$id,那么addslashes加的斜杠对数字上下文完全无效。攻击者直接提交1 and 1=1就能注入,因为斜杠只对引号内的字符串有意义。

第二,用htmlspecialchars或htmlentities处理SQL参数。这是把XSS防护函数误用到SQL场景,对单引号、双引号的转义方式和SQL语义完全不同,基本等于没防护。

第三,先转义后截断。有些代码对参数做addslashes后再用substr截取固定长度。如果攻击者把单引号放在截断点之后,转义斜杠被截掉,单引号重新生效。这种漏洞在订单号、用户名等有长度限制的字段里很常见。

第四,字符集问题。GBK编码下,如果数据库连接设置了GBK,而过滤函数按单字节处理,攻击者提交%df%27这样的序列,%df和反斜杠组合成一个合法汉字,单引号逃逸出来。现在新项目用UTF-8多了,但老系统迁移时仍然大量存在。

预编译不是万能保险

很多人看到PreparedStatement就跳过,这是审计中的常见误判。预编译只对绑定变量的位置有效,如果SQL语句里存在无法参数化的部分,比如order by、group by、limit后面的数字、表名、列名,这些位置不能用问号占位,开发人员往往会用字符串拼接。审计时重点看这些位置的参数是否来自前端。

String sql = "select * from user order by " + orderBy;
PreparedStatement ps = conn.prepareStatement(sql);

这里的orderBy如果来自请求参数,预编译完全保护不了。类似的情况还有MyBatis中${}和#{}混用,${}部分就是拼接点。

框架特性带来的隐藏拼接

Hibernate的HQL、JPA的JPQL、MyBatis的动态SQL,都可能产生原生SQL拼接。HQL里如果使用字符串拼接构造查询,同样存在注入。特别是Hibernate的createQuery方法,如果传入的是拼接字符串,和JDBC拼接没有本质区别。MyBatis的like语句也经常出现拼接,比如:

select * from user where name like '%${name}%'

这种写法在搜索功能里非常普遍,审计时看到like就要条件反射检查拼接方式。

二次注入的识别

有些参数在写入数据库时做了转义,但从数据库读出来再次拼进SQL时没有处理。这类漏洞在审计时容易被忽略,因为第一眼看到的是参数已经转义过了。判断方法是:追踪数据从入库到出库的完整链路,如果出库后的数据进入了新的SQL拼接,且出库时没有再次过滤,就存在二次注入。典型场景是用户注册时用户名转义入库,后续在后台管理界面按用户名查询时直接拼接。

存储过程和函数内部的拼接

存储过程内部使用动态SQL时,审计难度更高,因为代码在数据库端。但白盒审计时可以查看存储过程定义。重点看存储过程内部是否使用了EXEC、sp_executesql、或者拼接字符串后执行。如果存储过程入参来自应用层,而应用层没有过滤,存储过程内部的拼接就是注入点。

框架全局过滤的盲区

有些项目在Filter或Interceptor里做了全局参数过滤,比如对所有request参数做replace处理。审计时不要看到全局过滤就认为安全了。要检查过滤规则是否覆盖了所有参数来源,包括header、cookie、multipart表单、JSON body、路径变量。很多全局过滤器只处理getParameter,而JSON body通过InputStream读取,完全绕过过滤。另外,全局过滤如果只是替换单引号为两个单引号,对数字型注入依然无效。

实际审计中的快速验证方法

确认一个拼接点存在注入,不一定需要完整构造payload。在代码层面,只要确认参数可控且拼接前没有有效过滤,就可以定性。但为了报告严谨,可以结合代码逻辑推断注入类型。数字型参数直接拼接,注入类型就是数字型;字符串参数拼接时如果只过滤了单引号但没有处理反斜杠,可能存在宽字节注入;如果使用了预编译但order by等位置拼接,就是预编译绕过。

修复建议要具体到代码

审计报告里写“使用参数化查询”是不够的,要给出针对具体代码的修改方式。数字型参数直接绑定变量;字符串参数使用PreparedStatement的setString;order by等无法参数化的位置,使用白名单映射,前端传的字段名映射到后端固定的列名列表;表名同理。对于MyBatis,把${}改成#{},如果确实需要动态表名,在Java代码里做白名单校验后再拼接。

容易被忽视的注入面

除了常规的查询参数,还有几个位置经常出现注入但审计时容易跳过:报表导出功能里的筛选条件、后台批量操作时的ID列表、API接口中的JSON字段、文件上传后的文件名写入数据库、日志记录功能中的用户可控数据。这些位置往往因为业务优先级低、开发人员安全意识弱,拼接方式更加随意。

白盒审计SQL注入的本质是理解数据流和编码上下文。同一个过滤函数在不同编码、不同SQL位置、不同数据库驱动下效果完全不同。审计时保持对拼接点的敏感度,对过滤逻辑的怀疑态度,对框架特性的深入了解,才能把真正的漏洞从大量误报中筛出来。