Skip to content

对大多数数据库,SQL 注入的原理基本相似,因为每个数据库都遵循一个 SQL 语法标准。但也存在细微差异,包括语法、函数的不同。

  • 攻击者对数据库的注入,无非利用数据库获得更多数据或更大权限,利用方式可以归为以下几个大类:
    • 查询数据
    • 读写文件
    • 执行命令

1. MySQL

绝大多数关系型数据库管理系统(RDBMS)在解析 SQL 语法时,对保留字和函数名是不区分大小写的。表名(Table Name)和列名(Column Name)是否区分大小写,取决于数据库类型和操作系统。Linux 默认区分,Windows/macOS 默认不区分。

1.1 MySQL 中的注释

  • # :从 # 字符到行尾;
  • -- :从 -- 到行尾(必须有空格,注入时可用 + 替代空格);
  • /* */ :中间的字符。

  • /*!*/ 有特殊意义,/*!55555, ...*/ 意思是若 MySQL 版本号高于或等于 5.55.55,语句被执行。如果 !后面不加入版本号, MySQL 会直接执行 SQL 语句。

1.2 获取元数据

MySQL 5.0 及其以上版本提供 INFORMATION_SCHEMA, INFORMATION_SCHEMA 是信息数据库,它不存储用户的业务数据,而是存储关于服务器的所有信息:有哪些库、有哪些表、有哪些列、谁有权限等等。

1.2.1 查询用户数据库名称

select SCHEMA_NAME from INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1

语句解析:

从 INFORMATION_SCHEMA.SCHEMATA 表中查询出第一个数据库名称。

SCHEMATA 是 INFORMATION_SCHEMA 库中的一张表,这张表里记录了服务器上所有数据库(Schema) 的信息。

SCHEMA_NAME: 这是 SCHEMATA 表中的一个列名。这一列存放的就是具体的数据库名称(比如 mysql, information_schema, performance_schema, 或者网站自己的业务库 shop_db)。

所以 SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA 的意思就是:把所有数据库的名字都列出来。

LIMIT 0,1 切片查看:

  • 通常注入点(比如网页上的一个文章标题位置)一次只能显示一条数据。如果你一下子查询出 10 个数据库名字,网页可能会报错或者只显示第一个,导致数据混乱。

  • LIMIT 子句用来限制返回的数据量。语法是 LIMIT [偏移量], [数量] ;

  • LIMIT 0,1 表示跳过 0 行(从第一行开始),只取 1 行数据,及取出了 SCHEMA 表的第一行数据。

1.2.2 查询当前数据库表

select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=(select DATABASE()) limit 0,1

语句解析:

DATABASE(): 这是一个 MySQL 的内置函数。它的作用是返回当前网站连接的数据库名称。

where TABLE_SCHEMA=(select DATABASE()) :去查找那些 所属数据库 等于 当前数据库 的信息。

例:

以下是一张 INFORMATION_SCHEMA.TABLES 表:

TABLE_SCHEMA (所属库) TABLE_NAME (表名) TABLE_ROWS (行数)
information_schema SCHEMATA 50
mysql user 3
dvwa guestbook 15
dvwa users 5

语句执行步骤:

  1. (select DATABASE()):

    • 数据库先执行这部分,发现当前网站连接的是 dvwa 库。

    • 语句变成了:WHERE TABLE_SCHEMA = 'dvwa'。

  2. FROM INFORMATION_SCHEMA.TABLES:

    • 数据库打开上面的那张 Excel 表。
  3. WHERE TABLE_SCHEMA = ...:

    • 数据库把 information_schema 和 mysql 开头的行全部盖住。

    • 现在只剩下 dvwa 下面的两行:guestbook 和 users。

  4. SELECT TABLE_NAME:

    • 数据库只盯着 “TABLE_NAME” 那一列看。
  5. LIMIT 0,1:

    • 数据库取出筛选结果中的第一行。

    • 结果: guestbook

1.2.3 查询指定表的所有字段

select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='Student' LIMIT 0,1

INFORMATION_SCHEMA.COLUMNS 表给出了表中的列信息。

1.3 UNION 查询

UNION 操作符用于将两个或多个 SELECT 语句的结果集“纵向”合并成一个结果集。

  • MySQL 与 Oracle 不像 SQL Server 那样可以执行多语句。所以在利用查询时,通常配合 UNION 关键字。

比如有两张表:

  • 表 A(文章): 只有两列,分别是 标题 和 正文。

  • 表 B(用户密码表): 也有两列,分别是 用户名 和 密码。

UNION 的作用就是把 表 B 里的数据,强行粘贴到 表 A 的下面。

当网页原本只打算展示“文章”时,如果注入了 UNION,网页就会不知情地把“用户名和密码”当作“文章标题和正文”一起显示出来。

规则

列数必须相同

这是最关键的限制。

  • 如果前面的 SELECT 查了 3 列(例如 id, title, content)。

  • 后面的 SELECT 也必须查 3 列(例如 user, pass, email)。

  • 如果不一致,数据库会直接报错,注入失败。

注入前通常要先用 ORDER BY 或者 SELECT 1,2,3,4... 来测试,目的就是摸清前面到底有几列,以便凑出相同的列数。

数据类型必须兼容

  • 如果第一列是数字(ID),那么后面拼上去的第一列最好也是数字。

  • 如果第二列是文本(Title),后面拼上去的最好也是文本。

  • 注:MySQL 在这方面比较宽容,它会尝试自动转换类型,但在 SQL Server 或 Oracle 中这点非常严格。

基本语法

假设有两个表:

表 1: news (新闻表)

id title
1 欢迎光临
2 今日特价

表 2: users (用户表)

username password
admin 123456
guest guest888

执行 UNION 查询:

SELECT id, title FROM news
UNION
SELECT username, password FROM users;

结果集(合并后):

id title
1 欢迎光临
2 今日特价
admin 123456
guest guest888

注意:结果集的列名通常沿用第一个 SELECT 语句的列名(这里显示为 id 和 title)。

  • UNION: 默认会进行去重操作。如果两张表里有完全一样的数据行,它只保留一行。这需要数据库进行排序和比较,速度较慢。

  • UNION ALL: 不去重。不管有没有重复,全部拼接到一起。速度更快。

  • 一般 UNION 就够。

其他细节

如果网页代码写着“循环显示所有查询结果”,那么联合查询的内容就会直接显示在第一个查询结果的下方。

如果网页只显示第一条结果(如 id=1 的文章),攻击者通常会把前面的 id 改成一个不存在的值(比如 -1),强迫数据库显示后面的 UNION 结果: id = -1 UNION SELECT ...

1.4 ORDER BY

原理:利用索引排序

正常情况下,ORDER BY 后面跟的是列名(比如 ORDER BY username)。

但是,SQL 标准也允许我们使用数字(列的索引/位置)来进行排序:

  • ORDER BY 1:表示按照查询结果的第 1 列进行排序。

  • ORDER BY 2:表示按照查询结果的第 2 列进行排序。

  • ORDER BY N:如果原本的查询结果只有 \(N-1\) 列,而你试图让它按第 \(N\) 列排序,就会报错。

代码层面

假设 PHP + MySQL:

// 原始 SQL 语句
$sql = "SELECT id, title, content FROM news WHERE id = $id";

这里查了 3 个字段:id, title, content。

  1. 注入 ORDER BY 3 时:

    SQL :SELECT id, title, content FROM news WHERE id = 1 ORDER BY 3

    数据库:按第 3 列(content)排序。 -> 执行成功。

  2. 注入 ORDER BY 4 时:

    SQL :SELECT id, title, content FROM news WHERE id = 1 ORDER BY 4

    数据库:按第 4 列排序 -> 抛出异常,执行失败。

1.5 回显位利用与实战

1.5.1 回显位

后端的 SQL 语句虽然查出了 3 列数据(比如 id, title, content),但网页的前端设计可能只允许显示其中两项(比如只显示 title 和 content,而 id 是隐藏在代码里用于逻辑处理的)。

  • 如果查询的数据放在第 1 列(对应 id),它虽然被查出来了,但网页不显示它,叫“无回显”。

  • 如果数据放在第 2 列(对应 title),网页把它打印在了标题的位置,叫“有回显”,第 2 列就是回显位。

找回显位的目的: 了解把查询的数据(比如密码),放在 UNION SELECT 的第几个位置,才能让它显示出来。

1.5.2 如何利用 UNION SELECT 1, 2, 3 虚构列寻找

虚构列

  • SELECT username ...

    去 username 这一列查数据

  • SELECT 1 ...

    直接造数据,数字 1。

假设直接在数据库里执行:

SELECT 'hello', 123;

数据库会凭空生成一张“临时表”:

'hello' 123
hello 123

它创建了两个临时列,第一列全是字符串 "hello",第二列全是数字 123。

利用方法

错误:

直接输入:id=1 UNION SELECT 1, 2, 3

因为网页通常只设计显示一条数据。如果不修改 ID,数据库会返回两行数据:

  1. 原来的数据(ID=1 的那条)

  2. 假的填充数据(1, 2, 3)

程序通常只取第一行显示。所以看到的还是那篇原来的文章,1, 2, 3 看不见。

正确:

让第一行(原数据)消失,强迫网页显示第二行(伪造的数据)。

Payload: id=-1 UNION SELECT 1, 2, 3

原理:

  1. id=-1:数据库去查 ID 为 -1 的数据。若 ID 都是正数,则这条查不到任何数据(结果为空)。

  2. UNION:把我们的 1, 2, 3 拼在空结果后面。

  3. 结果:现在结果集里只有一行数据,就是 1, 2, 3。网页只能把这行数据显示出来 -> 寻找回显位。

1.5.3 实战演示与解读

假设你输入了 id=-1 UNION SELECT 1, 2, 3 后,网页变成了这样:

网页标题:2

网页正文:3

(原本显示ID的地方什么都没显示)

解读:

  • 数字 1 没有出现:说明第 1 列的数据不会显示在页面上(可能是隐藏 ID)。

  • 数字 2 出现在了标题位置:说明第 2 列是回显位。

  • 数字 3 出现在了正文位置:说明第 3 列也是回显位。

结论:

你可以在第 2 列或者第 3 列填入指令。

把数字换成 Payload

比如,想查当前数据库的名字和用户:

原始测试 Payload:

UNION SELECT 1, 2, 3

真正的攻击 Payload:

UNION SELECT 1, database(), user()

网页此时就会显示:

网页标题:dvwa (原本显示 2 的地方变成了数据库名)

网页正文:root@localhost (原本显示 3 的地方变成了用户名)

总结

  1. 确定列数: 用 ORDER BY。

  2. 让位: 把 ID 改成 -1 或其他不存在的值。

  3. 占位: 用 UNION SELECT 1, 2, 3... 填充。

  4. 观察: 哪个数字显示在屏幕上,哪个位置就是回显位。

  5. 替换: 把那个数字换成想查的函数(如 version(), database())或子查询。

1.6 MySQL 函数利用

数据库通常都内置许多系统函数。

1.6.1 load_file() 读取文件操作

该函数帮助用户快速读取文件。文件位置必须在服务器上,文件必须为全路径名称(绝对路径),用户必须持有 FILE 权限,文件容量必须小于 max_allowed_packet 字节(默认为 16 MB,最大为 1 GB)。

union select 1,load_file('/etc/passwd'),3,4,5,6

一些防注入语句不允许单引号出现,使用以下语句绕过:

union select 1,load_file(0x2f6574632f706173737764),3,4,5,6

0x2f6574632f706173737764 是 /etc/passwd 十六进制转换的结果(利用 Burp Suite Decoder,以下同理),或者使用

union select 1,load_file(char(47,101,116,99,47,112,97,115,115,119,100)),3,4,5,6

使用函数组合将文件转换为十六进制数据:

select hex(loadfile(char(...)))

1.6.2 into outfile 写文件操作

像磁盘写入文件,与 load_file() 一样,必须持有 FILE 权限,并且文件必须为全路径名称。

select '<?php phpinfo();?>' into outfile 'c:\wwwroot\1.php'
select char(...) into outfile 'c:\wwwroot\1.php'

1.6.3 连接字符串 concat() concat_ws()

concat()

select name from student where id=-1 union select concat(user(),',',database(),',',version());
  • concat() 函数中的逗号也可以用十六进制数表示(0x2c)。

concat_ws()

select name from student where id=-1 union select concat(0x2c,user(),database(),version());
  • MySQL 常用函数速查表

1.7 MySQL 显错式注入

存在注入点,但无数据回显,却有报错回显时,把想要的数据拼接到错误信息里。

显错注入通常利用 MySQL 中特定函数的逻辑漏洞或特性。当这些函数接收到一个格式错误的参数时,MySQL 会报错,并且在报错信息中复述刚才那个错误的参数。

  • 目前最流行、最稳定的是利用 XML 解析函数:updatexml() 和 extractvalue()。

1.7.1 updatexml()

  • 原意: 用来替换 XML 文档中的数据。

  • 语法: updatexml(XML_document, XPath_string, new_value)

  • 漏洞点: 第二个参数 XPath_string 需要符合 XPATH 格式(比如 /root/node)。如果给它一个不符合格式的特殊字符(比如 ~),它就会报 XPATH syntax error。

构造 Payload

利用 ~ 作为非法字符。在 MySQL 中,~ 的十六进制编码是 0x7e。

AND updatexml(1, concat(0x7e, (你的查询语句), 0x7e), 1)

假设想爆数据库名:

http://example.com/news.php?id=1 AND updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)

数据库的反应:

  1. 执行 SELECT database(),得到结果 dvwa。

  2. concat 将结果拼接成 ~dvwa~。

  3. updatexml 试图解析路径 ~dvwa~。

  4. 发现 ~ 是非法字符,解析失败。

  5. 抛出错误:

    XPATH syntax error: '~dvwa~'

1.7.2 extractvalue()

和 updatexml 相比参数少了一个,写起来更短一点。

  • 原意: 从 XML 文档中提取值。

  • 语法: extractvalue(XML_document, XPath_string)

  • 漏洞点: 同样是第二个参数不支持特殊字符。

构造 Payload

AND extractvalue(1, concat(0x7e, (你的查询语句), 0x7e))
http://example.com/news.php?id=1 AND extractvalue(1, concat(0x7e, (SELECT user()), 0x7e))

报错结果:

XPATH syntax error: '~root@localhost~'

1.7.3 限制:32 字符长度

MySQL 在报 XPATH 错误时,错误信息最长只显示 32 个字符。

如果查出的数据很长(比如一串加密的 Hash 或者 GROUP_CONCAT 后的长字符串),后面的部分会被截断。

解决方案:切片法 (substr)

如果数据太长,就分段查。利用 substr() 函数。

  • 查前 30 个字:

    ... concat(0x7e, substr((your_query), 1, 30), 0x7e) ...

  • 查第 31 到 60 个字:

    ... concat(0x7e, substr((your_query), 31, 30), 0x7e) ...

(注:为了给前后的 ~ 留位置,通常一次截取 30 个字符比较稳妥)

1.7.4 老版本:floor() 报错 (Double Query Injection)

在 MySQL 5.1.5 之前(没有 XML 函数时),或者 XML 函数被禁用时,这是唯一的选择。

它利用了 count()、group by 和 rand() 结合时的内部计数 bug。

公式(非常复杂,直接背下来):

AND (SELECT 1 FROM (SELECT count(*), concat((你的查询语句), 0x7e, floor(rand(0)*2)) x FROM information_schema.tables GROUP BY x) a)

特点:

  • 优点: 兼容性极好,几乎通杀所有版本。

  • 缺点: 语句极其冗长,容易被 WAF 拦截;生成的报错信息比较杂乱;偶尔需要刷新几次才能出结果(随机性)。

由于 updatexml 更简洁,现在 floor 报错用得相对少了,但在极端的过滤环境下依然是必杀技。

1.7.5 补充:XML 文档

简单来说,XML (可扩展标记语言) 就是一种专门用来“存数据”和“运送数据”的文本格式。

  • HTML : 作用是 “显示” 数据。比如把标题加粗、把图片放中间。

  • XML (后端的数据格式): 作用是 “传输” 数据。不负责字号多大、颜色多红,只负责 “这是什么数据” 。

树形结构

示例:

<website>
    <user id="1">
        <username>admin</username>
        <password>123456</password>
        <level>super_admin</level>
    </user>

    <user id="2">
        <username>guest</username>
        <password>guest888</password>
        <level>normal</level>
    </user>

</website>

关键特征:

  1. 自定义标签: 这里的 <username>、<password> 并不是规定好的,而是写代码的人自己定义的。这就叫“可扩展”(Extensible)。

  2. 严格的嵌套: 就像文件夹一样,<user> 在 <website> 里面,不能乱放。

  3. 唯一的根: 最外面必须只能有一个大标签包着所有内容(这里是 <website>)。

XML 与 SQL 注入

有时候业务系统会把一堆数据打包成 XML 格式存到数据库的一个字段里。

MySQL 为了读取这些数据,提供了 XPath 语法。

  • XPath 就像是文件系统的路径。

  • 如果要找上面的用户名,路径大概是:/website/user/username。

回顾 updatexml 注入原理:

当我们构造 Payload:

updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1)

  1. 正常情况: updatexml 的第二个参数期待一个合法的路径(比如 /website/user)。

  2. 攻击情况: 我们给了它一个 ~root@localhost~。

  3. 触发报错: ~ 在 XML 路径语法中是非法字符。MySQL 解析器一看:“这 ~ 是什么鬼?这不是合法的 XML 路径结构啊!”

  4. 结果: MySQL 报错说:“你的路径 ~root@localhost~ 写法有误”。这就把我们查到的数据直接卖出来了。

1.8 宽字节注入

宽字节注入 (Wide Byte Injection) 是一种专门用来绕过代码层防御(特别是转义函数)的技术,常见于 PHP + MySQL 中。

1.8.1 核心原理:字符集的不匹配

理解两个前提:

A. addslashes()

为了防止 SQL 注入,开发者通常会使用 PHP 的 addslashes() 函数或者开启 magic_quotes_gpc(旧版本)。

它的作用是:在特殊字符(单引号 '、双引号 "、反斜杠 \、NULL)前面强制加一个反斜杠 \ 进行转义。

  • 输入: 1' OR 1=1

  • 转换后变成: 1\' OR 1=1

  • 数据库看到: 这是一个普通的字符串,包含了一个单引号,而不是闭合符号。注入失败。

B. GBK 编码 (双字节)

  • 数据库连接使用了 GBK 双字节编码,或其他宽字节字符集,如 BIG5。

  • GBK 规则: 如果一个字节的值大于 128 (hex 0x80),它会被认为是一个汉字的首字节,把后面紧跟的一个字节与前面合起来当作一个汉字解析。

1.8.2 过程分析

利用 GBK 的这个特性,在单引号 ' 前面故意加一个奇怪的字符,比如 %df。

构造 Payload

攻击者输入:%df%27

  • %df:一个高位字节。

  • %27:单引号 '。

经过 PHP 过滤 (关键时刻)

PHP 的 addslashes() 识别到了单引号 %27,于是在它前面加了一个反斜杠 \ (也就是十六进制的 %5c)。

此时的数据流变成了三个字节:

%df + %5c (\) + %27 (')

进入 MySQL (GBK 环境)

MySQL 使用 GBK 编码来解析这串数据。

  1. 读到了 %df。这是一个大于 128 的值。

  2. %df%5c 在 GBK 编码中正好是一个合法的汉字 —— “縗” (运)。

  3. 单引号 %27 不再被转义!

1.9 MySQL 长字符截断

MySQL 长字符截断是指:当攻击者输入的字符串长度超过了数据库字段规定的最大长度(比如 VARCHAR(10))时,MySQL 在某些配置下不会报错,而是自动把多余的部分截断,只保留前面的部分存入数据库。

1.9.1 核心原理:非严格模式的“宽容”

这个漏洞之所以存在,核心原因在于 MySQL 的 SQL Mode 配置。

  • Strict Mode (严格模式): 如果你输入的数据太长,MySQL 会直接报错(Error),拒绝写入。这是安全的。

  • Non-Strict Mode (非严格模式): MySQL 只会报 Warning,然后把数据截断,强行塞进去。

1.9.2 攻击场景演示:管理员账号注册劫持

假设某网站的注册逻辑如下:

  1. 检查用户名是否存在: 代码查询 SELECT count(*) FROM users WHERE username = '$input'。

  2. 如果不存在,则注册: 执行 INSERT INTO users ...。

数据库结构:

  • username 字段类型为 VARCHAR(7) (最大长度 7 个字符)。

  • 已经存在管理员账号:admin。

攻击步骤:

  1. 攻击者输入 Payload:

    攻击者尝试注册一个用户名为:admin x

    (注意:admin 后面跟了 2 个空格,最后加个 x。总长度为 8)

  2. 第一步:代码层检查 (Bypass)

    后台执行 SQL:SELECT * FROM users WHERE username = 'admin x'

    • 数据库里只有 admin。

    • 因为 'admin' 不等于 'admin x'。

    • 结果: 用户名未被占用,允许注册。

  3. 第二步:写入数据库 (Truncation)

    后台执行 SQL:INSERT INTO users (username, password) VALUES ('admin x', '123456')

    • username 字段限制长度为 7。

    • 输入的数据 admin x 长度为 8。

    • MySQL(非严格模式)触发截断机制,切掉第 8 个字符 x。

    • 实际存入的数据: admin (注意:admin 后面带了两个空格)。

  4. 第三步:登录验证 (The Magic)

    现在数据库里有两条记录(假设没有唯一性约束,或者利用空格绕过):

    1. id=1, username='admin' (真管理员)

    2. id=99, username='admin ' (攻击者)

    当攻击者用 admin 登录时,MySQL 在进行字符串比较时(在默认校对规则下),通常会忽略末尾的空格。

    即:WHERE username = 'admin ' 等同于 WHERE username = 'admin'。

    后果:

    • 如果注册的账号被系统认为是“管理员”,你可能直接拥有了管理员权限。

    • 或者,如果你利用这个机制去“修改密码”,系统可能会查找到第一个匹配的账号(真管理员),从而把真管理员的密码给改了。

  5. 这利用了 MySQL “存的时候区分空格,查的时候忽略空格” 的模糊地带。

1.9.3 漏洞复现的前提条件

  1. MySQL 处于非严格模式

  2. 业务逻辑缺陷:

    注册时检查用户是否存在的逻辑,必须是在代码层通过 SQL 查询结果判断,而不是依赖数据库的 Unique 报错。

  3. 字段长度已知:

    攻击者需要知道(或猜到)username 字段的具体长度(比如 7),才能构造出刚好在第 8 位被截断的 Payload。

1.10 延时注入

延时注入是 SQL 注入中最无奈、最慢、但也是最通用的一种手段。当没有数据回显,没有错误回显时,(此时的注入也叫做盲注)只能以页面加载时间作为注入内容的响应结果。

  • 手动延时注入很痛苦,通常使用 Python 脚本或 SQLMap 等工具。

1.10.1 核心函数

A. SLEEP(N)

  • 作用: 让数据库暂停执行 N 秒。

  • 示例: SELECT SLEEP(5); -> 数据库会卡住 5 秒。

B. IF(condition, true, false)

  • 作用: 逻辑判断。

  • 语法: 如果 condition 为真,执行 true 部分;否则执行 false 部分。

  • 示例: IF(1=1, SLEEP(5), 0) -> 因为 1=1 是真,所以执行 SLEEP(5)。

C. BENCHMARK(count, expr) (备用方案)

  • 作用: 当管理员禁用了 SLEEP 函数时使用。它让数据库重复执行某条指令 N 次,消耗 CPU 资源来制造延时。

  • 示例: BENCHMARK(10000000, MD5(1)) -> 做一千万次 MD5 运算,以此来拖慢时间。

1.10.2 逐字猜解

  • 目标 URL: http://example.com/product.php?id=1

  • 已知条件: 页面没有任何回显,输入 and 1=2 页面也没有任何变化(无布尔反馈)。

  • 攻击目标: 确认当前数据库名的第一个字母是不是 d。

http://example.com/product.php?id=1' AND IF(ASCII(SUBSTR(DATABASE(),1,1))=100, SLEEP(5), 1)--+
  1. ASCII(SUBSTR(DATABASE(),1,1))=100 (Condition):

    • DATABASE(): 获取当前数据库名(假设是 dvwa)。

    • SUBSTR(..., 1, 1): 从第 1 个字符开始,截取 1 个字符。结果是 'd'。

    • ASCII(...): 把 'd' 转换成 ASCII 码。字符 'd' 的 ASCII 码刚好是 100。

    • 判断:100 = 100,结果为 真 (True)。

  2. SLEEP(5) (True 分支):

    • 因为前面的判断为真,数据库会执行这个函数,暂停 5 秒 响应。
  3. 1 (False 分支):

    • 如果判断为假(比如第一个字母不是 'd'),则返回数字 1,页面会秒开。
  4. 如果把条件改成 length(database())=?,可以做猜解数据库名长度等操作。

  5. 可以通过浏览器的刷新按钮或开发者工具(F12)的 Network 面板查看时间。

1.10.3 提高效率

  1. 二分法 (Binary Search):

    不要按顺序猜 (a, b, c...),而是猜范围。

    • ASCII > 100? -> 是 (Sleep)。

    • ASCII > 110? -> 否 (秒开)。

    • 那就在 100 到 110 之间猜。

  2. 自动化工具:

    使用 Python 脚本或 SQLMap。