SQL注入漏洞利用与盲注技术分析
SQL注入漏洞利用与盲注技术分析
SQL 注入(SQL Injection, SQLi) 是长期位于 OWASP Top 10 榜单的经典漏洞。 即使在 ORM(对象关系映射)框架高度普及的今天,只要有历史遗留代码,或者开发者图省事手写了拼接 SQL 的复杂报表查询,SQL 注入依然能让整个数据库在瞬间被Dump。
本文将跳出简单的 ' or 1=1 -- 基础利用思维,深入数据库解析引擎的底层,推演 Union 联合查询、报错注入,以及在毫无回显的极端环境下,如何利用“时间”作为信道进行时间盲注(Time-based Blind SQLi)。
1. 数据的越界:SQL 注入的底层逻辑
任何“注入”类漏洞(包括 SQL 注入、命令注入、XSS 注入)的底层逻辑只有一条: 程序未能严格区分“控制数据(代码/指令)”与“用户数据(载荷)”,导致用户数据被解析引擎当成了指令来执行。
1.1 漏洞代码示例
假设有一个根据用户 ID 查询详情的 PHP 后端逻辑:
1.2 解析引擎视角的越界执行
- 正常情况:用户输入
1。数据库执行SELECT ... WHERE id = 1。 - 越界发生:黑客输入
1 UNION SELECT password, 1 FROM admin。 数据库解析引擎收到的完整指令变成了:SELECT username, email FROM users WHERE id = 1 UNION SELECT password, 1 FROM admin
在这个瞬间,原本属于数据的 UNION SELECT 字符串,突破了语法的边界,被数据库引擎识别成了SQL 关键字。黑客成功劫持了后端的查询逻辑,将管理员的密码也一并查了出来!
2. 回显注入实战:Union 与报错注入
在实战中,能否直接看到数据,决定了我们采用什么战术。
2.1 Union 联合查询注入 (有数据回显)
如果后端会将 SQL 查询的结果直接展示在网页上(如商品列表),这是黑客最喜欢的场景。
利用步骤:
- 判断字段数:使用
ORDER BY。 输入id=1 ORDER BY 3正常,id=1 ORDER BY 4报错,说明前面的SELECT查询了 exactly 3 个字段。 - 寻找回显点:使用
UNION SELECT 1,2,3。 因为UNION要求前后两次查询的字段数必须一致,这也是上一步测字段数的原因。页面上显示了数字 2,说明第 2 个字段是回显点。 - 窃取数据:在回显点注入系统函数或查表语句。
id=-1 UNION SELECT 1, version(), database()(注意前面的id=-1是为了让前面的查询为空,从而只显示我们注入的数据)。
2.2 报错注入 (Error-based SQLi)
如果页面不展示查询的数据,但开启了详细的数据库错误提示(如 PHP 的 mysqli_error 开启),我们可以故意构造语法错误,让数据库把我们想要的数据通过“报错信息”吐出来。
💻 实战接触:利用 XPath 语法错误(MySQL 典型)
extractvalue函数用于解析 XML,如果第二个参数不是合法的 XPath 格式,它就会报错,并在报错信息中显示那个非法的路径。 我们用concat拼接了~(0x7e) 和我们要查的database()。 页面真实的报错输出:XPATH syntax error: '~my_db~'黑客完美地从报错信息中拿到了当前数据库名my_db!
3. 无回显场景:盲注 (Blind SQLi) 的数学推演
现代 Web 应用通常会关闭错误提示,并且查询结果只返回 True 或 False(比如登录成功或失败)。我们既看不到数据,也看不到报错,这叫盲注。
3.1 布尔盲注 (Boolean-based Blind)
页面只有两种状态:内容 A(对应 SQL 逻辑为真)和内容 B(对应 SQL 逻辑为假)。
黑客如何通过 True/False 获取数据?答案是“逐位推断(二分法)”。 假设黑客想知道管理员密码的第一个字母:
id=1 AND ascii(substring((SELECT password FROM admin LIMIT 1), 1, 1)) > 64如果页面返回 A(真),说明第一个字母的 ASCII 码大于 64。id=1 AND ascii(substring((...), 1, 1)) > 96如果页面返回 B(假),说明介于 64 到 96 之间。
通过写 Python 脚本自动化发起请求,最多只需发 7 次请求,就能精确锁定一个字符。
3.2 时间盲注:时间盲注 (Time-based Blind)
如果页面连 True 和 False 的状态都没有(比如一个留言提交接口,不管 SQL 怎么查,永远返回“提交成功”)。 此时,唯一的出路是利用**“时间”**作为信道。
利用核心:SLEEP() 或 WAITFOR DELAY 函数
黑客构造如下语句:
id=1 AND IF(ascii(substring((SELECT database()), 1, 1)) = 109, SLEEP(5), 0)
底层逻辑推演:
- 数据库引擎执行
IF条件判断。如果当前数据库名的第一个字母的 ASCII 码是 109(即字母m)。 - 条件为真,数据库引擎执行
SLEEP(5),整个 SQL 查询会被强行挂起 5 秒钟。 - 后端代码必须等 SQL 查完才能返回 HTTP 响应。
- 黑客的 Python 脚本在发送请求后开始计时:
- 如果 5 秒后才收到 HTTP 响应,说明猜对了!字母是
m。 - 如果瞬间(几毫秒)就收到了 HTTP 响应,说明猜错了,继续猜下一个字母。
- 如果 5 秒后才收到 HTTP 响应,说明猜对了!字母是
这就是时间盲注较为复杂、但同时极其缓慢且容易受网络波动影响的原因。
4. WAF 绕过艺术
在实战中,目标网站通常部署了 WAF(Web应用防火墙)。如果你直接输入 UNION SELECT,会被瞬间拦截并封 IP。
绕过 WAF 本质上是利用WAF的正则引擎与后端数据库解析引擎之间的差异(类似于 HTTP 走私)。
4.1 常见的 Bypass 技巧
- 空格替换:WAF 拦截了
UNION SELECT(中间有空格)。- 替换方案:
UNION/**/SELECT、UNION%0aSELECT、UNION(SELECT...)。
- 替换方案:
- 大小写与复写:WAF 的正则没写好。
- 替换方案:
uNiOn SeLeCt,或UNunionION(如果 WAF 只过滤一次union,过滤后剩下的字符刚好拼成新的union)。
- 替换方案:
- 等价函数替换:WAF 拦截了
substring()。- 替换方案:使用
mid()或substr()。
- 替换方案:使用
- 编码混淆:利用数据库的隐式解码。
- 替换方案:将字符串转为 Hex 编码
0x61646d696e代替'admin'。
- 替换方案:将字符串转为 Hex 编码
5. 蓝队防御:参数化查询 (Parameterized Queries)
面对 SQL 注入,传统的过滤单引号、使用 addslashes() 转义等方法,都存在被宽字节注入等高级手法绕过的风险。
最有效的防御手段是使用参数化查询(Prepared Statements)。
5.1 为什么参数化查询能根除 SQL 注入?
以 PHP PDO 为例:
底层防线解析: 当使用预编译时,交互过程被分成了两步:
- 预编译期:后端先将 SQL 的“骨架”(
SELECT username FROM users WHERE id = ?)发送给数据库。数据库引擎此时已经完成了对这段 SQL 语句的语法解析和编译,确定了这是一条查询语句。 - 执行期:后端再把用户输入(
$user_input)作为纯粹的数据参数发送给数据库。
此时,即使黑客输入了 1 UNION SELECT...,由于SQL 的语法树已经在第一步被固定死了,数据库引擎只会把黑客的输入当作一个长长的字符串常量去匹配 id,绝对不可能将其重新解析为 SQL 关键字!