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 后端逻辑:

$id = $_GET['id'];
// 【漏洞点】直接将用户的字符串拼接到 SQL 指令中
$sql = "SELECT username, email FROM users WHERE id = " . $id;
$db->query($sql);

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 查询的结果直接展示在网页上(如商品列表),这是黑客最喜欢的场景。

利用步骤

  1. 判断字段数:使用 ORDER BY。 输入 id=1 ORDER BY 3 正常,id=1 ORDER BY 4 报错,说明前面的 SELECT 查询了 exactly 3 个字段。
  2. 寻找回显点:使用 UNION SELECT 1,2,3。 因为 UNION 要求前后两次查询的字段数必须一致,这也是上一步测字段数的原因。页面上显示了数字 2,说明第 2 个字段是回显点。
  3. 窃取数据:在回显点注入系统函数或查表语句。 id=-1 UNION SELECT 1, version(), database() (注意前面的 id=-1 是为了让前面的查询为空,从而只显示我们注入的数据)。

2.2 报错注入 (Error-based SQLi)

如果页面不展示查询的数据,但开启了详细的数据库错误提示(如 PHP 的 mysqli_error 开启),我们可以故意构造语法错误,让数据库把我们想要的数据通过“报错信息”吐出来。

💻 实战接触:利用 XPath 语法错误(MySQL 典型)

# 假设用户输入拼接到 id 后面
id=1 AND extractvalue(1, concat(0x7e, (SELECT database()), 0x7e))

extractvalue 函数用于解析 XML,如果第二个参数不是合法的 XPath 格式,它就会报错,并在报错信息中显示那个非法的路径。 我们用 concat 拼接了 ~ (0x7e) 和我们要查的 database()页面真实的报错输出: XPATH syntax error: '~my_db~' 黑客完美地从报错信息中拿到了当前数据库名 my_db


3. 无回显场景:盲注 (Blind SQLi) 的数学推演

现代 Web 应用通常会关闭错误提示,并且查询结果只返回 TrueFalse(比如登录成功或失败)。我们既看不到数据,也看不到报错,这叫盲注

3.1 布尔盲注 (Boolean-based Blind)

页面只有两种状态:内容 A(对应 SQL 逻辑为真)和内容 B(对应 SQL 逻辑为假)。

黑客如何通过 True/False 获取数据?答案是“逐位推断(二分法)”。 假设黑客想知道管理员密码的第一个字母:

  1. id=1 AND ascii(substring((SELECT password FROM admin LIMIT 1), 1, 1)) > 64 如果页面返回 A(真),说明第一个字母的 ASCII 码大于 64。
  2. 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)

底层逻辑推演

  1. 数据库引擎执行 IF 条件判断。如果当前数据库名的第一个字母的 ASCII 码是 109(即字母 m)。
  2. 条件为真,数据库引擎执行 SLEEP(5)整个 SQL 查询会被强行挂起 5 秒钟
  3. 后端代码必须等 SQL 查完才能返回 HTTP 响应。
  4. 黑客的 Python 脚本在发送请求后开始计时:
    • 如果 5 秒后才收到 HTTP 响应,说明猜对了!字母是 m
    • 如果瞬间(几毫秒)就收到了 HTTP 响应,说明猜错了,继续猜下一个字母。

这就是时间盲注较为复杂、但同时极其缓慢且容易受网络波动影响的原因。


4. WAF 绕过艺术

在实战中,目标网站通常部署了 WAF(Web应用防火墙)。如果你直接输入 UNION SELECT,会被瞬间拦截并封 IP。

绕过 WAF 本质上是利用WAF的正则引擎与后端数据库解析引擎之间的差异(类似于 HTTP 走私)。

4.1 常见的 Bypass 技巧

  1. 空格替换:WAF 拦截了 UNION SELECT(中间有空格)。
    • 替换方案:UNION/**/SELECTUNION%0aSELECTUNION(SELECT...)
  2. 大小写与复写:WAF 的正则没写好。
    • 替换方案:uNiOn SeLeCt,或 UNunionION(如果 WAF 只过滤一次 union,过滤后剩下的字符刚好拼成新的 union)。
  3. 等价函数替换:WAF 拦截了 substring()
    • 替换方案:使用 mid()substr()
  4. 编码混淆:利用数据库的隐式解码。
    • 替换方案:将字符串转为 Hex 编码 0x61646d696e 代替 'admin'

5. 蓝队防御:参数化查询 (Parameterized Queries)

面对 SQL 注入,传统的过滤单引号、使用 addslashes() 转义等方法,都存在被宽字节注入等高级手法绕过的风险。

最有效的防御手段是使用参数化查询(Prepared Statements)。

5.1 为什么参数化查询能根除 SQL 注入?

以 PHP PDO 为例:

$stmt = $pdo->prepare('SELECT username FROM users WHERE id = :id');
$stmt->execute(['id' => $user_input]);

底层防线解析: 当使用预编译时,交互过程被分成了两步:

  1. 预编译期:后端先将 SQL 的“骨架”(SELECT username FROM users WHERE id = ?)发送给数据库。数据库引擎此时已经完成了对这段 SQL 语句的语法解析和编译,确定了这是一条查询语句。
  2. 执行期:后端再把用户输入($user_input)作为纯粹的数据参数发送给数据库。

此时,即使黑客输入了 1 UNION SELECT...,由于SQL 的语法树已经在第一步被固定死了,数据库引擎只会把黑客的输入当作一个长长的字符串常量去匹配 id,绝对不可能将其重新解析为 SQL 关键字!