资讯动态

SQL注入入门:Union联合查询注入从原理到实战

发布时间:2026/10/16 4:00:26 来源:尧图企业网站定制
SQL注入这块任何想往Web安全方向走几步的人、或者后端开发写到一半突然发现接口被人“玩”过的朋友早晚都要补这一课。网上一搜“SQL注入基础”要么是工具一把梭的截图要么直接从复杂注入技巧开讲对刚入门的人非常不友好。这篇内容我尽量压成一条主线——Union联合查询注入从前端传参到数据库返回结果把每一步的原理和手测思路都摊开讲透配合本地模拟环境现场演示让你看完既能理解攻击面是怎么回事也知道开发侧应该怎么把自己守成铁桶。所有演示都放在你自己搭建的教学环境或授权靶场里未经授权的目标系统不要碰这个不是客套话是底线。1. SQL注入的底层逻辑数据和指令的边界是怎么碎的1.1 一个“报销单”故事讲懂SQL注入先把SQL注入的本质用大白话讲明白。你去财务报销手里有张报销单财务人员按流程把“金额”这一栏的数字抄进系统这就是一条正常的请求。但如果有人在报销单“金额”栏写了“1000并且额外给自己批三天假”财务也不检查把整句话当成“金额”就录入系统那结果就不受控了。SQL注入就是同一类问题后端代码把用户输入的“数据”直接拼接进“SQL指令”里数据库分不清哪部分是数据、哪部分是命令于是把你输入的内容当成了SQL语法去执行。举个例子正常的登录查询可能是这样拼出来的$sql SELECT id, username, email FROM users WHERE username . $_POST[username] . AND password . $_POST[password] . ;你输入的用户名是admin密码是123456拼出来就是SELECT id, username, email FROM users WHERE username admin AND password 123456这没有任何问题。但如果你在用户名字段输入admin --拼出来的SQL就变成了SELECT id, username, email FROM users WHERE username admin -- AND password 123456注意看你输入的单引号把原本SQL里的第一个单引号闭合了后面的--是SQL里的注释语法注释符号后面的所有内容都会被数据库忽略。于是AND password ...这段身份校验直接没了查询条件只剩下username admin。这就是SQL注入最基础、最原始的一种形态通过精心构造的输入改变原SQL语句的语义。这个例子里--前后各有一个关键符号单引号负责“闭合”注释符负责“切断后半段”。很多刚入门的人只记得记Payload却不知道自己输入的每个字符在SQL解析器眼里意味着什么一旦换环境、换数据库立刻抓瞎。我建议你把这个“拼接后看完整SQL”的习惯刻在脑子里遇到不明白的注入点先在本地把最终SQL打印出来看一眼比什么技巧都稳。1.2 注入点不是只有URL参数一个地方新手最容易陷入的误区就是认为只要往URL里塞一段奇怪字符串就算测注入。实际上只要是后端代码把外部传入的数据拼进SQL的地方都可能成为注入点常见的位置包括URL查询参数?id1、?usernameadmin这类最典型。POST表单字段登录框、搜索框、注册信息别以为POST就安全。Cookie里的用户标识有些系统从Cookie直接取值查数据库。HTTP请求头的自定义字段比如客户端版本号、设备ID部分接口会拿它拼SQL。判断一个位置能不能作为注入点关键不是它长在URL还是请求体里而是“用户可控内容是否被直接拼进了SQL语句”。这个认知比背十个Payload都重要。另外强调一个常见误区很多刚入门的人以为“用了ORM框架就不会有SQL注入”。ORM确实会帮你转义和参数化但那不意味着绝对安全如果你在ORM里写raw query、拼接排序字段、或者用whereRaw这一类接口还是会把拼接问题带回来。所以判断注入风险永远要看最终落到数据库里的SQL是怎么生成的而不是看外层用了什么框架。1.3 为什么Union联合查询注入适合作为第一篇精读SQL注入基础里技术分支非常多报错注入、布尔盲注、时间盲注、宽字节注入、Union联合查询注入……为什么大多数人建议先学Union联合查询注入两个原因第一它够直观。Union注入的核心能力是把数据库里其他地方的数据“合并”到当前查询结果集中并直接回显到页面上。你能亲眼看到数据出现在浏览器里这种正向反馈对建立信心、验证分析是否正确特别重要。盲注要猜几百次响应差异Union注入只要条件满足一次查询就能把结果带出来。第二它涉及的知识点最体系化。要成功执行Union注入你需要理解SQL的UNION运算符、表的字段列数、数据类型匹配、数据库元信息表比如information_schema这些知识。这些知识不只服务Union注入往后学报错注入、学SQL优化、学数据字典全都是同一套底层地基。把Union注入啃下来相当于一次性把SQL查询的核心机制过了一遍。2. Union联合查询注入的原理解读两个硬性条件必须同时满足2.1 UNION运算符到底帮我们做了什么UNION是SQL标准里的集合运算操作符作用是把两个或多个SELECT查询的结果合并成一个结果集。举个例子你有一个学生表student和一个教师表teacher想让两张表的数据在同一个列表里展示出来就可以写SELECT name, age FROM student UNION SELECT name, age FROM teacher数据库会先执行左边的查询再执行右边的查询然后把双方的结果纵向拼在一起返回。这个能力的本质是“将一段额外查询的结果追加到主查询结果之后”而Union注入利用的正是这一点如果主查询是后端代码拼接出来的那注入的内容只要能让原始查询合法结束再补一个UNION和一段自定义SELECT就能把数据库里任意表的数据“并”到页面显示出来的查询结果里。2.2 为什么必须满足“列数一致”和“类型接近”UNION在合并结果集时数据库会强制校验两条SELECT语句的字段数量是否一致。左边查出三列右边写了两列数据库直接报错左边查出三列右边写了四列同样报错。这是第一道硬门槛。第二道门槛是字段类型兼容。MySQL等数据库要求两个结果集的对应列数据类型要兼容比如左边第一列是整数右边第一列是字符串很多情况下会报类型错误。所以为了稳妥注入时选择的占位内容通常要跟目标列的显示类型尽量统一。如果你不知道目标列是什么类型最常用的办法就是用数字和字符串混合占位逐个位置试过去看到哪个位置能正常显示就知道那一列对什么类型友好。除此之外还有一个容易被忽略的条件你要让主查询本身“查不到数据”。为什么如果主查询能查出正常数据那UNION拼接出来的额外结果会排在下面页面如果只渲染第一条记录你就什么都看不到。常规做法是把注入点参数改成不存在的值比如id-1使主查询返回空结果这样UNION后面那段查询的结果自然就成了结果集里的第一行直接呈现在页面上。把这三个条件连成一句话就是让主查询闭合且无结果让UNION子查询列数与主查询一致让对应列类型尽量匹配。三者缺一不可这也是手测Union注入时最核心的排查思路。3. 在本地模拟项目里走一遍完整验证流程3.1 准备一个完全可控的教学靶机理论讲再多不动手都是纸上谈兵。下面我带你搭一个最小化的演示环境用来复现和理解Union注入。这里有一个原则必须提前说清楚你要测试的系统要么是你自己写的要么是你拥有明确授权的靶机环境绝对不要拿这个思路去试探任何没有授权的线上系统。安全工作者的能力不是体现在“能打进多少站”而是体现在“知道技术原理的同时知道边界在哪”。演示环境我用一个本地的MySQL数据库和一个极简的PHP接口来模拟当然你用Python Flask、Node.js写一个同样的等效接口也没问题核心在于数据库和查询逻辑。先建一个教学库为了贴近实际场景我们模拟一个某高校内部社团报名系统的数据表CREATE DATABASE demo_database; USE demo_database; CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(50), age INT, email VARCHAR(100), mobile VARCHAR(20) ); INSERT INTO student VALUES (1, 张三, 20, zhangsanexample.com, 13800000001), (2, 李四, 21, lisiexample.com, 13800000002), (3, 王五, 22, wangwuexample.com, 13800000003);后端模拟一个按昵称查询信息的接口。这里的SQL代码是刻意按不安全的拼接方式来写的目的就是让你直观看到问题代码长什么样?php $conn new mysqli(127.0.0.1, root, your_password, demo_database); $id $_GET[id]; $sql SELECT id, name, age, email FROM student WHERE id . $id; $result $conn-query($sql); if ($result) { while ($row $result-fetch_assoc()) { echo ID: . $row[id] . br; echo 姓名: . $row[name] . br; echo 年龄: . $row[age] . br; echo 邮箱: . $row[email] . br; } } else { echo 查询出错: . $conn-error; } ?访问query.php?id1就能看到张三的信息。这个接口的每一个输出都对应查询的一列后续我们判断回显位时就要用到。3.2 第一步确认注入点是否真实存在拿到一个参数先别急着上Union第一步永远是验证这里到底能不能拼进SQL语义。最经典的做法是传一个单引号看看反应测试query.php?id1如果后端把单引号原样拼接SQL就会变成WHERE id 1语法不合法数据库会抛错。错误类型通常是You have an error in your SQL syntax这本身就是在告诉你用户输入被当作SQL语句执行了。再做一次更稳妥的验证传query.php?id1 AND 11页面正常返回张三再传query.php?id1 AND 12页面不返回任何数据。两次结果的差异说明我们插入的AND 12确实参与了查询逻辑注入点是实打实存在的。这里的原理也很好懂AND 11恒为真不影响原查询AND 12恒为假整个查询条件被强行改成了查不到任何数据。判断注入点这步我强烈建议你多做几个排列单引号、双引号、反斜杠、AND 11、AND 12分别看响应差异。不要因为一种方式报错就急着下结论不同数据库对引号的处理细节有差异尤其是在过滤了单引号的环境里双引号和反斜杠往往能帮你发现更隐蔽的拼接路径。3.3 第二步用ORDER BY摸清字段列数注入点确认之后第二步不是直接写UNION而是先确认主查询的列数。为什么顺序这么重要因为前面说了UNION合并结果集要求两边列数一致你连左边有几列都不知道右边写几列都是瞎猜。这里要用到ORDER BY排序语句的一个特性ORDER BY 1表示按第一列排序ORDER BY 2表示按第二列排序以此类推。如果排序的数字超过了实际列数数据库就会报错。所以我们可以用递增的数字去探测边界测试query.php?id1 ORDER BY 3正常返回说明至少有三列。 测试query.php?id1 ORDER BY 4报错说明这个查询一共只有三列。这里要注意有些MySQL版本在ORDER BY后面可以直接用数字ORDER BY 4报错的原因就是“列数超出范围”。用这一组测试你就能快速拿到列数为下一步做准备。这个思路我自己实践下来非常省时间比直接在UNION后面挨个试SELECT 1,2,3更精准。因为SELECT 1,2,3如果列数不匹配虽然也会报错但报错信息不如ORDER BY的边界感清晰新手容易看半天不知道自己错在哪。3.4 第三步用UNION SELECT定位回显位知道列数之后我们就可以构造一个Union探测语句。由于当前查询有三列尝试测试query.php?id-1 UNION SELECT 1,2,3这里使用id-1是为了让主查询不返回任何正常数据这样联合查询的结果就会直接显示到页面上。如果页面显示ID: 1姓名: 2年龄: 3说明三个占位符被原样回显到了对应位置。这就把“回显位”找到了。接下来我们把占位符换成真实的数据列名。查询当前数据库名SELECT id, name, age FROM student WHERE id -1 UNION SELECT 1, database(), 3页面会把当前数据库名显示在“姓名”的位置因为第二个占位符对应的是第二列。这个“第二列能显示字符串”的规律非常关键后续你要显示表名、字段名就把对应信息填到第二个空位里。回到演示项目里我通常还会让学生自己再跑一遍UNION SELECT 1,2,3,4来验证列数边界。如果页面报错说明主查询并不是四列如果正常显示四列说明你ORDER BY的探测对象选错了。这类对照实验看着简单但能把原理真正内化。4. 从库表结构定位到数据提取一套通用的分析思路4.1 用数据字典来获取表名和字段名回显位已经定位了但如果只想通过Union看到id1后面的数据那意义不大。注入的价值在于你能访问整个数据库的数据字典从而枚举出所有表、所有字段。在MySQL中当前实例的库表信息存储在information_schema这个系统数据库里。查询当前库下所有表名的SQL可以这样写SELECT id, name, age FROM student WHERE id -1 UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema database()这里database()返回当前使用的数据库名table_name是表名字段。执行后页面第二列会显示当前库里的所有表名我们能看到student表。再看具体的字段名SELECT id, name, age FROM student WHERE id -1 UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_name student执行后第二列会显示student表的所有字段id、name、age、email、mobile。到这里整个分析链路就闭环了查库名、查表名、查字段名、查数据。这套思路为什么值得记因为它在很多数据库里是通用的只是数据库字典的名称和字段名略有差异。你掌握了MySQL的这套体系以后换到其他数据库只需要查对应版本的数据字典API文档核心逻辑完全一样。4.2 合并显示时为什么要用GROUP_CONCAT刚入门的人经常会遇到一个问题SELECT ... UNION SELECT 1, table_name, 3执行完一条记录只显示一个表名想看完整列表得不断调整偏移量效率很低。MySQL提供了一个聚合函数GROUP_CONCAT可以把多行结果拼成一行用逗号分隔。比如SELECT id, name, age FROM student WHERE id -1 UNION SELECT 1, GROUP_CONCAT(table_name), 3 FROM information_schema.tables WHERE table_schema database()这样一次就能看到所有表名。这个小函数在后来的数据追踪、日志分析里也经常用到属于性价比很高的一招。有一点要提醒不同的数据库对聚合结果的长度有限制当表非常多、拼接结果过长时显示内容可能被截断。如果你在真实测试中只看到部分内容别急着怀疑注入失效先考虑是不是输出长度的限制问题。4.3 常见问题速查表Union注入从入门到跑通我在带新手的过程中遇到过大量重复性报错这里整理成一张速查表方便你对照排查。现象可能原因排查方向注入单引号后页面无报错参数被过滤或转义拼接后没到达数据库尝试双引号、编码绕过确认后端是否加了过滤函数ORDER BY数字增大后始终正常查询可能使用了子查询或固定字段映射换一个注入点或回页面找真实列数UNION SELECT列数一致但报错数据类型不兼容将占位内容统一改为数字或统一字符串逐个位置替换测试页面不显示UNION部分数据主查询也返回了数据Union结果排在后面把id改为负数或非法值让主查询结果为空只能看到第一条UNION数据返回多行时页面循环输出受限使用GROUP_CONCAT聚合或利用LIMIT调整数据位置查询表名时一片空白当前数据库用户权限不足更换更高权限账号或检查是否连接到了错误的数据库实例这张表是我经过多次重复教学后整理出来的基本覆盖了新手最常见的六个坑。遇到问题不要慌对照每一行的方向和结果重新看一下最终拼接的SQL八成都能定位。5. 开发侧防守看完别人的攻击路径怎么把自己守成铁桶5.1 参数化查询为什么是治本方案每一次拼SQL的地方都是一个风险点防守的核心原则就一条永远不让外部数据直接参与SQL语法解析。最可靠的办法就是参数化查询也常叫预编译语句。参数化查询的原理是先把SQL语句的骨架发给数据库数据库对这个骨架完成编译之后再通过独立通道把参数值传进去。参数值到数据库时只是一个“值”不会再参与语法解析。还是用报销单类比财务系统先把“报销单”的表单结构固化你填写的内容不管里面写了什么都只是被当作“金额”字段的一个值而不是指令。从根上断了注入的可能性。同样的查询使用MySQLi的预处理写法是这样的$stmt $conn-prepare(SELECT id, name, age, email FROM student WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute();?是占位符bind_param把$id绑定为参数。不管你输入什么它都只是一个整数类型的值不可能变成额外的SQL片段。这一条做法就能解决绝大多数SQL注入问题也是我建议所有后端开发默认使用的写法。现代主流语言和框架都有成熟的参数化方案比如Python的pymysql用%s占位Node.js的mysql2用?占位Java的PreparedStatement是JDBC标准。无论框架怎么换核心思想完全一致先编译骨架再传参数。5.2 纵深防御别把所有希望寄托在一个点参数化查询确实能挡住常规注入但安全不能只靠一招。成熟的开发团队通常会叠加多层防守对输入做白名单或类型校验比如接收id的接口强制只允许整数用ctype_digit或正则先卡一遍非法的直接拒绝。给数据库账号配置最小权限Web应用连库使用的账号只授予当前业务表的SELECT/INSERT/UPDATE/DELETE权限不授予FILE、DROP、GRANT等危险权限。即使注入发生了攻击者也没有权限拖库。关闭或简化错误回显线上环境不要开display_errors把数据库报错信息包装成统一错误页面避免攻击者拿到表结构线索。对SQL查询日志做定期审计找出所有使用字符串拼接查询的地方逐一改造为参数化查询。接入Web应用防火墙等防护设备对明显的注入特征进行拦截比如单引号结合UNION SELECT、information_schema这类组合。我个人见过很多“只加过滤函数”的项目比如用addslashes或者简单替换单引号来防注入。这种做法在特定编码下可能有效但遇到宽字节、二次解码等情况就会失效。过滤只能作为辅助手段不能作为主力防线。真要根治还是把参数化查询用到位。最后给出一份开发侧的自检清单每次上线前对照着过一遍检查项完成状态所有SQL查询是否都使用参数化/预处理语句是 / 否 / 不适用是否还有任何字符串拼接生成SQL的代码是 / 否排序字段、表名等动态部分是否做了严格白名单是 / 否数据库账号是否遵循最小权限原则是 / 否线上环境是否关闭了详细错误回显是 / 否上线前是否做过基础的注入测试和代码走查是 / 否6. 写给小白的几点心得最后说几句我在教初学者和带团队时总结的体感经验。第一学SQL注入基础最忌讳“背Payload不学原理”。很多人把一串串注入语句背得滚瓜烂熟一旦数据库版本不同、代码写法不同就完全不会分析了。真正该花时间的是搞懂SQL解析器怎么看待引号、注释、空格搞懂UNION的合并机制和数据库系统表的结构。原理通了Payload是你顺手就能写出来的东西。第二练手环境一定要自建。现在有很多靶场项目和在线实验平台搭一个本地环境既不违法也不缺德更不会惹上麻烦。自己从建表、写接口、拼错代码开始走一遍再亲手把代码改成参数化查询体验一遍修复前后的差异这个流程的价值远超看十篇文章。公网上那些“测试一下”的念头早点断掉安全圈最看重的就是职业操守。第三学会看最终SQL这是最能提升排查效率的习惯。不管是自己写代码还是分析他人的问题每次遇到模糊不清的报错先把拼接后的SQL打出来看一遍95%的问题当场就能定位。这个习惯我从入门一直保持到现在处理线上问题时它帮我省下的时间不可估量。Union联合查询注入只是SQL注入基础里的一小块拼图但如果你把它彻底吃透后面去看盲注、报错注入、二次注入会发现底层逻辑全是相通的。先把这一条线走通建立起“数据与指令边界”这个概念你再回头读其他安全文章就不会再觉得云里雾里了。

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

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

免费获取报价 →
↑