资讯动态

SQL注入进阶:二次注入与无列名注入实战解析

发布时间:2026/8/7 3:54:31 来源:尧图企业网站定制
1. 项目概述与核心挑战最近在复盘一些经典的CTF Web题目BUUCTF平台上的[SWPU2019]Web1这道题给我留下了挺深的印象。它不像那些直接给个输入框让你怼‘ or 11--的入门题而是把SQL注入玩出了点新花样核心考察点在于对“二次注入”和“无列名注入”这两种进阶手法的理解和灵活运用。很多朋友在做到这一步时容易被题目看似正常的交互流程迷惑或者卡在不知道如何在没有明确列名的情况下爆出数据。今天我就结合当时的解题思路把这题的完整链条拆解清楚从信息搜集、漏洞定位到最终的Payload构造和Flag获取一步步带你走通。无论你是刚接触Web安全的新手还是想巩固一下SQL注入的各类姿势这篇复盘都应该能给你带来一些直接的参考。简单来说这道题模拟了一个带有广告发布和查看功能的Web应用。你的目标就是利用这个应用存在的SQL注入漏洞从数据库中挖出隐藏的Flag。它巧妙地设置了两道关卡第一你需要发现一个存储型二次注入点将恶意代码“寄存”到数据库里第二当恶意代码被触发时你面临的是一个无法直接得知表名和列名的环境需要用到“无列名注入”技术来一步步推导出数据。整个过程就像侦探破案环环相扣非常考验对SQL注入原理的深入理解和对各种绕过技巧的熟练度。2. 环境初探与功能点分析拿到题目第一件事永远是信息搜集。访问靶机地址我们通常会看到一个相对简洁的Web界面。以[SWPU2019]Web1为例典型的界面可能包含以下几个功能模块首页/广告展示页一个列出所有广告的页面可能显示广告标题、内容等。广告发布页面一个表单允许用户输入广告标题和广告内容并提交。广告详情页点击某个广告标题后跳转到展示该广告详细内容的页面。我们的操作流程一般是这样先在发布页面提交一个广告然后回到首页看到广告列表再点击自己发布的广告标题查看详情。这里就存在第一个需要留意的点数据流向。你提交的广告标题和内容被存储到了数据库当你在首页点击标题时应用很可能是通过广告的ID或标题本身去数据库查询然后渲染出详情页。这个“存储-再调用”的过程就是二次注入可能发生的温床。接下来我会用简单的测试来探测每个输入点。比如在发布广告的标题和内容框里尝试输入一些特殊字符单引号‘、双引号“、括号()、反斜杠\等。观察页面的反应是直接报出数据库错误说明可能存在注入还是过滤了某些字符或是毫无反应地正常显示这一步的初步探测非常关键它能帮你快速定位哪个参数可能存在问题。注意在实战CTF或授权测试中这种探测要控制频率和Payload的“攻击性”避免使用sleep()等可能对靶机造成压力的函数进行盲注优先使用基于报错的探测语句。在我的测试中我发现当在广告发布的“标题”字段输入一个单引号‘并提交后页面有时会正常跳转没有立即报错。但是当我随后在首页点击这个标题为单引号的广告时详情页却加载异常甚至直接显示了数据库的报错信息例如You have an error in your SQL syntax...。这个现象强烈暗示标题字段在存入数据库时未被充分过滤但在从数据库取出并用于后续查询时引发了SQL语法错误。这正是“二次注入”的典型特征恶意数据像“定时炸弹”一样先被存进去在另一个逻辑环节被引爆。3. 漏洞原理深度剖析二次注入与无列名注入3.1 二次注入Second-Order SQL Injection原理为什么叫“二次”因为它需要两次数据库操作才能完成注入攻击。第一次操作存储阶段攻击者将包含SQL注入代码的输入比如‘提交给应用程序。应用程序可能对输入进行了一些转义或处理例如调用了addslashes()或mysql_real_escape_string()使得这个输入在本次插入语句中是安全的被当作普通字符串存入了数据库。例如存储的标题变成了‘带转义可能变成\但存储的仍是单引号本身。第二次操作触发阶段在另一个功能点如查看详情应用程序从数据库中取出之前存储的这个数据并未经再次过滤或转义直接拼接到了一个新的SQL查询语句中。这时当初存入的那个单引号就脱离了字符串的上下文成为了新SQL语句的一部分从而闭合了字符串引入了注入。举个例子假设查看广告详情的后端代码逻辑如下以PHP为例// 从数据库取出广告标题假设从$_GET[‘id’]获取广告ID然后查询出标题 $title $row_from_db[‘title’]; // 然后可能用这个标题去做其他查询比如查询相关评论题目逻辑可能不同 $sql “SELECT * FROM another_table WHERE related_title ‘“ . $title . “‘“;如果$title是从数据库取出的我们之前存入的‘那么拼接后的SQL语句就变成了SELECT * FROM another_table WHERE related_title ‘‘‘这显然多了一个单引号会造成语法错误。如果我们存入的不是一个简单的‘而是一段精心构造的Payload比如‘ UNION SELECT ... --那么就能执行我们想要的查询了。3.2 无列名注入No-Column Name Injection原理在常规的SQL注入中我们通过UNION SELECT联合查询来获取数据时需要知道要查询的表名和列名。例如UNION SELECT 1, username, password FROM users。但有些情况下我们无法通过information_schema数据库元信息库来获取这些名字可能因为权限限制、information_schema被过滤或题目故意设计如此。这时“无列名注入”就派上用场了。它的核心思想是利用已知的列数通过给列起别名Alias或者利用子查询Subquery来绕过对列名的直接依赖逐步推导出数据内容。常见的手法有利用JOIN进行列名爆破不适用于本题所有情况但是一种思路通过UNION与自身表JOIN制造错误信息来泄露列名但本题环境可能不报错。使用数字或别名进行查询在UNION SELECT中我们可以用数字、字符串或者子查询来代替列名。关键是要保证前后查询的列数一致。通过子查询和别名逐位获取数据核心方法这是本题最可能用到的方法。我们可以构造这样的PayloadUNION SELECT 1, (SELECT 2 FROM (SELECT 1, 2 UNION SELECT * FROM target_table LIMIT 1 OFFSET 0) a), 3, 4...这个语句有点绕我来拆解一下最内层的SELECT 1, 2创建了一个虚拟表有两列值分别是1和2。我们给它起个别名比如a。UNION SELECT * FROM target_table将目标表的数据联合到这个虚拟表上。这里有个关键前提target_table的列数必须和SELECT 1, 2的列数相同这里是2列否则UNION会失败。我们可以通过ORDER BY来先测出列数。LIMIT 1 OFFSET 0表示取目标表的第一行数据。外层的SELECT2FROM ... a意思是从上面这个联合结果集别名a中选择第2列的数据。注意这里的2是反引号包裹的数字2它在这里不是数字而是列的别名。因为内层SELECT 1, 2语句给第二列起了个默认别名2。所以这个外层查询最终返回的就是目标表第一行、第二列的数据值。通过改变OFFSET的值0, 1, 2...我们可以遍历目标表的每一行。通过改变外层SELECTx中的x1, 2, 3...我们可以遍历每一列。这种方法完全不需要知道目标表具体的列名是什么比如id,username只需要知道列数然后通过列的位置别名1,2,3...来引用它们。4. 实战解题步骤拆解理解了原理我们开始动手。假设靶机地址是http://node4.buuoj.cn:2xxxx/。4.1 第一步确认二次注入点与列数探测发布广告在广告发布页面标题Title输入探测Payload‘。内容Content可以随意填比如test。提交。触发错误回到首页找到你刚刚发布的那个广告标题可能显示为‘或乱码点击它进入详情页。此时如果页面出现SQL语法错误就证实了标题字段存在二次注入并且触发点在查看详情功能。判断列数我们需要知道在触发注入的那个SQL查询中SELECT语句查询了多少列以便后续使用UNION。使用经典的ORDER BY方法但这次Payload需要被“存储”后再触发。发布新广告标题输入‘ ORDER BY 1 --。注意--后面有个空格在SQL中表示注释。提交。点击该广告查看详情。如果页面正常说明ORDER BY 1执行成功即原查询至少有一列。继续尝试发布标题为‘ ORDER BY 2 --、‘ ORDER BY 3 --……的广告并依次点击查看。当页面出现错误时例如Unknown column ‘4‘ in ‘order clause‘就说明超出了列数。假设ORDER BY 5出错ORDER BY 4正常那么原查询的列数就是4。实操心得在构造这类存储型Payload时要特别注意空格和注释符的处理。有些题目会过滤空格我们可以用/**/MySQL注释符来代替。注释符--空格很重要或#用于注释掉原查询后面的部分防止语法错误。在这个题目中经过测试使用--是有效的。4.2 第二步定位回显点与数据库信息搜集知道列数假设为4后我们需要用UNION SELECT来确认哪几列的数据会显示在页面上即可见回显点。构造联合查询发布广告标题输入‘ UNION SELECT 1,2,3,4 --。提交。查看回显点击该广告。如果页面正常显示并且原本显示广告标题、内容或其他信息的地方出现了数字1、2、3、4中的某一个或几个那就找到了回显点。假设数字2和3的位置显示了内容。现在我们就可以把2和3的位置替换成我们想查询的信息函数。 3.查询基础信息 * 发布广告标题输入‘ UNION SELECT 1,database(),version(),4 --。这里database()显示当前数据库名version()显示数据库版本。提交后点击查看。 * 从回显点2和3的位置我们可能看到数据库名比如web1和版本信息。4.3 第三步无列名注入获取Flag到了最关键也是最难的一步。我们通常需要先知道表名、列名但题目可能过滤了information_schema或者像本题一样设计了一个我们不知道列名的表来存放Flag。我们就需要使用前面讲的无列名注入技术。猜测或获取表名。虽然可能是无列名但表名有时可以通过其他方式猜测如flagsecrethere_is_flag或者题目描述给出提示。如果information_schema可用可以先查‘ UNION SELECT 1,group_concat(table_name),3,4 FROM information_schema.tables WHERE table_schemadatabase() --假设我们查到有个表叫flag。确定目标表列数。为了对flag表使用无列名注入我们必须知道它有多少列。我们可以用ORDER BY来试探这个表需要将其作为子查询‘ UNION SELECT 1,(SELECT 1 FROM (SELECT * FROM flag ORDER BY 1) a),3,4 --逐渐增加ORDER BY的数字直到报错。假设测试出flag表有2列。这个步骤可能需要在Payload中嵌套子查询构造时需要仔细。构造无列名注入Payload获取数据。现在我们知道flag表有2列我们想取出它的所有数据。我们可以构造如下Payload‘ UNION SELECT 1, (SELECT GROUP_CONCAT(1, ‘:‘, 2) FROM (SELECT * FROM flag) a), 3, 4 --逐层拆解这个PayloadSELECT * FROM flag这是最内层取出flag表的所有数据。假设表里只有一行两列数据是(1, ‘flag{this_is_test_flag}‘)。(SELECT * FROM flag) a给这个结果集起个别名a。SELECT GROUP_CONCAT(1, ‘:‘,2) FROM (SELECT * FROM flag) a从别名a的结果集中选择第1列用反引号包裹的别名1和第2列别名2用冒号连接起来。GROUP_CONCAT函数将所有行合并成一个字符串如果有多行数据的话。最后整个子查询的结果也就是拼接好的字符串1:flag{this_is_test_flag}会显示在页面的第2个回显点上。获取Flag。发布标题为上述Payload的广告点击查看。在回显位置你应该能看到类似1:flag{xxxxxx-xxxx-xxxx}的内容冒号后面的部分就是梦寐以求的Flag。避坑指南在构造这种多层嵌套的子查询时MySQL可能会报错例如“Every derived table must have its own alias”每个派生表都必须有自己的别名。这就是为什么我们在子查询外面一定要加a、b这样的别名。另外反引号在MySQL中用于引用列名或表名特别是当名称是数字或关键字时在这里它引用的是我们通过SELECT 1,2...创建的虚拟列的别名。5. 常见问题与排查技巧实录在实际操作中你可能会遇到各种各样的问题。下面我整理了几个常见的情况和解决思路问题现象可能原因排查与解决思路发布含Payload的广告后首页不显示或点击无反应1. Payload包含特殊字符被前端或后端拦截。2. 插入数据库时发生错误导致记录未成功写入。1. 尝试使用URL编码后的Payload如单引号‘用%27代替空格用%20或。2. 简化Payload先测试最基本的‘是否成功存储和触发。ORDER BY测试列数时数字很大了还不报错1. 注入点不在ORDER BY子句附近可能在其他位置如WHERE。2. 应用程序对错误进行了屏蔽不显示具体错误信息。1. 尝试使用基于布尔True/False的盲注Payload来测列数例如‘ AND (SELECT 1)1 --和‘ AND (SELECT 1)2 --观察页面内容差异。2. 使用UNION SELECT NULL,NULL,...来测试直到页面正常回显NULL的个数即为列数。UNION SELECT后页面空白或报错“列数不匹配”1. 列数判断错误。2.UNION前后字段类型不兼容。1. 重新用ORDER BY或UNION SELECT NULL...精确判断列数。2. 在UNION SELECT中尝试用NULL代替数字因为NULL可匹配任何类型。无列名注入Payload执行报语法错误1. 别名缺失或错误。2. 反引号使用错误或数据库不支持。3. 子查询返回多行但外层期望单行。1. 确保每个派生表由子查询产生的临时表都赋予了别名如a,b。2. 检查反引号是否为英文半角。在某些数据库或模式下可以用a.1或1 as col1的形式。3. 使用LIMIT 1来确保子查询只返回一行或者使用能聚合多行的函数如GROUP_CONCAT()。知道表名flag但无列名注入查不出数据1. 表名猜错。2.flag表列数判断错误。3. Flag不在第一列或第二列需要遍历。1. 尝试其他常见表名如flags,secret,hint等。2. 重新确认flag表的列数。可以构造SELECT * FROM flag UNION SELECT 1,2,3...来测试直到不报错UNION需要的列数就是flag的列数。3. 修改无列名Payload中的 1 和 2 尝试 1 、 2 、 3 ... 或者使用SELECT GROUP_CONCAT(1, ‘-‘,2, ‘-‘,3...)来同时查看多列。个人经验分享在做这类题目时养成“步步为营及时验证”的习惯非常重要。每构造一个Payload都要想清楚它的预期结果是什么。如果结果不符就拆解Payload从最内层的子查询开始分段测试。可以先用简单的SELECT ‘test‘来验证注入点是否可用再逐步增加复杂度。另外浏览器的开发者工具F12中的“网络”Network选项卡是你的好朋友查看请求和响应有时错误信息或关键数据会藏在HTTP响应头或JSON数据里而不是直接显示在页面上。6. 防御思路与总结反思从这道题目中我们作为开发者也能学到如何避免此类漏洞永远不要信任用户输入这不仅是安全口号更是铁律。无论是直接输入还是从数据库取出的“已存储”数据在将其用于拼接SQL语句前都必须进行严格的检查和过滤。使用参数化查询预编译语句这是防止SQL注入最有效、最根本的方法。使用像PDOPHP、PreparedStatementJava这样的数据库接口让SQL语句的“结构”和“数据”分离数据库引擎会严格区分它们恶意代码就无法改变查询结构。最小化数据库权限给Web应用使用的数据库账户分配最小必要的权限比如只有SELECT、INSERT没有DROP、FILE等这样即使发生注入危害也能被限制。对输出进行编码即便数据安全地存入了数据库在渲染到前端页面时也要进行HTML编码等处理防止XSS等二次攻击。回过头看[SWPU2019]Web1这道题它是一道非常好的、将二次注入和无列名注入结合在一起的综合练习题。它告诉我们安全漏洞可能隐藏在不那么直接的数据流里而绕过信息缺失的限制则需要更深入的理解和更巧妙的技巧。攻克它的过程本身就是对SQL注入知识体系的一次巩固和提升。下次再遇到存储点触发点分离的场景或者看不到information_schema的情况你就能更快地联想到这些思路了。

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

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

免费获取报价