首页 / 资讯中心 / 文章详情

SQL注入入门:union联合注入从原理到实战全解析

SQL注入入门:union联合注入从原理到实战全解析 ★ FEATURED ARTICLE
1. 从第一关说起为什么union联合注入是理解SQL注入的最佳入口很多人学SQL注入一上来就被各种报错注入、布尔盲注、时间盲注绕晕了。我带了不下二十个新人发现一个规律凡是第一关没吃透的后面越学越虚。第一关看着简单就一个?id1的参数但它把SQL注入最核心的几个概念全串起来了——参数拼接、回显位、列数判断、数据库元数据读取。union联合注入之所以适合作为入门第一课是因为它有一个其他注入方式不具备的优势结果直接回显在页面上。你不需要靠猜、靠延时、靠布尔真假来判断页面会明明白白告诉你查询结果是什么。这一关的典型场景是这样的一个新闻展示页面URL里带一个id参数后端代码大概长这样——SELECT * FROM articles WHERE id $id。你传1进去它返回第一篇文章你传1进去页面报语法错误。这个报错就是突破口说明单引号被原样拼进了SQL语句没有做任何过滤。接下来要做的就是构造一个闭合原查询、再拼接自己查询的payload。union联合注入的本质是把两条SELECT语句的结果合并成一张表返回。前提是两条语句的列数必须一致数据类型要兼容。所以整个注入流程可以拆成四步判断闭合方式、确定列数、找可回显的列、读取目标数据。每一步都有明确的信号反馈这也是它适合新手的原因——你能看到自己的每一步操作产生了什么效果。注意本文所有操作均在本地搭建的靶场环境中进行目的是理解注入原理、学会防御方法。任何未经授权对他人系统的测试都是违规行为。我见过太多人卡在第一步——闭合方式判断。有人拿到一个注入点上来就union select 1,2,3结果页面没反应就以为没有注入。其实很可能闭合方式不对。第一关用的是单引号闭合但实际环境中可能是双引号、括号、甚至like模糊查询。所以判断闭合方式是所有union注入的前置动作这一步做不对后面全白搭。2. 闭合判断与列数探测union注入的两块基石2.1 单引号报错背后的信息量在URL后面加一个单引号?id1页面如果返回类似You have an error in your SQL syntax的报错说明单引号被带入了SQL语句。这时候后端的SQL大概变成了SELECT * FROM articles WHERE id 1多出来的单引号破坏了语法结构。这个报错本身就是最有价值的信息——它确认了注入点存在也确认了参数被单引号包裹。但要注意不是所有报错都会显示在页面上。有些系统做了错误屏蔽页面只显示系统繁忙之类的提示。这种情况下单引号报错法就失效了需要换用其他方式判断比如and 11和and 12的页面差异。不过第一关没有做任何防护报错信息直接输出所以单引号法最直接。判断闭合方式有一个小技巧先试?id1如果报错再试?id1 --。--是注释符在URL中会被解析为空格所以实际发送的是--把后面的单引号注释掉了。如果加上注释符后页面恢复正常说明闭合方式就是单引号。这个验证步骤很关键它排除了报错是其他原因导致的可能性。2.2 order by判断列数的原理与实操确定闭合方式后下一步是判断当前查询返回了几列。为什么必须知道列数因为union要求前后两条SELECT语句的列数完全一致列数不对就会报错。判断列数最常用的方法是order by。order by在SQL里的作用是按第N列排序。如果N超过了实际列数数据库就会报错Unknown column N in order clause。所以我们可以从1开始递增直到报错为止最后一个不报错的数字就是列数。具体操作?id1 order by 1 --页面正常?id1 order by 2 --正常?id1 order by 3 --正常?id1 order by 4 --报错。说明当前查询返回3列。这个方法的优点是简单直接缺点是如果页面屏蔽了报错就看不到反馈。第一关没有屏蔽所以用order by最方便。也可以用union select来猜列数。?id1 union select 1 --如果报错说明列数不是1?id1 union select 1,2 --不报错说明列数是2。但这种方法每次都要改payload不如order by递增来得系统。我一般建议新手先用order by确定列数再用union select验证两个方法互相印证避免误判。提示order by的数字可以用二分法加速。比如先试10报错说明列数小于10再试5不报错说明列数在5到10之间。但第一关列数少直接递增就行没必要复杂化。2.3 确定回显位为什么1,2,3里只有部分数字会显示知道列数是3之后构造?id1 union select 1,2,3 --。这时候页面会显示查询结果。但你会发现页面上显示的不是1,2,3这三个数字而是某些位置显示数字某些位置显示的是文章内容。这是因为原查询的id1有结果union的第二条语句结果被追加在后面但页面模板只取了第一行数据来渲染。具体来说后端代码可能是$row mysqli_fetch_array($result)只取了结果集的第一行。原查询id1返回了一行数据union的第二行数据虽然也在结果集里但没被取到。所以要让union的结果显示出来需要让原查询返回空结果。方法很简单把id1改成一个不存在的值比如id-1。这样原查询返回空union的第二行就成了结果集的第一行页面就会显示我们注入的数字。?id-1 union select 1,2,3 --页面会显示1、2、3分别出现在哪个位置。假设页面显示的是2和3说明第2列和第3列是回显位第1列不回显。后续读取数据时就要把要查的内容放在第2或第3列的位置。这一步的常见坑是有人直接用id1做union发现页面没变化就以为注入失败。其实不是失败是原查询的结果把union结果盖住了。改成id-1或者id0就能看到效果。这个细节很多教程一笔带过但新手最容易在这里卡住。3. 从回显位到数据库一步步把数据钓出来3.1 数据库名、版本、当前用户的读取确定回显位在第2列和第3列之后就可以把想要查询的函数替换进去。最基础的信息收集包括数据库名database()、数据库版本version()、当前用户user()。构造payload?id-1 union select 1,database(),version() --。页面第2列显示数据库名第3列显示版本信息。第一关的数据库名通常是security版本可能是5.7.x或8.0.x。这些信息看起来不起眼但决定了后续的注入策略。比如MySQL 5.0以上才有information_schema库才能用元数据表查表名列名版本低于5.0就只能靠猜表名。当前用户user()也很重要。如果是rootlocalhost说明数据库连接用的是高权限账户后续可能可以读写文件如果是普通用户就只能老老实实查数据。第一关用的是root但这只是靶场环境实际环境中遇到root权限的概率并不高。注意database()、version()、user()这些函数在union注入中可以直接放在回显位。但如果回显位只有一个可以用concat()把多个信息拼在一起比如concat(database(),0x3a,version())0x3a是冒号的十六进制用来分隔。3.2 information_schema注入中最重要的元数据宝库MySQL 5.0之后引入了一个系统数据库information_schema里面存储了所有数据库、表、列的信息。对注入来说这就是一张地图。核心的三张表是表名作用关键字段SCHEMATA存储所有数据库名SCHEMA_NAMETABLES存储所有表名TABLE_SCHEMA, TABLE_NAMECOLUMNS存储所有列名TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME查数据库名的payload?id-1 union select 1,2,(select group_concat(schema_name) from information_schema.schemata) --。group_concat()把多行结果拼成一行避免只显示第一个数据库名。查指定数据库的表名?id-1 union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schemasecurity) --。这里table_schema指定数据库名第一关就是security。查指定表的列名?id-1 union select 1,2,(select group_concat(column_name) from information_schema.columns where table_schemasecurity and table_nameusers) --。这三步走完数据库的结构就完全暴露了。第一关的security库里通常有users表users表里有id、username、password三个字段。知道这些之后直接查数据就行。3.3 读取users表数据从字段到具体值查用户名和密码?id-1 union select 1,group_concat(username),group_concat(password) from users --。页面第2列显示所有用户名第3列显示所有密码。第一关的密码通常是明文或者MD5如果是MD5可以用在线工具或者本地字典跑一下。这里有一个细节group_concat()默认有长度限制MySQL默认是1024个字符。如果数据量大可能会被截断。可以用select group_concat_max_len查看当前限制或者在注入时用substring()分段读取。不过第一关数据量小直接group_concat就够了。如果回显位只有一个可以用concat_ws()把多个字段拼在一起?id-1 union select 1,2,concat_ws(0x3a,username,password) from users limit 0,1 --。limit 0,1控制每次读一行通过改变偏移量逐行读取。这种方法适合回显位少、数据量大的场景。提示实际注入中group_concat和limit配合使用最灵活。先用group_concat看总量如果被截断再用limit逐行读。第一关数据少group_concat一步到位。4. 那些教程不会告诉你的实战细节4.1 注释符的三种写法与适用场景union注入中注释符用来把原SQL后面的内容注释掉保证语法正确。常见的注释符有三种--两个减号加一个空格SQL标准注释URL中空格要用或%20代替所以写成--或--%20。#MySQL特有注释URL中要编码为%23。/* */块注释可以注释中间的内容比如union/*xxx*/select。第一关用--最方便因为在URL中自动解析为空格。但有些环境对--不友好比如参数被urldecode多次可能变成字面加号。这时候用%23更稳。我一般建议新手先试--不行再换%23。还有一个坑--后面必须跟空格才能生效--中的就是空格。如果写成--直接跟字符比如--abcMySQL会把它当成减法运算不是注释。这个细节在手工注入时经常被忽略。4.2 字符型与数字型的区别单引号不是必须的第一关是字符型注入参数被单引号包裹所以payload里要闭合单引号。但如果是数字型注入SQL语句是SELECT * FROM articles WHERE id $id没有引号payload就不需要单引号。判断方法很简单传?id1正常传?id1报错说明有引号传?id1 and 11正常传?id1 and 12页面变化说明没引号。数字型注入的union payload是?id-1 union select 1,2,3 --没有单引号。字符型则是?id-1 union select 1,2,3 --。这个区别看似小但实际注入中搞错了就完全没反应。我见过有人在一个数字型注入点上硬加单引号试了半天以为没有注入其实就是闭合方式判断错了。4.3 回显位被占用时的替代方案有时候union select的1,2,3都显示在页面上但页面模板只渲染特定列比如只显示第2列。这时候如果第2列被原查询占用就需要用id-1让原查询为空。但如果id-1也不行可能是原查询用了limit 1或者页面逻辑特殊。替代方案是用and 12让原查询返回空?id1 and 12 union select 1,2,3 --。and 12让原查询条件为假返回空结果集union的结果就成了第一行。这个方法在id-1不生效时特别有用。还有一种情况是页面只显示查询结果的某些字段比如只显示title和content。这时候需要先确定哪些列对应title和content然后把要查的数据放在对应的列位置。第一关页面显示的是文章标题和内容对应第2列和第3列所以payload里把数据放在第2、3列就能显示。4.4 常见报错与快速排查表报错信息可能原因解决方法You have an error in your SQL syntax闭合方式不对或注释符没生效检查单引号是否闭合注释符是否正确The used SELECT statements have a different number of columnsunion前后列数不一致用order by重新确认列数Unknown column N in order clauseorder by的数字超过实际列数减小数字直到不报错Illegal mix of collations数据类型或字符集不兼容用concat或hex转换数据类型Subquery returns more than 1 row子查询返回多行用group_concat或limit 1限制这张表是我自己踩坑总结的实际注入中遇到报错先对照排查能省不少时间。特别是第一个报错十有八九是闭合或注释的问题。5. 从注入到防御理解攻击才能更好防守5.1 预编译语句为什么能挡住union注入union注入能成功根本原因是用户输入被当作SQL代码执行了。防御的核心思路就是让用户输入永远只是数据不参与SQL语法解析。预编译语句Prepared Statement就是干这个的。以PHP为例不安全的写法是$sql SELECT * FROM articles WHERE id $id然后mysqli_query($sql)。安全的写法是$stmt $mysqli-prepare(SELECT * FROM articles WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute();?是占位符bind_param把$id绑定为整数。即使用户传-1 union select 1,2,3 --数据库也只会把它当成一个字符串去匹配id字段不会解析成SQL语句。预编译在数据库层面就把代码和数据分开了这是最彻底的防御方式。但预编译不是万能的。如果表名、列名、order by的字段名是动态拼接的预编译就保护不了。因为这些位置不能用占位符只能拼接。这时候需要用白名单校验比如order by只允许id、title、date三个值其他一律拒绝。5.2 输入过滤的局限性与绕过思路很多系统用addslashes()或str_replace()过滤单引号但这只能挡住字符型注入挡不住数字型注入。而且过滤规则如果不够全面很容易被绕过。比如只过滤了单引号没过滤双引号只过滤了小写union没过滤大写UNION只过滤了空格没过滤/**/。从攻击者视角看常见的绕过方式包括大小写混写UnIoN、内联注释union/**/select、双写ununionion、编码%55nion。这些绕过手法说明黑名单过滤永远有漏网之鱼。防御应该用白名单只允许合法的字符和格式而不是试图枚举所有非法输入。提示addslashes()在GBK编码下还有宽字节注入的问题。%df经过addslashes变成%df%5c%df%5c在GBK中是一个合法汉字单引号就逃逸出来了。所以字符集也要统一用UTF-8避免宽字节问题。5.3 最小权限原则为什么数据库账户不该用root第一关的靶场用的是root账户这是为了方便教学。实际环境中Web应用连接数据库的账户应该遵循最小权限原则只给必要的SELECT、INSERT、UPDATE、DELETE权限不给DROP、FILE、PROCESS等权限。这样即使被注入攻击者也无法读取敏感文件、无法提权、无法删除数据。具体来说可以创建一个专用账户CREATE USER webapplocalhost IDENTIFIED BY strongpassword; GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO webapplocalhost;。这样webapp账户只能操作appdb库不能访问information_schema以外的其他库也不能读写文件。最小权限原则配合预编译基本能挡住绝大多数SQL注入攻击。但安全是一个整体还需要配合WAF、日志监控、定期扫描等手段。单靠某一种防御措施都不够。6. 手工注入之外什么时候该用工具什么时候必须手工6.1 sqlmap的便利与盲区说到SQL注入很多人第一反应是sqlmap。确实sqlmap能自动完成闭合判断、列数探测、数据读取省时省力。但sqlmap不是万能的。它依赖大量的请求和响应特征来判断注入类型如果目标系统有WAF、有频率限制、或者响应不稳定sqlmap很容易误判或卡住。而且sqlmap的自动化会掩盖很多细节。新手如果一上来就用sqlmap可能跑出了数据但完全不知道数据是怎么出来的。遇到sqlmap跑不出来的注入点就束手无策了。所以我一直建议先手工把第一关完整走一遍理解每一步的原理再用sqlmap验证和加速。手工注入的价值在于你能根据页面的实时反馈调整payload。比如order by报错了你能立刻判断是列数问题还是闭合问题union没回显你能想到用id-1让原查询为空。这些判断是工具替代不了的。6.2 手工注入的完整流程复盘把第一关的完整流程串一遍访问?id1页面正常显示第一篇文章。访问?id1页面报SQL语法错误确认注入点存在。访问?id1 --页面恢复正常确认单引号闭合。访问?id1 order by 1 --到order by 4 --确定列数为3。访问?id-1 union select 1,2,3 --确定回显位为第2、3列。访问?id-1 union select 1,database(),version() --读取数据库名和版本。访问?id-1 union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schemasecurity) --读取表名。访问?id-1 union select 1,2,(select group_concat(column_name) from information_schema.columns where table_schemasecurity and table_nameusers) --读取列名。访问?id-1 union select 1,group_concat(username),group_concat(password) from users --读取数据。这九步走完第一关就彻底通了。每一步都有明确的输入和输出可以反复验证。我建议新手把这九步抄下来在本地环境跑三遍以上直到不看笔记也能独立完成。6.3 从第一关延伸到后续关卡的学习路径第一关是union注入的基础后续关卡会在这个基础上增加难度。比如第二关是数字型注入payload不需要单引号其他流程一样。第三关是字符型加括号闭合payload变成?id-1) union select 1,2,3 --。第四关是双引号加括号payload变成?id-1) union select 1,2,3 --。第五关开始有过滤需要绕过and、or、union等关键词。后面的关卡会涉及报错注入、布尔盲注、时间盲注、堆叠注入等。学习路径很清晰先把union注入的四种闭合方式无引号、单引号、单引号加括号、双引号加括号练熟再学过滤绕过最后学报错和盲注。union注入是基础中的基础基础打牢了后面学起来会快很多。我在实际带新人的过程中发现第一关花两个小时彻底吃透比后面囫囵吞枣做十关效果更好。因为第一关涵盖了注入的核心思维观察反馈、构造payload、验证结果、逐步深入。这个思维模式一旦建立后面只是换不同的函数和绕过方式而已。最后分享一个我自己的习惯每学一种新的注入类型我都会在本地搭一个最小化的靶场把payload从简到繁写在一个文本文件里标注每一步的预期结果。这样下次遇到类似场景直接翻笔记就能快速定位。第一关的笔记我现在还留着虽然简单但每次翻到都能想起当初卡在id-1那个点上的自己。
阅读完成 · 觉得有帮助?
咨询建站