SQL注入Union攻击实战指南:从原理到PortSwigger靶场通关技巧

发布时间:2026/10/9 6:03:32
SQL注入Union攻击实战指南:从原理到PortSwigger靶场通关技巧 搞Web安全的人应该都绕不开PortSwigger的Web Security Academy这套靶场把漏洞原理拆解得非常细尤其是SQL注入专题从基础到进阶分了十几关。最近我把SQL注入里的Union攻击实验室一条线全部刷完说实话虽然是老技术但真到实战里还是容易翻车列数不对、字段类型不匹配、注释符号失效、数据库方言差异……每个坑都能卡你半小时。这篇就把我在PortSwigger靶场上做Union注入的完整经验整理出来从原理到逐个实验拆解再到排查思路照着走一遍你的Union注入就算真正入门了。文章面向正在刷靶场、准备渗透测试面试或者想系统学习Web安全的同学不需要你有很强的代码基础但至少要知道SQL是干什么的。1. 先搞清楚Union注入到底在打什么1.1 为什么是Union而不是其他方式SQL注入的利用方式有很多种布尔盲注、时间盲注、报错注入、堆叠注入、Union注入等。Union注入属于直接注入里最高效的一种它利用的是SQL语法中UNION运算符的去重合并功能把攻击者控制的查询结果直接拼接到原查询结果中回显到页面。换句话说别的方式得盲猜、得靠时间延迟Union注入如果成立数据直接摆在眼前。PortSwigger的靶场把Union注入单独拎出来安排了好几个不同场景的实验室背后其实在传递一个核心思路Union注入是探测-匹配-提取三步走。探测是搞明白原查询有几个字段匹配是搞明白每个字段能不能显示字符串提取是把目标数据塞进回显字段里。用一句话记住Union注入的价值它是从数据库里拉数据最快的方式前提是原查询的结果会被页面渲染出来。这一点初学者很容易忽略Union攻击迫使我们要先判断原查询结果是否直接展示如果页面压根不显示原查询结果Union注入多半不适用得换布尔或时间盲注。1.2 触发Union攻击的四个硬性条件并不是所有SQL注入点都能用Union只有同时满足以下四个条件Union注入才能成立。我在靶场里反复验证过缺一条都不行。条件一原查询与Union查询的列数必须完全一致。数据库会要求两个SELECT语句返回相同数量的列多一列少一列都会直接报错。这是Union注入最经典的第一道关卡所以PortSwigger第一个相关实验就是教你怎么确定列数。条件二对应位置的数据类型必须兼容。虽然很多数据库允许隐式转换但最稳妥的做法是让每个列都用NULL占位然后逐列替换为字符串探测找出哪些列能渲染文本。这个逻辑在PortSwigger的实验二里体现得特别明显它专门让你判断哪个列包含字符串。条件三页面需有回显位置。Union注入本质是数据带外显示如果原查询结果不会在响应体里输出或者输出被剥离那你注入的查询结果也看不见。判断方法很简单用一个不存在的id去查如果页面依然显示0行或者不报错就说明可能有回显但被处理了如果页面依然渲染正常查询结果那就是标准回显。条件四注入点必须能控制原查询的闭合方式。你要保证注入的SQL语句和原语句拼接后语法正确尤其要注意闭合字符单引号、双引号和注释符。PortSwigger的实验里通常会在id参数传通过报错或行为变化判断闭合符。这四个条件理解透了后面做实验基本就是按部就班。我把它们浓缩成个口诀列数要一致类型要匹配回显要存在闭合要正确。2. PortSwigger靶场实操前的准备2.1 环境与工具配置我自己的环境是三件套Burp Suite Community版免费足够用、最新版Firefox浏览器、FoxyProxy插件来切换代理。PortSwigger靶场是网页应用不需要本地装数据库所有环境都在它的云平台上你只需要一个账号。配置Burp代理的步骤很简单打开Burp的Proxy选项卡默认监听地址是127.0.0.1:8080。在浏览器FoxyProxy里添加一个代理代理类型HTTPIP填127.0.0.1端口填8080。浏览器访问http://burp下载CA证书并安装到证书管理机构这一步不装HTTPS流量拦不到。随便打开一个靶场实验页面在Burp的HTTP history里能看到请求记录说明代理正常。我见过不少人在这一步卡住其实核心就一条Burp能不能抓到HTTPS请求。如果抓不到90%是证书没装好。另外PortSwigger靶场自带一个Access the lab按钮点进去会分配一个随机子域名做题时直接在user agent里带上Mozilla/5.0否则有些页面行为异常。2.2 如何快速判断一个请求是否存在可注入点进了靶场以后先别急着上Union第一步是找注入点。PortSwigger的实验通常会给一个商品分类筛选功能URL长这样https://xxxx.web-security-academy.net/filter?categoryGifts这种场景下category参数就是潜在注入点。判断是否存在SQL注入我一般按这个顺序来第一步加一个单引号看反应。把categoryGifts改成categoryGifts提交后观察页面变化。如果回显500错误、数据库报错信息或者页面结构突变说明单引号打破了原有SQL语句。PortSwigger很多实验在出题时会屏蔽详细报错但页面状态码和行为变化本身就够用了。第二步加一个恒真或恒假条件看回显区别。比如categoryGifts AND 11--和categoryGifts AND 12--。如果两次请求的页面商品数量不一样第一次正常显示、第二次显示为空说明这个参数拼接进了SQL的WHERE子句并且可以被注入控制。第三步测试Union的可行性。在确认注入点后直接尝试categoryGifts UNION SELECT NULL--如果返回列数不匹配之类的错误说明Union被执行了只是列数问题如果返回正常页面说明系统处理了注入但没报错需要进一步探测。这一步就是整篇文章的重头戏具体怎么做看下一章。3. 核心实验逐级拆解从列数探测到数据提取3.1 实验一确定SELECT列数ORDER BY 与 UNION SELECT NULLPortSwigger的第一个Union实验目标就是确定原查询返回多少列。它有两个常见做法ORDER BY递增法和UNION SELECT NULL递增法。ORDER BY法在注入点后面拼接ORDER BY 1、ORDER BY 2……直到报错为止。因为SQL语句中排序列号超出实际列数时数据库会报错ORDER BY position out of range不同数据库报错不同但大多会异常。实际Payload是Gifts ORDER BY 1-- Gifts ORDER BY 2-- Gifts ORDER BY 3--当ORDER BY 3报错时说明原查询只有2列。但这个方法有个局限当页面不回显数据库错误、只回显通用错误页时你得对比正常页面的差异稍有干扰就难判断。而且ORDER BY被过滤时这条路直接堵死。UNION SELECT NULL法这是我更推荐的方法也是PortSwigger官方推荐方式。原理是数据库会强制要求两个SELECT的列数一致所以你可以逐步增加NULL的数量直到不再报错。Payload示例Gifts UNION SELECT NULL-- Gifts UNION SELECT NULL,NULL-- Gifts UNION SELECT NULL,NULL,NULL--当UNION SELECT NULL,NULL不报错时说明原查询是2列。为什么用NULL而不是1,2,3因为NULL可以和任意数据类型兼容不会因为类型不匹配而提前报错。反过来如果测试时用UNION SELECT 1,2在某些数据库里可能因为类型转换问题报错干扰你对列数的判断。这里有个判断技巧即使错误信息被隐藏你依然可以通过页面内容判断。比如我在做PortSwigger实验时UNION SELECT NULL返回500错误UNION SELECT NULL,NULL返回200且商品列表正常显示因为Union语句没有返回额外行原查询结果不受影响那就确定列数了。实操记录我在第一个实验里把categoryGifts UNION SELECT NULL,NULL,NULL--提交后页面顶部突然出现了一个额外的空白区域——这就是Union查询生成的额外行但因为没有东西可显示所以是空白。这个细节很重要它告诉你页面有回显位置且列数正确。下一步就是找哪一列能显示字符串。3.2 实验二查找各列可用的数据类型知道了列数假设是3接下来要确认哪些列可以显示字符串。因为Union查询的列类型需要和原查询兼容如果你在某一列放字符串但原查询那一列是数字数据库可能报错也有数据库会隐式转换但为了稳定我们逐列替换NULL为特征字符串。Payload模板是Gifts UNION SELECT qwerty,NULL,NULL-- Gifts UNION SELECT NULL,qwerty,NULL-- Gifts UNION SELECT NULL,NULL,qwerty--我习惯用一个容易识别的字符串比如abc123。提交每个Payload后观察页面里哪个位置出现了这个字符串。PortSwigger这个实验的做法是正常商品列表下面会多出来一个abc123字样的区域那个区域对应的列就是可回显字符串的列。需要注意不是所有列都适合显示字符串。比如原查询第一列可能是商品ID数字第二列是商品名称字符串第三列是价格数字。当你把qwerty放在第二列时页面正常显示而放在第一列时可能报错或者显示为空。所以必须逐个试不要想当然。在这个实验里我还踩过一个坑用UNION SELECT a,b,c试图一次测三列结果页面报错我还以为列数不对。仔细一看发现第一列是整数类型字符串放进去就报错。所以宁可一个一个试也别图省事。等到确定哪一列是字符串兼容后攻击面就打开了——你可以在那个位置注入任意SELECT子查询。3.3 实验三跨表提取数据从users表拿用户名密码这是Union注入最经典的高潮环节。PortSwigger的实验三通常会给你一个提示系统里有一张users表里面包含username和password字段。目标是通过Union注入把这两列数据提取出来然后用管理员账号登录。假设我们已经知道原查询有2列这是最常见的出题方式且两列都能显示字符串。那么Payload就是Gifts UNION SELECT username, password FROM users--提交后页面会把users表所有用户名和密码循环渲染出来。数据量多的话页面会变长你需要找到administrator那一行。PortSwigger的实验故意不给你账号列表就让你通过这招去dump。为什么要用username,password顺序因为原查询的第一列如果是商品名称字符串、第二列是商品描述字符串那我们就按顺序把用户名放在第一列、密码放第二列。如果放反了虽然通常也能显示但数据位置不对容易看混。更严谨的做法是先确认对应列的显示能力再把目标字段对应填进去。这个实验背后的SQL理解很关键UNION SELECT username, password FROM users相当于是把两个查询结果纵向拼接原查询返回的所有商品行加上users表所有行一起渲染。因为UNION默认会去重如果users表和商品数据恰好有重复内容可能出现行数减少的情况但实战中几乎不会遇到。如果想保留所有行可以用UNION ALL这也是一个技巧后面再说。做完这个实验你会对数据库里所有的表其实都可以通过Union跨表查这句话有直观的理解。只要你知道表名和字段名剩下的就是把它们放进Union查询里。3.4 实验四单列多数据提取group_concat或字符串拼接PortSwigger还有一个进阶实验原查询只有一列可回显或者说你需要从多个字段拿数据却只能塞进一个位置。此时就考验你对数据库函数的掌握了。以SQLite为例PortSwigger有些实验用SQLite有一个group_concat函数可以把多个值拼成一个字符串。比如你想从users表拿username和password但Union只能返回一列可以这样写Gifts UNION SELECT username || ~ || password FROM users--在SQLite中||是字符串连接符。当然如果一次只能回显一个字段你也可以把username和password拼起来显示。这种方法在MySQL里对应CONCAT()或GROUP_CONCAT()在Oracle里是||在PostgreSQL里也是||。如果是多行数据要合并SQLite的group_concat()更有用Gifts UNION SELECT group_concat(username || : || password) FROM users--这样所有用户名和密码会以admin:password1,user:password2的格式一次性显示。PortSwigger这个实验会刻意让你把某个关键表的数据都提取出来用group_concat能省很多时间。我在实测中发现很多初学者拿到一列回显后就懵了不知道该怎么把多个字段塞进去。其实核心思路就是利用数据库自身的字符串函数做拼接。你不需要编程语言来重组数据数据库自己就能搞定。4. 实战中你一定会遇到的坑与排查思路4.1 注释符号为什么有时候不生效Union注入的Payload末尾通常要加注释符目的是把原查询后面可能存在的尾巴比如LIMIT、ORDER BY、其他WHERE条件给注释掉。PortSwigger和前几年不同现在不少实验故意把场景设置成你需要注释掉后面的内容而注释符在不同数据库里有不同写法。SQL标准注释符是--但MySQL要求--后面必须跟一个空格或控制字符否则不生效。所以我习惯写成--或者--注意末尾一个空格。在URL里空格会编码成或%20。PortSwigger靶场里我常用--加百分号编码%20的方式这样最稳。另外一个坑是如果在HTTP请求体的POST参数里注入注释符直接放在末尾可能没问题但在GET的URL参数里你注释符后面的部分会被当作URL的一部分安全性测试代理会进行URL解码但如果你没编码有时会导致注释符失效。我的习惯是统一写成--加号在URL里代表空格或者在Burp里直接把空格写成%20。还有一个特殊情况如果原查询后面没有多余内容不加注释符可能也能成功。但实战中你不可能100%确定原SQL的结尾所以统一加注释符是最保险的。4.2 列数判断的几种手段与方法对比除了ORDER BY和UNION SELECT NULL还有两种判断列数的方法虽然PortSwigger实验里不常用但值得我们了解。报错信息判断法当你提交UNION SELECT NULL时如果数据库开启报错有时候会显示The used SELECT statements have a different number of columns这句话会直接告诉你当前查询的列数和目标列数的关系吗不一定但你可以通过不断调整NULL数量当不再报错时就是正确列数。DISTINCT关键字绕过法在Oracle等数据库里UNION和UNION ALL本身就有差异有时UNION去重会导致返回行数太少你以为没生效其实生效了。这种时候用UNION ALL SELECT NULL,NULL可能更好因为它保留所有重复行页面变化更明显。我用表格总结一下各方法的特点方法优点缺点适用场景ORDER BY递增不需要构造NULL序列简单直观遇到ORDER BY被过滤时失效判断报错依赖页面差异错误信息回显明确时UNION SELECT NULL递增通用性最强不依赖原查询列类型需要逐个尝试稍微繁琐所有场景推荐UNION ALL SELECT NULL递增避免UNION去重干扰行数变化更明显同样需要逐个尝试当Union结果显示行数异常时报错信息直接读出最快需要数据库开放报错测试环境、Debug模式4.3 数据库类型判断与语法差异同一个Union Payload在不同数据库上可能千差万别。PortSwigger靶场里涉及多种数据库PostgreSQL、Oracle、MySQL等你正在做某个实验时判断数据库类型往往决定了能不能成功。常见判断方式是利用数据库特有的语法或函数。比如Oracle的dual表是特有的你不能直接SELECT NULL不带from必须写作SELECT NULL FROM dual所以如果UNION SELECT NULL报错而UNION SELECT NULL FROM dual不报错那就是Oracle。MySQL允许不带FROM的SELECTPostgreSQL也允许SQLite也允许。字符串拼接也能判断Oracle和PostgreSQL用||MySQL默认也支持||除非开启PIPES_AS_CONCAT模式SQL Server用。如果SELECT a||b页面回显ab那多半是Oracle或PostgreSQL。还有注释符差异MySQL支持#和--Oracle和PostgreSQL支持--SQL Server支持--但有些也支持/*...*/。如果你提交UNION SELECT NULL#只有MySQL能解析。这个技巧在识别数据库时很有用。我的建议是提前把主流的几个数据库的Union语法差异写成一个速查表平时做题时对照着用。这里放一个精简版数据库字符串连接注释符是否支持不带FROM的SELECTMySQLCONCAT()或PostgreSQL或CONCAT()Oracle或CONCAT()SQL Server或CONCAT()--、/* */支持需看版本SQLite4.4 编码问题与绕过URL编码、空字节等在PortSwigger靶场里你提交的Payload会经过URL传递或POST表单传递特殊字符如果不编码可能被服务器解析错误。比如单引号在URL路径里没问题但如果你在Burp里直接修改请求Burp默认会帮你编码一部分字符但有些场景需要手动处理。最常见的是空格被编码成%20或。在URL的查询字符串中会被解析为空格所以UNIONSELECTNULL这种写法是有效的。但如果你在Burp Repeater里手动输入最好用%20替代空格以免不同上下文解析差异。另一个经典坑是如果目标应用层面做了简单的WAF它会过滤union、select等关键字。PortSwigger靶场目前没有专门针对Union绕过WAF的实验但真实场景中你可能会遇到。绕过方法包括大小写变体UnIoN、注释符拆词UN/**/ION、URL编码%75nion、双重编码等。在我个人项目测试中以下几种方式在真实环境里成功过绕过方式示例原理大小写混写UnIoN SeLeCt简单规则匹配大小写不敏感内联注释UN/**/ION SE/**/LECT数据库解析会还原注释内容但WAF正则可能被绕过百分号编码%55%4e%49%4f%4e利用URL解码顺序差异换行/制表符UNION%0aSELECT一些WAF不匹配空白变体注意PortSwigger官方实验场景不需要这些高级绕过它们主要考验基础理解。我列出来是提醒你不要只会标准Payload要在脑子里留一条如果过滤了怎么办的线。5. 从靶场走向真实场景利用和防护思路5.1 如何利用Union注入高效获取数据打通PortSwigger的Union实验后你就有了一套可复现的数据提取方法论。我自己总结了一个四步操作流第一步判断注入类型与闭合方式。用单引号、双引号、)等测试看哪种会破坏SQL结构。PortSwigger通常给category值这种场景简单单引号闭合即可。第二步确定列数。用UNION SELECT NULL递增法配合Burp的Intruder工具一次跑多个请求效率翻倍。你可以把请求发送到Intruder给NULL数量设置一个payload位置范围1-9跑一次全出来了。第三步确定可回显列。逐个替换NULL为随机字符串记录哪个位置出现这个字符串。这一步也能通过Burp的对比功能快速定位响应差异。第四步构造数据提取Payload。知道表名和字段名后直接用UNION SELECT 目标字段 FROM 目标表。如果不知道有哪些表可以通过数据库元数据查询去发现表名。比如MySQL的information_schema.tablesPostgreSQL的pg_tablesOracle的all_tables。这也是目前很多SQL注入工具干的事情。我在实际项目中用过这个思路成功注入后先查当前数据库名再查表名然后针对有敏感词的表user、admin、ticket逐列提取。整个过程如果靠手工每分钟能处理几十条数据但如果是大数据量还是建议用工具辅助因为工具能自动判断回显位置并重组数据。5.2 代码层怎么防住这类攻击作为防守方只有理解了Union注入的原理你才能真正防御住它。核心还是参数化查询和最小化权限两板斧。参数化查询PreparedStatement用预编译的SQL模板将用户输入作为参数绑定数据库不会将其中的引号、关键字解释为SQL代码。这是根治SQL注入的唯一可靠手段。在Java里是PreparedStatement在PHP里是PDO::prepare()在Python里是cursor.execute(SELECT * FROM products WHERE category ?, (category,))。对输入做白名单校验如果某参数本质上应该是一个整数或固定字符串校验它的格式就能挡掉不少注入。比如商品分类ID只能是数字那用intval()一转换注入立刻失效。但要注意不能完全依赖白名单因为有些搜索场景就是允许用户输入任意文本这时必须参数化。错误信息处理Union注入严重依赖页面回显所以生产环境一定要关闭详细错误显示。不要让数据库报错信息直接出现在响应体里。同时可以启用通用错误页这样即使注入语句执行了攻击者也无法从页面直观判断效果。WAF与输入过滤作为纵深防御可以在应用前面加WAF过滤UNION、SELECT等危险关键字。但WAF永远只是缓解不是根治。因为绕过手段太多加上SQL语句本身可能合法使用这些关键字。我见过不少站点用了WAF后依然被打穿原因就是后端代码根本没做参数化。我个人的体会是Union注入的难度不在于学会那个Payload而在于你理解数据流是怎么从数据库流到页面的。当你哪天看到一个URL参数能脑补出后端SQL语句的大致长相Union注入对你来说就是一件条件反射的事。PortSwigger的Union实验系列本质就是在帮你训练这种脑补SQL的能力。多刷几遍尤其是把实验里每道题用两种以上方法做出来你再去真实环境看那些注入点思路会清晰很多。最后分享一个小细节在PortSwigger靶场上每次点Reset lab或者切换实验时那个随机子域名的URL会变有时候你会在Burp里看到几百个历史请求找一个干净的测试点建议直接把老请求清空避免把不同实验的Payload混在一起不然排查问题会怀疑人生。Union注入真的不难难的是耐心和细心。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询