资讯动态

Union注入攻击原理与手工实战:从猜字段到数据提取的完整链路

发布时间:2026/10/20 3:02:39 来源:尧图企业网站定制
1. 从一次手工测试的卡壳说起Union注入到底难在哪很多刚接触Web安全的朋友在学完基础的SQL注入概念之后都会遇到一个很尴尬的阶段工具跑得飞起手工测却一脸茫然。尤其是碰到Union注入的时候明明知道它是最经典、最直观的注入类型之一但真到了实战环境里一个单引号丢进去页面要么报错要么毫无反应要么返回一堆看不懂的东西然后就不知道下一步该干嘛了。我自己刚开始做安全测试的时候也是这样。工具扫出来一个注入点报告里写着“Union-based SQL Injection”但我自己手工去复现的时候连字段数都猜不对更别提把数据从数据库里“捞”出来了。后来踩了足够多的坑才慢慢把Union注入的完整链路摸清楚——它其实是一套非常有逻辑的推理过程每一步都有明确的判断依据而不是靠运气去试。这篇文章就是想把Union注入攻击的原理和实操链路完整地拆开讲一遍。我会从数据库层面解释Union到底做了什么、为什么它能被用来注入、手工测试时怎么一步步确定字段数、怎么找显示位、怎么提取数据以及在真实环境中会遇到哪些让人头疼的意外情况。适合已经了解SQL注入基本概念、但想把手工程序化能力提上来的读者。如果你还在“工具跑完就完事”的阶段这篇内容应该能帮你把底层逻辑补上。2. Union注入的底层逻辑数据库帮我们做了一次“拼接查询”2.1 Union关键字在SQL里的本职工作要理解Union注入首先得回到SQL语言本身。Union是SQL中的一个集合操作符它的作用是把两个或多个SELECT语句的结果集合并成一个结果集返回。比如你有一张用户表想同时查出用户的名字和订单的编号可以用Union把两条查询拼在一起。这里有一个关键约束参与Union的每条SELECT语句返回的列数必须相同对应列的数据类型也必须兼容。这个约束不是随便定的因为数据库在执行Union的时候是把多个结果集按列对齐后纵向堆叠的。如果列数不一致数据库根本没法对齐就会直接报错。这个约束对攻击者来说既是障碍也是突破口。障碍在于你必须先搞清楚原始查询返回了几列才能构造出合法的Union语句。突破口在于一旦你确定了列数就可以在Union后面的SELECT里写入任意查询让数据库把你想查的数据和原始查询的结果一起返回给你。2.2 注入点是怎么和Union扯上关系的Union注入能成立的前提是应用程序把用户输入直接拼接到了SQL语句里并且这个SQL语句的查询结果会以某种形式展示在页面上。举个典型的例子一个商品详情页的URL可能是这样的http://example.com/product.php?id1后端代码可能是这样写的$id $_GET[id]; $sql SELECT name, price, description FROM products WHERE id $id; $result mysqli_query($conn, $sql);注意这里的$id是直接拼进去的没有做任何过滤。如果攻击者把id参数改成1 UNION SELECT ...那么最终执行的SQL就变成了SELECT name, price, description FROM products WHERE id 1 UNION SELECT ...数据库会先执行前半段查询再执行后半段查询然后把两个结果集合并返回。如果后半段查询是攻击者精心构造的那么页面上就会显示出攻击者想要的数据。这就是Union注入的核心利用Union的拼接能力把攻击者自己的查询“嫁接”到原始查询上借助页面的展示逻辑把数据带出来。2.3 为什么Union注入比其他注入类型更“友好”相比布尔盲注和时间盲注Union注入有一个巨大的优势它是回显注入。也就是说攻击者构造的查询结果会直接显示在页面上不需要通过页面真假状态或者响应时间来判断数据。这让Union注入的效率极高一次请求就能拿到一批数据而不像盲注那样一个字符一个字符地去猜。但这也意味着Union注入对条件的要求更高。它要求页面必须把查询结果展示出来而且展示的位置和格式要能被攻击者识别。如果页面只是把查询结果用于内部逻辑判断从来不显示给用户那Union注入就用不了只能退而求其次用盲注。在实际测试中我习惯先判断注入类型。如果页面有明显的回显位优先尝试Union注入如果页面没有任何数据展示再考虑盲注。这个判断顺序能省下大量时间。3. 手工Union注入的完整推理链路从猜字段到出数据3.1 第一步确认注入点是否存在在构造Union语句之前必须先确认这个参数确实存在注入。最常用的方法是丢一个单引号进去观察页面反应。如果页面报错说明单引号被带进了SQL语句破坏了原有的语法结构。如果页面正常但内容有变化也可能存在注入需要进一步判断。不过现在的应用很多都做了错误屏蔽单引号不一定能看到报错信息。这时候可以用经典的and 11和and 12对比法。如果and 11页面正常and 12页面异常或内容不同基本可以确定存在注入。还有一种情况是参数本身是数字型不需要引号闭合。比如id1 and 11和id1 and 12的对比就能判断。数字型和字符型的区别在于字符型需要用引号闭合前面的语句数字型不需要。3.2 第二步确定原始查询的字段数这是Union注入最关键的一步。因为Union要求前后列数一致所以你必须先知道原始查询返回了几列。常用的方法有两种ORDER BY和UNION SELECT。ORDER BY的方法是从小到大递增数字直到页面报错。比如id1 ORDER BY 1 id1 ORDER BY 2 id1 ORDER BY 3 ...当ORDER BY n报错时说明字段数小于n那么字段数就是n-1。这个方法的好处是不需要页面有回显位只要页面能正常/异常切换就行。UNION SELECT的方法是直接构造UNION SELECT 1,2,3...看哪个数字组合能让页面正常返回。如果列数不对数据库会报错。这个方法的好处是一旦列数正确你还能顺便看到哪些数字会显示在页面上。我个人的习惯是先用ORDER BY快速确定字段数再用UNION SELECT找显示位。两步走比一步到位更稳妥因为ORDER BY的报错信息通常更明确。3.3 第三步寻找显示位确定字段数之后下一步是找出哪些列的内容会显示在页面上。构造一个UNION SELECT把每个位置都填上不同的数字或字符串然后观察页面哪些位置出现了这些数字。比如字段数是3构造id1 UNION SELECT 1,2,3如果页面上显示了2和3说明第2列和第3列是显示位。这两个位置就是后续提取数据的“窗口”。这里有个细节原始查询的id1可能返回空结果导致Union后面的结果成为唯一结果集。如果原始查询有结果Union会把两组结果都返回页面上可能同时显示原始数据和注入数据。为了更清晰地看到注入结果通常会把原始查询的条件改成一个不存在的值比如id-1这样原始查询返回空Union的结果就独占页面了。3.4 第四步提取数据库信息找到显示位之后就可以在显示位对应的位置写入子查询把数据库信息带出来。常用的信息包括当前数据库名database()数据库版本version()当前用户user()所有数据库名SELECT schema_name FROM information_schema.schemata指定数据库的表名SELECT table_name FROM information_schema.tables WHERE table_schemaxxx指定表的列名SELECT column_name FROM information_schema.columns WHERE table_namexxx具体数据SELECT username, password FROM users这些子查询需要放在显示位对应的列位置。比如显示位是第2列和第3列那么构造id-1 UNION SELECT 1,database(),version()页面上第2列的位置就会显示数据库名第3列显示版本信息。3.5 第五步处理多行数据的拼接问题Union注入有一个限制每次只能返回一行数据。如果子查询返回多行数据库会报错。这时候需要用GROUP_CONCAT或者LIMIT来分批提取。GROUP_CONCAT可以把多行数据拼成一个字符串返回SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schematest这样一次就能拿到所有表名用逗号分隔。如果数据量太大GROUP_CONCAT可能会被截断这时候可以用LIMIT配合偏移量分批取。实测中我发现GROUP_CONCAT的默认长度限制在不同数据库版本里不一样。如果发现数据被截断了可以用SELECT group_concat_max_len查看当前限制必要时用SET SESSION group_concat_max_len100000临时调大。4. 真实环境里的意外情况为什么你的Union注入总是失败4.1 页面不回显Union注入的硬伤Union注入最怕的就是页面没有回显位。有些应用虽然存在注入但查询结果只用于内部逻辑判断从来不显示给用户。比如登录验证场景SQL查询的结果只用来判断用户是否存在页面上只显示“登录成功”或“登录失败”。这种情况下Union注入拿不到任何数据只能转盲注。还有一种情况是页面有回显但回显的内容被转义或过滤了。比如和被转义成HTML实体导致你注入的标签无法正常解析。这时候需要根据具体的过滤规则调整payload。4.2 魔术引号和转义单引号被吃了很多老旧的PHP应用开启了magic_quotes_gpc或者使用了addslashes之类的函数会把单引号转义成\。这时候你输入的单引号无法闭合前面的语句注入就失败了。应对方法取决于具体的转义逻辑。如果是数字型注入不需要引号闭合转义就不影响。如果是字符型注入可以尝试用\来转义转义符本身或者用十六进制编码绕过。比如0x61646d696e代表admin不需要引号。4.3 关键字过滤Union被拦了有些WAF或应用层过滤会直接拦截包含union、select、information_schema等关键字的请求。这时候需要用到绕过技巧大小写混写UnIoN SeLeCt内联注释union/**/select双写关键字ununionion selselectect编码绕过URL编码、十六进制编码替换空格用/**/、、%09等代替空格这些绕过方法不是万能的需要根据具体的过滤规则来调整。我通常会用Burp Suite的Intruder模块批量测试不同的绕过payload看哪个能通过。4.4 字段类型不匹配Union报类型错误Union要求对应列的数据类型兼容。如果原始查询的第2列是整数类型而你在Union的第2列写了一个字符串数据库可能会报类型转换错误。这时候可以用NULL来占位因为NULL可以匹配任何类型。比如id-1 UNION SELECT NULL,database(),NULL这样就不需要关心每一列的具体类型了。4.5 权限不足information_schema查不到有些数据库用户权限很低无法访问information_schema库。这时候查表名和列名就会失败。可以尝试查询其他系统表或者直接猜表名和列名。如果连database()都查不到那可能当前用户连基本的信息查询权限都没有Union注入的价值就大打折扣了。5. 从Union注入延伸出去防御视角下的关键控制点5.1 参数化查询从根上堵住拼接Union注入能成立的根本原因是SQL语句拼接。如果应用使用参数化查询Prepared Statement用户输入永远不会被当作SQL代码执行Union注入就无从谈起。参数化查询的原理是SQL语句的结构先被数据库编译好参数值随后传入数据库只把参数当作数据不当作代码。这样即使参数里包含UNION SELECT也只会被当作一个普通的字符串值。$stmt $conn-prepare(SELECT name, price FROM products WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute();这是最有效的防御手段没有之一。5.2 最小权限原则让注入拿不到敏感数据即使应用存在注入漏洞如果数据库用户权限被严格限制攻击者能拿到的数据也有限。比如Web应用只用一个只能查询特定几张表的用户无法访问information_schema也无法读取其他库的数据。这样即使Union注入成功攻击者也只能看到有限的信息。5.3 输入验证与白名单只允许合法的输入对于已知格式的参数比如数字型ID可以在应用层做严格的类型验证。如果id参数只允许数字那么任何包含字母或特殊字符的输入都会被拒绝Union注入的payload根本传不进去。白名单比黑名单更可靠。黑名单容易被绕过白名单只允许已知合法的字符集攻击面小得多。5.4 错误信息屏蔽不给攻击者反馈Union注入的很多步骤依赖报错信息来判断字段数、显示位等。如果应用关闭了详细错误显示只返回统一的错误页面攻击者就失去了重要的判断依据。虽然这不能阻止注入但能显著增加攻击的难度。5.5 WAF与日志监控增加攻击成本WAF可以拦截常见的注入payload尤其是那些特征明显的Union注入语句。虽然WAF不是万能的但能挡住大部分自动化工具和低水平的攻击者。配合日志监控可以及时发现异常请求并做出响应。6. 我踩过的那些坑Union注入实操中的经验教训6.1 字段数猜对了但显示位是假的有一次测试一个页面ORDER BY确定字段数是4UNION SELECT 1,2,3,4也正常返回页面上确实显示了2和3。但当我用database()替换2的时候页面却显示空白。后来才发现第2列在页面上的显示逻辑是“如果值等于2则显示否则隐藏”。这种“假显示位”在真实环境里并不少见需要多试几个不同的值来确认。6.2 GROUP_CONCAT被截断数据拿不全用GROUP_CONCAT查表名的时候发现只返回了前几个表名后面的都没了。一开始以为是注入失败后来查了group_concat_max_len才发现默认只有1024字节。改成更大的值之后数据就完整了。这个坑在数据量大的时候特别容易踩。6.3 原始查询有结果Union结果被挤到后面如果原始查询的id1有结果Union的结果会追加在后面。页面上可能只显示原始数据注入数据被挤到了第二页或者被截断了。这时候把id改成-1或者一个不存在的值让原始查询返回空Union的结果就能独占页面了。6.4 编码问题导致中文数据乱码提取中文数据的时候有时候会显示乱码。这通常是字符集不匹配导致的。可以尝试用HEX()函数把数据转成十六进制然后在本地解码。虽然多了一步但能保证数据完整性。6.5 时间盲注和Union注入的混合使用有些场景下Union注入只能拿到部分数据剩下的数据需要靠盲注来补。比如information_schema被禁用了只能靠猜表名。这时候可以把Union注入和布尔盲注结合起来先用Union确认表名长度再用盲注逐字符猜解。虽然效率低但总比拿不到数据强。7. 写在最后Union注入的价值在于理解“查询拼接”这件事Union注入本身并不复杂它的核心就是利用SQL的Union操作符把攻击者的查询拼接到原始查询上。但它的价值远不止于“能拿到数据”这个结果。通过手工复现Union注入的完整链路你会对SQL语句的结构、数据库的查询执行逻辑、应用层的数据展示方式有更深入的理解。这些理解在后续学习更复杂的注入类型比如堆叠注入、二次注入时会变成非常扎实的基础。我在实际测试中越来越觉得工具能帮你快速发现注入点但真正理解注入原理、能根据环境灵活调整payload的还是得靠手工。Union注入是最好的手工训练场之一因为它每一步都有明确的反馈你能清楚地知道自己在做什么、为什么这么做。如果你正在学Web安全建议找一个合法的靶场环境把Union注入的每一步都亲手走一遍。从猜字段到出数据完整地跑通一次比看十篇文章都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑