news 2026/10/20 3:02:35

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Union注入攻击原理与手工实战:从猜字段到数据提取的完整链路

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?id=1

后端代码可能是这样写的:

$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 1=1和and 1=2对比法。如果and 1=1页面正常,and 1=2页面异常或内容不同,基本可以确定存在注入。

还有一种情况是参数本身是数字型,不需要引号闭合。比如id=1 and 1=1和id=1 and 1=2的对比就能判断。数字型和字符型的区别在于,字符型需要用引号闭合前面的语句,数字型不需要。

3.2 第二步:确定原始查询的字段数

这是Union注入最关键的一步。因为Union要求前后列数一致,所以你必须先知道原始查询返回了几列。常用的方法有两种:ORDER BY和UNION SELECT。

ORDER BY的方法是从小到大递增数字,直到页面报错。比如:

id=1 ORDER BY 1 id=1 ORDER BY 2 id=1 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,构造:

id=1 UNION SELECT 1,2,3

如果页面上显示了2和3,说明第2列和第3列是显示位。这两个位置就是后续提取数据的“窗口”。

这里有个细节:原始查询的id=1可能返回空结果,导致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_schema='xxx'
  • 指定表的列名:SELECT column_name FROM information_schema.columns WHERE table_name='xxx'
  • 具体数据: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_schema='test'

这样一次就能拿到所有表名,用逗号分隔。如果数据量太大,GROUP_CONCAT可能会被截断,这时候可以用LIMIT配合偏移量分批取。

实测中我发现,GROUP_CONCAT的默认长度限制在不同数据库版本里不一样。如果发现数据被截断了,可以用SELECT @@group_concat_max_len查看当前限制,必要时用SET SESSION group_concat_max_len=100000临时调大。

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确定字段数是4,UNION 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结果被挤到后面

如果原始查询的id=1有结果,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注入的每一步都亲手走一遍。从猜字段到出数据,完整地跑通一次,比看十篇文章都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:02:32

PandaWiki搭建智能知识库:RAG与向量检索让API查询从15分钟提速到30秒

我们团队最近这几个月&#xff0c;内部系统里最明显的一个变化&#xff0c;就是API接口的查询效率彻底翻身了。以前看一眼接口文档、翻一次旧项目的调用代码&#xff0c;动不动就是十五分钟起步&#xff0c;现在同样的活儿&#xff0c;半分钟内就能拿到答案。把这件事从根上解决…

作者头像 李华
网站建设 2026/10/20 3:02:31

秒杀系统设计:Redis原子操作与消息队列削峰实战

1. 秒杀系统的本质与整体设计思路秒杀系统这个词听起来挺唬人&#xff0c;但拆开来看&#xff0c;它要解决的核心问题就一个&#xff1a;在极短时间内&#xff0c;把大量并发请求挡在核心业务逻辑之外&#xff0c;只让极少数有效请求真正落到数据库上。我见过不少团队一上来就堆…

作者头像 李华
网站建设 2026/10/20 3:02:08

Word粘贴到富文本编辑器格式保留:剪贴板HTML清洗与内联翻译实战

搞网页富文本编辑器的人&#xff0c;十有八九都被同一个问题折磨过&#xff1a;用户从Word里复制一段图文&#xff0c;粘贴到编辑框里&#xff0c;结果要么变成一堆天书乱码&#xff0c;要么丢了字体、丢了行距、表格碎成一地。明明Word里整整齐齐&#xff0c;一进网页就“原形…

作者头像 李华
网站建设 2026/10/20 3:01:40

SAP性能分析:用工作进程采样精准定位ABAP CPU时间瓶颈

各位做SAP性能分析的朋友&#xff0c;尤其是常年跟ABAP慢事务、CPU时间过高的报表打交道的同仁&#xff0c;应该都有过这种经历&#xff1a;用户甩过来一个事务代码&#xff0c;说“这个超级慢”&#xff0c;你去ST05开SQL跟踪、去SAT做运行时分析&#xff0c;折腾半天&#xf…

作者头像 李华
网站建设 2026/10/20 3:01:37

时间差分算法实战:从TD(0)到深度Q网络,原理、调参与避坑指南

1. 从“预测下一秒”说起&#xff1a;时间差分算法到底在解决什么问题如果你接触过强化学习&#xff0c;大概率听过“时间差分”这个词。它不像深度学习那样需要海量数据和昂贵算力&#xff0c;也不像监督学习那样要求每个状态都有标准答案。时间差分算法的核心魅力在于&#x…

作者头像 李华
网站建设 2026/10/20 3:01:28

PHP+Redis+MySQL库存对账脚本:数据一致性保障实践

库存对账这件事&#xff0c;听起来简单&#xff0c;真做起来全是细节。我前段时间刚给某个电商项目写过一个 PHP 对账脚本&#xff0c;每分钟跑一次&#xff0c;比对活跃商品在 MySQL&#xff08;DB&#xff09;和 Redis 里的库存是否一致。用到的技术不复杂&#xff0c;就是 P…

作者头像 李华