理解注入的本质

我总结为以下三个方面:

  1. 数据与代码的混淆。

  2. 闭合引号(单引号 '、双引号 "、括号 ))的艺术。

  3. 注释符的差异(--+, #, /*, ;%00 等在不同 DB 中的用法)。

数据与代码的混淆

这是所有注入类漏洞(SQLi, XSS, Command Injection)的根本原因

  • 理想情况
    • 代码 (Code):是程序员写好的逻辑,比如 SELECT * FROM users WHERE name = ...。这是用来执行的命令。
    • 数据 (Data):是用户输入的,比如 zhangsan。这只是一个文本字符串,不应该具备任何执行能力。
  • 漏洞产生
    • 在早期的或者不规范的代码中,程序员为了图省事,直接用字符串拼接的方式把用户输入(数据)强行拼到了 SQL 语句(代码)中。
    • 数据库(编译器/解释器)是“笨”的。它拿到拼接后的完整语句时,分不清哪部分原本是程序员写的,哪部分是用户填的。它只知道按照 SQL 语法去执行。

假设后端代码是:

1$sql = "SELECT * FROM users WHERE id = " + $user_input;
  • 正常用户输入 1
    • 数据库执行:SELECT * FROM users WHERE id = 1
    • 这里的 1 是数据。
  • 黑客输入 1; DROP TABLE users
    • 数据库执行:SELECT * FROM users WHERE id = 1; DROP TABLE users
    • 混淆发生了:黑客输入的 ; DROP TABLE users 本来应该被当作“查找 id 为这一长串字符的用户”,但数据库把它解析成了“结束上一条语句,并执行删除表的操作”。数据摇身一变,成为了代码

闭合引号的艺术

要想让你的“数据”变成“代码”,你必须先逃逸出程序员为你设定的“数据牢笼”。这个“牢笼”通常就是引号。

为什么要闭合? 在 SQL 语句中,字符串类型的数据必须用引号包裹。 后端代码通常长这样:

1SELECT * FROM users WHERE username = '$user_input';

注意 $user_input 两边有一对单引号。

场景模拟:

  1. 输入admin
    • 拼接到 SQL:... WHERE username = 'admin';
    • 状态:安全。admin 被单引号仅仅包裹,只是一个字符串。
  2. 输入(未闭合)admin OR 1=1
    • 拼接到 SQL:... WHERE username = 'admin OR 1=1';
    • 状态:失败。数据库在找一个名字叫 "admin OR 1=1" 的人。你的攻击语句被当作了名字的一部分。
  3. 输入(闭合引号)admin' OR 1=1 --
    • 拼接到 SQL:... WHERE username = 'admin' OR 1=1 --';
    • 原理
      • 你输入的第一个 ' 与程序员写的左边那个 ' 配对成功了!
      • 此时,字符串结束了。
      • 你后面输入的 OR 1=1 就“光着身子”跑到了引号外面。
      • 在引号外面的内容,就是代码!

复杂的闭合: 程序员可能会用各种方式包裹输入,你需要(Fuzzing)并闭合它:

  • 单引号id = '$id' -> 注入 '
  • 双引号id = "$id" -> 注入 "
  • 括号id = ($id) -> 注入 )
  • 单引号+括号id = ('$id') -> 注入 ')
  • 双引号+括号+括号id = (("$id")) -> 注入 "))

技巧:当输入 ' 报错,输入 ') 也报错,但输入 ")) 正常回显时,你就探测出了后端的 SQL 结构。

注释符的差异

当闭合了前面的引号,注入了自己的 Payload 之后,你的 Payload 后面通常还有程序员写的残留代码(比如原本用来闭合的右引号 ',或者 LIMIT 0,1 等)。

这些残留代码会破坏 SQL 语法,导致报错。注释符的作用就是把屁股擦干净,让数据库忽略掉 Payload 后面所有的东西。

不同的数据库,注释符是不同的,这在实战中非常关键:

MySQL

  1. # (井号)
    • SQL 语法:SELECT * FROM users WHERE id=1 # and...
    • HTTP 传输坑点:在 URL 中,# 是锚点符号(Anchor),浏览器不会把 # 发送给服务器。
    • 解决:必须 URL 编码为 %23
  2. -- (减号减号空格)
    • 注意:标准 SQL 语法规定,-- 后面必须跟一个空格(或者控制字符)才算注释。
    • HTTP 传输坑点:在 URL 中,尾部的空格容易被忽略或传输丢失。
    • 解决:这就是 --+ 的由来。在 URL 中,+ 号会被解码为空格。所以 --+ 到达数据库时就变成了 -- (减号减号空格),完美符合语法。
  3. /\* ... \*/ (内联注释)
    • 用于注释中间一段,或者在版本探测绕过 WAF 时使用。

MSSQL (SQL Server) & PostgreSQL

  • -- (双减号)
    • 不需要空格,直接生效。这是最标准的注释。
  • /\* ... \*/
    • 同样支持。

Oracle

  • -- (双减号)
    • 同上。

特殊情况:;%00 (空字节截断)

  • 原理:在早期的 PHP 版本或某些老旧数据库(如 Access)中,%00 (Null Byte) 代表字符串结束。
  • 用法id=1 and 1=1 ;%00
  • 现状:现代语言和数据库大多已修复此问题,但在某些特殊的 WAF 绕过或文件上传漏洞结合 SQL 注入时仍可能有用。

一个注入 Payload 剖析

假设我们想绕过登录框(万能密码)。 后端代码:SELECT * FROM users WHERE user='$u' AND pass='$p';

攻击 Payloadadmin' #

数据库视角解析

  1. 原始语句SELECT * FROM users WHERE user='admin' #' AND pass='...';
  2. 闭合admin' 中的 ' 闭合了 user=' 的左引号。
  3. 数据变代码: 因为闭合了,原本应该是用户名的位置,现在变成了 SQL 逻辑的一部分。但这里我们没有加额外的逻辑(如 OR 1=1),只是利用了 admin 用户名。
  4. 注释# 把后面所有的内容 AND pass='...'; 全部注释掉了(变灰了)。
  5. 最终执行SELECT * FROM users WHERE user='admin' (密码检查逻辑被删除了,直接以 admin 身份登录。)

这就是理解本质的力量。知道了它为什么生效,哪怕以后遇到了 WAF 把 # 过滤了,也能立刻反应过来:“哦,我可以用 --+ 或者 /* 来代替注释,或者我不注释了,我手动闭合后面的引号 ' OR '1'='1。”