SQL注入联合查询实战:从union select 1,2,3到数据库拖库

发布时间:2026/8/15 11:45:22
SQL注入联合查询实战:从union select 1,2,3到数据库拖库 1. 项目概述从“万能密码”到联合查询的深度探索在Web安全测试或CTF竞赛中提到SQL注入很多人的第一反应可能就是那个经典的admin or 11。这确实是入门但如果你止步于此那就像只学会了用螺丝刀拧螺丝却不知道如何用它来拆解一台精密的发动机。今天我们要深入探讨的是SQL注入攻击中一个极其强大且核心的技术——Union联合查询特别是像union select 1,2,3这样的探测语句。这不仅仅是构造一个Payload那么简单它背后是一套完整的、用于“透视”数据库内部结构的侦察方法论。为什么union select 1,2,3如此重要因为它解决了一个根本性问题在盲注中我们是在黑暗中摸索而联合查询则是为我们打开了一扇窗让我们能直接看到查询结果从而高效地获取数据。无论是CTFshow的SQL注入挑战还是DVWA靶场的高阶关卡联合查询都是绕不开的必考知识点。它允许我们将恶意构造的查询结果附加到原始查询结果之后从而在页面上“回显”出我们想要的信息比如数据库名、表名、列名乃至最敏感的用户账号密码。简单来说这个过程分为三步判断注入点 - 探测字段数 - 利用联合查询获取数据。而union select 1,2,3...正是第二步和第三步的桥梁。这里的数字1,2,3是占位符它们的核心作用是帮助我们确定前端页面会展示原始查询结果中的哪几个列我们只有把关键信息“放置”在会被显示的列位置上才能成功看到回显。接下来我将以一个资深渗透测试员的视角带你完整走一遍从发现注入点到通过联合查询拖库的全过程并分享那些官方手册里不会写的实战技巧和避坑指南。2. 联合查询的核心原理与前置条件拆解在撸起袖子直接写Payload之前我们必须彻底理解Union查询是如何工作的以及它生效的前提条件。这能让你在遇到复杂环境时知道问题出在哪里而不是盲目地试错。2.1 Union操作符的运作机制UNION操作符用于合并两个或多个SELECT语句的结果集。关键规则在于列数必须相同每个SELECT语句必须拥有相同数量的列。列数据类型必须兼容对应列的数据类型不必完全相同但必须是数据库可以隐式转换的如数字和字符串在某些情况下可以兼容。结果集自动去重UNION默认会删除重复的行。如果需要保留所有行包括重复的则使用UNION ALL这在某些情况下能略微提升性能且不影响我们的注入利用。一个合法的例子SELECT name, email FROM users WHERE id1 UNION SELECT model, price FROM products WHERE stock 0;这个查询会返回两列数据第一列是用户名或产品型号第二列是邮箱或价格。在SQL注入的语境下我们攻击的原始查询可能是SELECT title, content FROM articles WHERE id$input我们的目标是构造一个输入$input使得最终执行的SQL语句变成SELECT title, content FROM articles WHERE id-1 UNION SELECT username, password FROM admin-- 这样原本查询文章的结果为空因为id-1通常不存在而UNION后面的查询结果就会完整地显示在页面上。2.2 成功实施Union注入的四大前提不是所有SQL注入点都能用Union注入。它需要满足以下几个条件这也是我们探测流程的设计依据注入点位于SELECT语句的查询部分Union只能拼接SELECT。如果注入点在INSERT、UPDATE、DELETE或WHERE子句的某些复杂部分可能需要先转为盲注或其他方式。页面有回显点应用程序必须将数据库查询结果的一部分直接显示在网页上例如文章标题、内容、用户名等。如果没有回显就需要用盲注技术。能够控制完整的查询语句我们需要用注释符如--,#将原始查询后半部分注释掉以确保我们拼接的UNION SELECT是语法正确的唯一查询。例如上面例子中的--注意后面有个空格注释掉了原查询的最后一个单引号。知道查询的列数这是union select 1,2,3出场的原因。我们必须精确地让两个SELECT的列数一致。注意在实际测试中经常会遇到页面报错“The used SELECT statements have a different number of columns”这直接指明了我们的列数猜测错误。这是一个重要的错误信息反馈。3. 实战五步法从零开始完成一次Union注入理论清晰后我们进入实战环节。我将以一个有回显的新闻查询页面为例假设其URL为view.php?id1。3.1 第一步确认注入点与注入类型首先我们需要验证这里是否存在SQL注入并确定其类型。基础探测id1正常显示文章。id1提交一个单引号。如果页面返回数据库错误如You have an error in your SQL syntax...则极可能存在字符型注入。如果页面显示异常空白、与id1不同但无报错也可能存在注入。id1 and 11如果页面正常而id1 and 12页面异常空白或内容消失则存在数字型注入。因为12为假导致整个WHERE条件为假查询不到数据。判断闭合方式对于字符型需要判断闭合单引号的方式。id1 --如果页面恢复正常说明是用单引号闭合。也可能是id1) --、id1)) --等。实操心得现代应用可能不直接显示数据库错误而是返回一个通用的错误页。此时观察页面内容的变化比等待报错更有效。and 11与and 12的对比是黄金法则。3.2 第二步使用Order By探测查询列数这是最关键的一步目的是搞清楚原始SELECT语句究竟查询了多少列。ORDER BY子句用于根据指定的列对结果集进行排序。它的参数可以是列名也可以是列的数字索引例如ORDER BY 1表示按第一列排序。当这个数字索引超过了实际的列数时数据库就会报错。探测过程从order by 1开始尝试页面应正常。逐步增加数字order by 2,order by 3,order by 4...假设当尝试order by 4时页面报错或显示异常而order by 3正常那么我们就可以断定原始查询的列数为3。Payload示例view.php?id1 order by 3 -- view.php?id1 order by 4 -- 此条应出错重要技巧可以使用二分法快速定位。例如先试order by 10如果出错试order by 5再根据结果调整能大大减少测试次数。3.3 第三步确定数据回显点Union Select 1,2,3知道列数假设为3后我们构造一个“占位查询”来替换原有数据并观察数字1,2,3在页面上的哪个位置被显示出来。这些显示位置就是我们可以用来输出敏感数据的“回显点”。Payload构造view.php?id-1 union select 1,2,3 --这里有几个关键点id-1目的是让前一个SELECT查询不到任何结果id-1的记录通常不存在。这样页面显示的内容就完全来自于我们UNION后面的select 1,2,3结果更清晰。有时也用id1但后面加and 12来使前半部分查询为空。union select 1,2,3列数必须与order by探测的结果一致。这里的123既是占位符也是我们后续注入的“输出面板”。观察页面提交后页面上原本显示文章标题、作者、日期的地方可能会变成数字1、2或3。记下哪些数字出现了。例如标题位置变成了2内容位置变成了3。这意味着我们后续可以将数据库名、表名等替换到2或3的位置上。3.4 第四步获取数据库信息确定了回显点假设2和3位置可显示我们就可以开始提取信息了。数据库本身提供了一系列信息函数Information Functions。常用信息函数database()当前数据库名称。user()当前数据库用户。version()数据库版本信息。version_compile_os操作系统信息。Payload示例view.php?id-1 union select 1, database(), version() --执行后页面可能在相应位置显示如news_db和8.0.26这样我们就知道了数据库名和版本。3.5 第五步拖库获取表名、列名、数据这是最终目标。在MySQL中元数据关于数据库和表的信息存储在名为information_schema的默认数据库中。这是我们攻击的“地图”。核心表information_schema.tables存储所有表的信息。关键列table_schema数据库名table_name表名。information_schema.columns存储所有列的信息。关键列table_schema,table_name,column_name。拖库流程爆表名 假设当前数据库是news_db我们想查看里面有哪些表。view.php?id-1 union select 1,2,table_name from information_schema.tables where table_schemanews_db limit 0,1 --limit 0,1表示从第0行开始取1条结果。通过修改limit 1,1、limit 2,1... 可以遍历所有表。通常我们会寻找像admin、user、password这样的敏感表名。爆列名 假设我们找到了一个名为users的表接下来查看它有哪些列。view.php?id-1 union select 1,2,column_name from information_schema.columns where table_schemanews_db and table_nameusers limit 0,1 --同样用limit遍历寻找username、password、email等列名。爆数据 最后直接从目标表里读取数据。view.php?id-1 union select 1,username,password from users limit 0,1 --这样用户名和密码就会显示在页面的2和3号位。高级技巧一次性爆出所有表名或列名 使用group_concat()函数可以将多行结果合并成一个字符串用逗号分隔非常方便。view.php?id-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schemanews_db --执行后3号位可能会显示articles,users,config,admin...4. 进阶技巧与复杂场景应对真实的渗透测试环境远比靶场复杂。下面分享一些应对策略。4.1 处理列类型不兼容问题有时union select 1,2,3会报错提示数据类型不匹配。例如原查询第一列是字符串而我们用数字1占位导致错误。解决方法是用NULL或空字符串代替数字因为NULL可以匹配任何类型。view.php?id-1 union select null,null,null --或者用字符串和数字组合试探view.php?id-1 union select a,b,c --4.2 绕过WAFWeb应用防火墙的过滤很多WAF会拦截union、select、information_schema等关键词。大小写绕过UnIoN SeLeCt双写绕过ununionion selselectect假设WAF只删除一次union内联注释MySQL/*!union*/ /*!select*/ 1,2,3编码绕过URL编码、十六进制编码。例如将select转为%73%65%6c%65%63%74。等价替换用information_schema.tables的别名或访问其他可能包含元数据的视图但information_schema是最标准的。4.3 无显错位注入盲注结合Union有时union后的select结果不直接显示在页面上但可以通过条件判断其是否执行成功这本质上是一种基于布尔或时间的盲注但利用了union的语法结构。例如通过union select if(11, sleep(5), 1),2,3来判断条件真假。5. 常见问题排查与防御浅谈5.1 实战中遇到的典型问题Unknown column 3 in order clause这通常意味着你使用的order by 3超出了列数范围。从1开始逐步增加测试。The used SELECT statements have a different number of columns这是最直接的错误明确告诉你union前后列数不一致。重新用order by精确探测。页面空白但无报错可能原因有①union前半部分查询到了数据干扰了显示尝试用id-1②存在WAF拦截了请求但未返回错误③数据类型不兼容尝试用NULL。union后查询的结果没有显示回显点判断错误。可能页面只显示了多行结果中的第一行尝试用limit 0,1控制输出并观察页面源代码有时数据藏在HTML注释或JS变量里。5.2 从攻击者角度看防御作为一名安全从业者了解攻击是为了更好的防御。防止Union注入核心在于使用参数化查询预编译语句这是根本解决方案。让SQL指令和数据分离数据库不会将输入的内容当作代码执行。严格的输入验证对id这类参数确保其为整数。对于字符型定义允许的字符集白名单。最小权限原则数据库连接账户不应拥有information_schema的访问权限但这在共享数据库环境中较难实现。自定义错误信息避免将详细的数据库错误信息直接返回给用户。使用Web应用防火墙WAF作为辅助手段可以拦截一些常见的攻击Payload。最后想说的是union select 1,2,3这个简单的语句是打开SQL注入利用大门的一把关键钥匙。它背后的思想——探测结构、寻找回显、系统化获取信息——贯穿了整个手动SQL注入测试的流程。掌握它你才算真正脱离了“脚本小子”的范畴开始以系统化的思维去理解和挖掘漏洞。在靶场如DVWA, SQLi-Labs中反复练习这个流程直到形成肌肉记忆是提升Web安全实战能力最扎实的一步。