
1. 从“万能密码”到联合查询理解SQL注入的核心攻击链很多刚接触Web安全的朋友最开始听说SQL注入可能都是从那个经典的“万能密码”开始的在登录框的用户名输入admin --密码随便填然后就能直接登录后台。这个简单的例子像一把钥匙打开了通往数据库世界的大门也暴露了无数应用最脆弱的一面。但“万能密码”只是SQL注入攻击的冰山一角它利用了注释符来截断密码验证逻辑属于一种逻辑绕过。而今天我们要深入探讨的是SQL注入攻击中更为强大、也更为核心的一种技术——Union联合查询。当你看到攻击载荷里出现union select 1,2,3...这样的结构时就意味着攻击者已经不再满足于简单的绕过登录他们的目标是直接窃取数据库里的任意数据从管理员密码、用户手机号到整个公司的业务核心表都可能被一网打尽。为什么union select如此关键因为它代表了SQL注入攻击从“布尔盲注”像猜谜一样通过页面返回的真假来推断数据或“时间盲注”通过页面响应延迟来推断这类间接、低效的攻击方式向直接数据回显的跨越。攻击者通过构造一个合法的UNION操作将自己的查询语句“拼接”到原始查询之后并将查询结果直接显示在网页上。这就像你本来只是在问门卫“我能进去吗”现在你直接伪造了一张工作证大摇大摆地走进办公室并开始翻阅任何你感兴趣的文件柜数据表。理解union select的原理、利用条件和绕过技巧不仅是渗透测试工程师的必修课也是每一位后端开发人员编写安全代码时必须绷紧的一根弦。接下来我们就从最基础的原理开始一步步拆解这个强大的攻击手段。1.1 Union联合查询的本质合并两个世界的结果集要理解攻击必须先理解工具本身。UNION操作符在标准的SQL语言中用于合并两个或多个SELECT语句的结果集。它有一个关键特性每个SELECT语句必须拥有相同数量的列并且列的数据类型也必须相似。数据库会执行这两个查询然后将第二个查询的结果集“追加”到第一个查询的结果集下面最后返回一个合并后的结果。举个例子假设我们有两个简单的表employees表有id,name,department三列。contractors表也有id,name,department三列。如果你想获取所有工作人员包括正式员工和合同工的名单可以这样写SELECT id, name, department FROM employees UNION SELECT id, name, department FROM contractors;数据库会返回一个包含所有不重复记录的结果集UNION默认会去除重复行如果想保留所有行包括重复的需要使用UNION ALL。在SQL注入的语境下这个特性被攻击者巧妙地利用了。原本的应用程序查询可能是这样的SELECT title, author, content FROM articles WHERE id {用户输入的ID};这里原始查询返回3个列title,author,content。攻击者发现注入点后会先通过order by或其它方式探测出原始查询的列数。假设探测出是3列那么他就可以构造这样的注入语句SELECT title, author, content FROM articles WHERE id 1 UNION SELECT username, password, email FROM users --注入后的完整SQL变成了SELECT title, author, content FROM articles WHERE id 1 UNION SELECT username, password, email FROM users -- ;--是SQL注释符它会让后面的所有内容比如原本查询中闭合单引号的都被数据库忽略。于是数据库执行了两个查询第一个查询返回了ID为1的文章信息第二个查询则直接返回了users表中的用户名、密码和邮箱。如果这个联合查询的结果被应用程序直接显示在页面上例如文章标题、作者、内容分别显示在网页的三个不同区域那么攻击者就能在页面上直接看到users表的数据攻击就此得逞。注意UNION前后查询的列数必须一致但列名和数据类型并不需要完全一致。数据库会以第一个查询的列名为准。数据类型“相似”即可例如数字类型的列和字符类型的列在某些数据库中可以合并数字会被隐式转换为字符串但这可能因数据库而异是攻击中需要测试的点。1.2 为什么是“select 1,2,3”探测与占位的关键步骤如果你看过一些SQL注入的实战案例或CTFCapture The Flag赛题一定会对union select 1,2,3这个“神秘代码”印象深刻。它看起来毫无意义既不是表名也不是具体数据为什么攻击总是从它开始这其实是攻击者在实施Union注入前必须完成的两个关键前置步骤的直观体现确定注入点和探测查询列数。select 1,2,3在这里扮演了“探测兵”和“占位符”的双重角色。第一步确认注入点并判断列数。攻击者通常无法直接看到后端SQL语句。他们首先需要通过输入、等字符观察页面是否报错或行为异常来确认是否存在SQL注入漏洞。确认漏洞存在后下一步就是搞清楚原始查询到底SELECT了多少列。因为UNION要求列数相等不知道列数就无法构造正确的Payload。探测列数最经典的方法就是使用ORDER BY子句。ORDER BY后面可以接列名也可以接数字这个数字代表结果集中第几列例如ORDER BY 1表示按第一列排序。攻击者会不断递增这个数字直到页面报错。?id1 ORDER BY 1 -- (页面正常) ?id1 ORDER BY 2 -- (页面正常) ?id1 ORDER BY 3 -- (页面正常) ?id1 ORDER BY 4 -- (页面报错)如果ORDER BY 4时报错而ORDER BY 3时正常就说明原始查询返回的结果集只有3 列。这就是数字“3”的由来。第二步确定数据回显点。知道有3列后攻击者就可以构造union select 1,2,3。这里的1,2,3本身是常量没有实际查询意义。它的核心目的是验证Union是否可用如果页面正常执行并返回了结果说明Union注入是可行的且列数判断正确。定位回显位置这是最关键的一步。应用程序通常会从数据库查询结果中取出某些列的值并显示在网页的特定位置。例如第一列title可能显示为文章大标题第二列author显示在作者栏第三列content显示在正文区域。攻击者提交?id-1 UNION SELECT 1,2,3 --注意这里把id设为-1或一个不存在的值是为了让原始查询结果为空从而确保页面显示的内容完全来自我们Union的select 1,2,3。然后他们观察网页。如果页面上原本显示“文章标题”的地方变成了数字“1”原本显示“作者”的地方变成了数字“2”正文区域变成了数字“3”那么他们就成功“标记”了网页上的数据回显点。接下来攻击就变得极其简单直接。攻击者只需要把select 1,2,3中的数字替换成他们想查询的真实数据库信息即可。例如他们发现数字“2”显示的位置非常醒目那么Payload就会变成?id-1 UNION SELECT 1, database(), 3 --这样数据库名就会显示在网页的“作者”位置。同理可以替换为select 1, group_concat(table_name),3 from information_schema.tables where table_schemadatabase()来爆出所有表名。所以union select 1,2,3不是一个攻击命令而是一个侦查命令。它完成了从“发现漏洞”到“准备窃取数据”之间最关键的桥梁工作。2. 深入Union注入的实战从信息收集到数据窃取理解了基本原理后我们来看一个完整的、模拟真实场景的Union注入攻击流程。这个过程就像一场精心策划的“入室盗窃”每一步都有明确的目的。我们将假设目标是一个存在数字型注入漏洞的新闻网站文章详情页URL形如http://target.com/news.php?id1。2.1 第一步侦察与探测——确认漏洞与列数攻击始于最细微的观察。攻击者首先会尝试修改id参数观察页面变化。基础测试输入id1 and 11页面正常显示id为1的文章。输入id1 and 12这是一个永假条件如果页面变成空白、报错或显示“文章不存在”则强烈暗示参数被代入SQL逻辑运算存在注入可能。对于数字型注入可能更简单直接尝试id1如果报语法错误则说明存在字符型注入参数被引号包裹。判断注入类型与闭合方式假设输入id1后页面报错You have an error in your SQL syntax...这告诉我们两个信息存在SQL注入id参数很可能是被单引号包裹的字符型。为了修复语法并注释掉后续部分我们尝试id1 ----在URL中相当于SQL的--注释号在URL编码中代表空格。如果页面恢复正常则确认了注入点和闭合方式。使用ORDER BY探测列数 这是最关键的一步。攻击者需要精确知道原始查询返回多少列。/news.php?id1 ORDER BY 1 -- (正常) /news.php?id1 ORDER BY 2 -- (正常) /news.php?id1 ORDER BY 3 -- (正常) /news.php?id1 ORDER BY 4 -- (报错Unknown column 4 in order clause)当ORDER BY 4报错时说明原始查询只有3列。至此侦查阶段完成。实操心得在实际测试中ORDER BY的报错信息可能被应用程序屏蔽页面只是空白或跳转。这时需要依靠“布尔状态”来判断。可以对比ORDER BY 3和ORDER BY 4时页面内容的细微差别比如某个HTML元素是否存在、页面标题是否不同。自动化工具如sqlmap就是通过比对大量请求的响应差异来智能判断列数的。2.2 第二步火力准备——验证Union并定位回显点知道有3列后就可以尝试Union查询了。验证Union可行性/news.php?id1 UNION SELECT 1,2,3 --提交这个请求。如果页面报错例如提示“UNION语句列数不匹配”或“数据类型不兼容”可能意味着列数判断有误或者数据库对Union有特殊限制某些情况下需要处理NULL值。如果页面正常显示即使看起来有点奇怪比如出现了数字1,2,3则说明Union成功。让原始查询失效凸显我们的Payload 通常我们会希望页面只显示我们Union查询的结果这样更清晰。因此将id设置为一个不存在的值让第一个SELECT结果为空。/news.php?id-1 UNION SELECT 1,2,3 --或者使用能导致原始查询无结果的条件如id1 and 12 UNION SELECT 1,2,3 --。定位回显点 访问上面的URL然后仔细查看页面源代码。在页面上寻找数字“1”、“2”、“3”出现的位置。它们可能出现在title标签内页面标题h1或h2标签内文章标题某个div classauthor内作者信息某个div classcontent内文章正文甚至可能隐藏在HTML注释!-- --里或者作为某个表单的隐藏输入值input typehidden value2。假设我们发现数字“2”显示在作者名的位置数字“3”显示在文章正文的位置。那么位置2和位置3就是我们可以用来输出数据库信息的“屏幕”。2.3 第三步情报收集——获取数据库元信息在窃取业务数据之前攻击者需要先摸清数据库的“地形图”。这通过查询数据库的元信息表如MySQL的information_schema来实现。获取当前数据库名/news.php?id-1 UNION SELECT 1, database(), 3 --此时页面作者名位置将显示当前连接使用的数据库名称例如news_db。获取所有表名 知道数据库名后就可以查询该数据库下有哪些表。这里通常使用group_concat()函数将多行结果合并成一行字符串方便显示。/news.php?id-1 UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schema database() --执行后可能在正文位置看到一长串表名如articles,users,config,admin_log...。攻击者的目光会立刻锁定users、admin这类敏感表。获取指定表的所有列名 假设我们对users表感兴趣。/news.php?id-1 UNION SELECT 1, group_concat(column_name), 3 FROM information_schema.columns WHERE table_schema database() AND table_name users --执行后可能会得到id,username,password,email,phone,create_time这样的结果。password和email字段成为重点目标。2.4 第四步终极目标——拖取敏感数据现在攻击者已经知道了数据库名news_db、表名users、列名username, password。最后一步就是直接提取数据。一次性提取所有用户数据 为了高效攻击者会尽量一次查询获取多行数据。同样使用group_concat()函数并常用concat_ws()来格式化输出方便区分不同记录。/news.php?id-1 UNION SELECT 1, group_concat(concat_ws(:, username, password)), 3 FROM news_db.users --这个查询会将users表中所有行的username和password用冒号连接起来然后所有行再合并成一个字符串。回显结果可能类似admin:7a57a5a743894a0e, user1:202cb962ac59075b, user2:250cf8b51c773f3a...处理数据量过大问题 如果表数据太多group_concat()可能有长度限制导致结果被截断。这时攻击者会使用limit子句分批次窃取。/news.php?id-1 UNION SELECT 1, concat_ws(:, username, password), 3 FROM news_db.users LIMIT 0,1 --然后依次修改LIMIT 1,1、LIMIT 2,1来遍历所有数据。至此一次完整的、基于Union联合查询的SQL注入攻击就完成了。从发现漏洞到拖走整个用户表攻击链清晰而致命。注意事项以上演示基于MySQL数据库。不同数据库如 PostgreSQL、Microsoft SQL Server、Oracle的系统表名、函数名有差异。例如在SQL Server中系统视图是sys.tables和sys.columns在Oracle中是all_tables和all_tab_columns。攻击者需要根据报错信息或经验判断数据库类型并调整Payload。3. 绕过防御与高级利用技巧随着开发者安全意识的提升单纯的Union注入漏洞已不如十年前常见但远未绝迹。而且攻击者为了利用那些被部分防御措施保护的漏洞发展出了多种绕过技巧。3.1 应对常见过滤与WAFWeb应用防火墙许多应用会尝试过滤一些敏感关键词如union、select、information_schema等。此外云WAF或硬件WAF也会检测这些特征。攻击者会采用以下方法尝试绕过大小写混淆UnIoN SeLeCt。一些简单的基于纯字符串匹配的过滤可能失效。双写关键字uniunionon seselectlect。如果过滤逻辑是简单地删除关键词例如将出现的union替换为空那么uniunionon在删除中间的union后剩下的部分正好拼成union。使用注释符分割关键字u/**/nion sel/**/ect。在SQL中/**/是多行注释但在很多解析器中它会被忽略。这可以绕过对连续关键词的检测。使用等价函数或语法替换替换information_schema.tables在MySQL中可以通过sys.schema_table_statistics或mysql.innodb_table_stats等替代视图来获取表信息需要相应权限。替换database()可以用datadir结合字符串截取来推断数据库名难度较大。编码与十六进制将敏感词转换成十六进制。例如select的十六进制是0x73656c656374。Payload可以写成union 0x73656c656374 1,2,3。或者对整段Payload进行URL编码、Unicode编码等。3.2 处理非常规回显与盲注结合有时即使Union注入成功查询结果也不一定直接显示在页面上。可能只显示第一行数据或者结果被用于程序逻辑判断而不输出。这时需要结合其他技巧。仅显示第一行数据如果页面只取结果集的第一行显示那么我们需要确保我们注入查询的结果位于第一行。这就是为什么之前要把原查询的id设为-1或使其不返回结果。如果不行可以尝试用limit控制或者使用聚合函数如max(),min()将多行数据压缩到一行。UNION SELECT 1, (SELECT group_concat(username) FROM users), 3这里子查询返回多行但通过group_concat合并成一行作为第二列的一个值返回。Union盲注在无法直接看到数据回显但页面会根据查询结果是否为空而有不同状态如HTTP状态码、页面内容长度不同时可以结合Union与布尔逻辑进行盲注。例如id1 AND (SELECT substring(database(),1,1)a) UNION SELECT 1,2,3 --如果数据库名第一个字母是a则AND条件为真页面正常执行Union可能显示2,3如果不是aAND条件为假第一个SELECT无结果Union后的结果可能不同。通过这种差异来逐位推断数据。虽然效率远低于直接回显但在严格受限的环境下是唯一途径。3.3 利用数据类型兼容性进行攻击UNION要求列的数据类型兼容。攻击者可以利用这一点来获取信息。例如如果某列是字符串类型但攻击者注入了一个数字数据库可能会尝试隐式转换。通过观察转换成功或失败引发的错误信息错误型注入有时能泄露数据。更高级的技巧是如果知道某列是整数型但想通过它输出字符串信息如version可以使用concat()函数将其与数字拼接或先转换成字符串。例如在需要整数的地方使用version会导致错误但使用concat(1, version)则可能成功并将版本信息作为字符串输出到整数列在页面上显示出来。4. 防御之道从开发到运维的全链路防护理解了攻击才能更好地防御。面对Union注入这种直接而危险的攻击方式防御必须多层次、全方位。4.1 根本解决方案使用参数化查询预编译语句这是唯一被公认为能从根本上防止SQL注入的方法。其原理是将SQL语句的结构模板与数据参数分开发送和解析。错误做法拼接字符串# Python危险示例 query SELECT * FROM articles WHERE id user_input_id cursor.execute(query)正确做法参数化查询# Python安全示例使用DB-API的parameter query SELECT * FROM articles WHERE id %s cursor.execute(query, (user_input_id,))// Java安全示例使用PreparedStatement String sql SELECT * FROM articles WHERE id ?; PreparedStatement pstmt connection.prepareStatement(sql); pstmt.setInt(1, Integer.parseInt(userInputId)); ResultSet rs pstmt.executeQuery();在这个例子中无论user_input_id传入的是1还是1 UNION SELECT 1,2,3 --数据库引擎都会将其始终视为一个整体的字符串或数字参数而不会将其解析为SQL语法的一部分。UNION、SELECT这些关键词在这里只是参数值里的普通字符不会被执行。这就彻底切断了注入的可能性。实操心得务必在项目中全面使用参数化查询接口。无论是哪种编程语言Java的PreparedStatement、Python的DB-API%s/?、PHP的PDObindParam、.NET的SqlParameter其核心思想都是一样的让数据库先编译好SQL语句结构然后再传入数据。注意存储过程如果使用动态SQL拼接同样存在注入风险并非绝对安全。4.2 辅助防御措施在参数化查询的基础上可以叠加其他防御层形成纵深防御。输入验证与过滤白名单验证对于已知有限集合的输入如状态值、类型值使用白名单。例如id参数如果只能是数字那么在代码层面就强制验证其为整数。类型强制转换在接收参数时立即进行类型转换。int id Integer.parseInt(request.getParameter(id))如果转换失败则直接拒绝请求。谨慎使用过滤黑名单过滤过滤union、select等关键词很容易被绕过不应作为主要防御手段。但可以作为一种补充过滤掉一些明显的恶意字符或过长的输入。最小权限原则为Web应用连接数据库的账户分配最小必要权限。绝对不要使用root或sa等超级管理员账户。只授予其对业务所需表的SELECT、INSERT、UPDATE、DELETE权限并严格限制其对information_schema、系统存储过程等的访问。这样即使发生注入攻击者也无法通过UNION SELECT读取其他数据库的信息或执行DROP TABLE、LOAD_FILE等危险操作能将损失控制在有限范围内。错误信息处理永远不要将详细的数据库错误信息直接返回给前端用户。这些信息如MySQL错误、表名、列名是攻击者构造Payload的宝贵线索。在生产环境中应配置自定义的错误页面只向用户返回友好的通用错误提示如“服务器内部错误”同时将详细的错误日志记录到服务器后台供管理员排查。使用Web应用防火墙WAFWAF可以作为一道网络层面的屏障基于规则库识别和拦截常见的SQL注入攻击模式包括各种绕过的变形。但WAF不是万能的它可能被新型攻击手法绕过也可能产生误报。它应该被视为安全体系中的一道“减速带”或“警报器”而非最终的城墙。4.3 安全开发流程与代码审计防御注入不能只靠运维和工具更要从源头抓起。安全编码规范在团队内强制推行使用参数化查询的规范并在代码审查Code Review中将其作为重点检查项。任何出现字符串拼接SQL的地方都必须给出合理解释。定期安全测试渗透测试邀请专业的安全团队或使用自动化工具如sqlmap、Burp Suite的Scanner对应用进行定期扫描和手动测试主动发现潜在的注入点。代码审计使用静态代码分析工具SAST扫描代码库自动识别可能存在SQL拼接风险的代码段。框架的安全使用现代开发框架如MyBatis、Hibernate、Entity Framework都提供了安全的查询方式。MyBatis务必使用#{}占位符会进行预编译严禁在动态SQL中不当使用${}会直接拼接字符串。!-- 安全 -- select idgetUser resultTypeUser SELECT * FROM user WHERE id #{id} /select !-- 危险存在注入风险 -- select idgetUser resultTypeUser SELECT * FROM user ORDER BY ${orderBy} /select如果orderBy参数用户可控此处就存在注入风险。对于排序字段应使用白名单验证。Hibernate/ JPA使用createQuery并设置参数或使用Criteria API避免拼接HQL/JPQL。5. 从攻击者视角看自动化工具与手动测试的博弈在真实世界中无论是恶意攻击还是授权的渗透测试Union注入的利用很大程度上依赖于自动化工具其中最著名的就是sqlmap。理解工具如何工作能帮助我们更好地防御。5.1 Sqlmap如何自动化实现Union注入当你给sqlmap一个可能存在注入的URL时它会执行一系列高度智能化的步骤检测注入点与数据库类型它首先会发送大量精心构造的测试Payload通过分析响应差异布尔盲注、错误信息错误型注入、时间延迟时间盲注来判断是否存在注入并精准识别数据库类型MySQL、MSSQL、Oracle等。探测列数它会自动使用ORDER BY技术通过二分查找等算法快速确定列数。判断回显点它会自动使用类似UNION SELECT NULL,NULL,NULL...的Payload然后替换其中的NULL为随机字符串观察哪个位置出现了该字符串从而确定哪些列可用于回显数据。提取数据一旦确认Union注入可行并找到回显点它就会像我们手动操作一样自动化地查询information_schema列出数据库、表、列然后批量拖取数据。它还能自动处理group_concat长度限制分块获取数据。绕过技巧集成sqlmap内置了庞大的“篡改脚本”tamper script库可以自动对Payload进行编码、混淆、分割以绕过常见的WAF和过滤规则。例如使用space2comment脚本将空格替换为注释符/**/。5.2 手动测试的不可替代性尽管sqlmap强大但手动测试在以下场景中不可或缺复杂业务逻辑注入点可能隐藏在复杂的JSON请求体、HTTP头部如Cookie、User-Agent、或者经过前端加密/编码的参数中。sqlmap可能无法自动识别和测试这些点需要手动抓包、修改重放。二阶注入攻击者将恶意Payload先存入数据库例如在注册用户名时输入admin --当应用程序后续从数据库取出该数据并拼接成新的SQL语句时触发注入。这种注入无法通过直接测试输入点发现需要手动分析整个业务流程。WAF/防御规则深度绕过面对定制化程度高的WAF或诡异的过滤逻辑sqlmap的通用篡改脚本可能失效。这时需要手动分析拦截规则构造极其特殊的Payload。例如利用数据库特性、冷门函数或非常规语法。验证与精准利用自动化工具可能会误报。手动测试可以最终确认漏洞的真实存在性和危害程度。在渗透测试报告中一个手动验证并成功利用的Union注入漏洞其严重性评级和说服力远高于工具扫描结果。作为防御方一个重要的心得是不要以为上了WAF或做了简单过滤就高枕无忧。定期使用sqlmap等工具对自己系统进行扫描是一个很好的习惯它能帮你发现那些因代码疏忽或框架误用而产生的、意想不到的注入点。同时要意识到最顶尖的攻击者会进行手动测试因此根本性的参数化查询和最小权限原则才是最终的依靠。Union联合查询注入作为SQL注入中最具代表性的“直接回显”攻击方式清晰地展示了当用户输入被误信任为代码时应用程序所面临的巨大风险。从看似无害的union select 1,2,3开始到整个数据库被拖库攻击路径直接而高效。对于开发者而言牢记“数据与代码分离”的原则严格使用参数化查询是关闭这扇危险之门的唯一钥匙。对于安全人员深入理解其原理和绕过技巧则是发现和修复漏洞的必备能力。在这个数据即价值的时代SQL注入防御的每一分投入都是在守护企业和用户最核心的资产。