1. 为什么这道题值得作为SQL注入的入门第一课说实话SQL注入这个方向很多新手一开始都是懵的。看了不少文章知道有字符型、数字型、报错注入、盲注、堆叠注入这些名词但一打开靶场就不知道鼠标该往哪点。我见过太多人卡在第一步不是不懂原理而是没有一个足够“温柔”的环境让他把整个流程走通。BUU SQL COURSE 1就是这样一个环境。它是BUUCTF平台上一道非常经典的入门级SQL注入题题目本身不复杂但它把联合查询注入这条链路设计得非常完整从判断注入点、猜解列数、确认回显位置到查库名、查表名、查字段名、拿flag每一步都有清晰的反馈。换句话说这道题不是让你“看会”注入而是让你“亲手走一遍”注入。我当时做这道题的时候印象最深的一点是它没有任何花里胡哨的过滤和绕过所有请求都老老实实地交到后端拼进SQL语句里你输入的每一个单引号、每一段UNION查询都会得到真实的数据库响应。这种“所见即所得”的特性对建立直觉特别有帮助。因为注入这个事儿本质上就是“你猜后台怎么拼SQL然后想办法让拼接结果符合你的意图”。这道题没有额外干扰后台拼SQL的方式就是最常见、最典型的写法你在这个环境里建立起来的分析思路放到别的题目里照样能用。另外这道题涉及的知识点其实覆盖了SQL注入里最核心的一整块拼图怎么判断注入类型为什么用ORDER BY试列数UNION SELECT的字段数为什么要和原查询一致怎么用系统库 INFORMATION_SCHEMA 查元数据等等。把这些东西吃透后面再碰堆叠注入、报错注入、布尔盲注都会顺很多。所以这篇东西我打算不按“题解”的套路来写而是按“一个新手从零开始应该怎么思考和操作”的顺序来写。每一个步骤我都会讲清楚两个问题这么做的目的是什么这么做的原理是什么。这样你做完这道题之后得到的不是一份答案而是一套可以迁移到其他靶场和题目上的方法论。2. 前置侦察目标环境的第一轮试探做题的第一步不是急着上payload而是先搞清楚题目给了什么、入口在哪、参数是什么。这个习惯越早养成越好因为真实场景下大部分攻击面都是从“多看了一眼”里发现的。2.1 题目入口与参数定位打开题目你会看到一个典型的SQL课程练习页面大致功能是“输入一个编号查询对应的课程信息”。页面上有个输入框输入数字比如1会返回一条课程记录输入2返回另一条。这个交互非常直白但恰恰是这种直白背后藏着整个漏洞的核心用户输入被直接拼进了SQL查询语句。先别急着动手用浏览器开发者工具F12看一下请求是怎么发的。你会发现数据是通过GET请求提交的URL里有一个明显的id参数例如http://目标地址/?id1这一步看着不起眼但它确认了一件很重要的事后端大概率是这样拼SQL的SELECT 字段列表 FROM 表 WHERE id 用户输入也可能是数字型不带引号SELECT 字段列表 FROM 表 WHERE id 用户输入到底是哪一种就是接下来要测试的第一个问题。这个判断会直接影响你后面构造payload的方式——带引号的字符型要想办法闭合引号不带引号的数字型直接拼接数字运算或UNION查询就行。2.2 单引号试探法与报错信息的价值拿到参数之后第一个payload永远是最经典的单引号?id1输入之后页面大概率会报错。这里要特别提醒一下报错不是坏事反而是天大的好事。你可以从报错信息里读出很多后端细节——数据库类型、SQL语句的拼接方式、有没有做过滤。如果页面把SQL错误信息直接回显出来那这个题目就非常“友好”了。我当时实操的时候输入单引号后页面直接吐出了一段SQL语法错误提示大意是“在 xxx 附近有语法错误”。这个信息至少确认了三件事后端确实是直接把参数拼进SQL的存在注入点数据库是SQL Server系的不同数据库的报错格式有差异注入点是字符型因为语句里出现了单引号闭合的痕迹。不过要注意不是所有环境都会回显这么明确的报错。很多生产环境会屏蔽详细报错只给你一个500或者空页面。但靶场环境通常不会做得那么绝因为它的目的是教新手“看到”注入过程。所以在这个题里报错信息就是你的第一份情报。2.3 判断注入类型的两个关键测试确认了注入点存在之后下一步是精确判断“字符型”还是“数字型”。这一步特别重要因为很多人一开始就把类型判断错了后面所有payload都不生效然后在错误的方向上浪费了大量时间。最简单的判断方法是输入一个让恒等式成立的payload观察页面表现是否和正常查询一致。先测数字型。输入?id1 AND 11如果页面正常返回id为1的记录再输入?id1 AND 12如果页面返回空说明这个位置是数字型注入因为12是假条件直接把整个查询结果过滤掉了。数字型注入的特点就在这里你输入的东西是直接跟数字做算术和逻辑运算的。再测字符型。输入?id1 AND 11页面如果正常返回再输入?id1 AND 12页面如果返回空说明是字符型注入。这里的原理是后台拼出来的SQL大概长这样SELECT ... WHERE id 1 AND 11你看我们输入的内容把整个WHERE条件“闭合”了原本的id 1变成了id 1 AND 11恒为真。当条件改成12时恒为假查询结果自然为空。回到BUU SQL COURSE 1这个题目实测下来它是一个字符型注入也就是说后台拼接SQL时用的是单引号包裹参数大概长这样SELECT ... FROM course WHERE id 用户输入知道了这一点后面所有构造payload的思路就清晰了核心就一句话想办法把前端的单引号闭合掉然后把你自己的查询逻辑拼接到原本的SQL语句后面。3. 联合查询注入的完整链路从猜列数到拿flag确认了字符型注入点之后接下来的操作就是SQL注入里最经典的“联合查询注入”UNION-based injection。这一节是整个题目的高潮部分我会按顺序拆解每一步并解释每一步背后的原理。3.1 用ORDER BY试出原查询的字段数联合查询有一个铁律UNION SELECT出来的字段数必须和原查询的字段数一致否则数据库会报错。所以第一个要做的事情就是搞清楚原查询到底select了几个字段。最常用的方法就是ORDER BY。原理很简单ORDER BY 1表示按第1列排序ORDER BY 2按第2列排序如果指定的列数超过了实际列数数据库就直接报错。所以我们可以通过不断增大ORDER BY后面的数字测试出边界。在字符型注入下payload长这样?id1 ORDER BY 1-- ?id1 ORDER BY 2-- ?id1 ORDER BY 3--这里的--是SQL里的注释符作用是吞掉原本id 1后面的那个单引号。在MySQL里--后面必须跟一个空格URL编码后就是--当然靶场环境有时候也接受#但我习惯用--因为兼容性更好。SQL Server对注释符的要求不太一样但在这个环境里实测--是可以用的Burp Suite和浏览器URL栏里都试过没毛病。实际操作时我一般从1开始往上试也可以直接二分猜。试到ORDER BY 2正常ORDER BY 3报错就能确定原查询只有两列。这个“报错拐点”就是答案。这一步的关键心得是不要通过“页面是否正常”来判断而要关注“页面是否报错”。在ORDER BY的测试里只要没报错就说明列数还没超继续加。我第一次做的时候看到ORDER BY 3还能正常显示差点以为有3列后来仔细一看才发现返回的行完全不对实际上是数据库报错被页面吞掉了只显示了空白。这里要提醒一下有些环境和题目设计不同会把报错信息隐藏只给你一个空页面。所以在做判断时要同时看“页面是否有正常数据”而不是只看有没有文字。3.2 定位回显位UNION SELECT的数值替换技巧知道了有两列下一步就是确认哪一列会把查询结果显示到页面上。这个“显示位”也叫回显位后面查出来的库名、表名、字段名都要往这个位置上放。先用一个占位符来测试?id1 UNION SELECT 1,2--这里有个细节值得展开说为什么前面要写?id1而不是?id-1甚至有时候用?id9999原因很简单——我们要让原查询的结果为空这样UNION后面的数据才能成为唯一的返回结果。如果你让id1原查询先返回了一条id为1的记录UNION的结果会追加在后面页面上可能显示两条数据虽然也能看出来但不够干净。我当时用的是?id-1因为正常情况下id不可能是负数原查询必然返回空UNION SELECT的结果就是唯一结果。然后页面上的表现是原本显示课程名称的位置出现了“1”原本显示课程编号的位置出现了“2”也可能是反过来。看到这种情况立刻就能锁定回显位第1列、第2列都回显但哪一个列值直接显示成我们输入的内容取决于页面的渲染逻辑。如果页面只显示一个字段那我们就只能把敏感信息放在那个有效回显位上如果两个都显示那就舒服了一次可以查两个数据。这一步的原理是这样的UNION SELECT 1,2中的1和2并不是真的数字常量而是“我们控制的字符串”。页面上回显出“1”说明这个位置就是第1列在页面上渲染的位置。后面我们把1替换成database()、version()、表名、字段名它就会把真实数据渲染出来。这就像你先在一个表格里填了占位符确认了哪一格会被打印出来然后再把真正的数据填进去。3.3 通过系统库查询库名、表名与字段名回显位确认之后真正的信息收集阶段就开始了。先查当前数据库名。SQL Server系和MySQL的语法略有不同但这个题目环境实测支持MySQL风格那么直接?id-1 UNION SELECT 1,database()--页面的第二个回显位会直接输出当前库名正常情况下是一个类似buu_sql_course_1或者ctf这种名字。我当时查出来是一个看起来像课程库的名字无所谓先记下来后面所有表名查询都要用到它。接下来查这个数据库里有哪些表。在MySQL里所有数据库的元信息都存在information_schema.tables这个系统表里。查询语句?id-1 UNION SELECT 1,table_name FROM information_schema.tables WHERE table_schema库名--注意如果库名里有下划线直接单引号包起来就行。这一步返回的table_name就是当前数据库里的所有表名。题目为了简单表名通常不会太多很快就看到一个关键表比如course、flag之类的。我当时看到的是一个名称非常直白的表一眼就知道数据在里面。最后查这个表里有哪些字段?id-1 UNION SELECT 1,column_name FROM information_schema.columns WHERE table_schema库名 AND table_name表名--字段名也是一样清晰明了。通常是类似id、name、flag这种名字。到这里整条链路已经走通了一大半剩下的就是最后一步把数据取出来。3.4 一次性拼接多行数据的高效技巧这里分享一个能省不少时间的小技巧。按照前面的流程查表名时一次UNION SELECT只能返回一行。如果一个库里有好几张表你就得改一次table_name条件查一次效率很低。但实际上我们可以用GROUP_CONCAT把多行数据拼成一行一次性输出全部表名?id-1 UNION SELECT 1,GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema库名--这样页面的回显位上就会一次性输出用逗号分隔的全部表名。查字段名同理?id-1 UNION SELECT 1,GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema库名 AND table_name表名--这个技巧在真实做题和测试中非常实用。尤其是表多、字段多的场景省下的时间不是一点半点。我后来做其他靶场时这个习惯也一直保留着。3.5 取出flag并验证结果最后一步直接查询目标表里的数据?id-1 UNION SELECT 1,flag FROM 表名--如果表里的字段不叫flag而是叫其他名字就换成对应的字段名。如果表有多个字段想一次全查出来也可以这样?id-1 UNION SELECT 1,GROUP_CONCAT(列名1, 列名2) FROM 表名--这里要特别注意如果目标是数字型注入payload里不需要单引号和注释符直接?id-1 UNION SELECT 1,flag FROM 表名就行。但咱们这个题目是字符型所以单引号和注释符一个都不能少。我当初做的时候这里其实卡了一下原因是字段名搞错了。我当时查字段列表时看到好几个字段顺手就取了一个看起来很“像”flag的字段结果返回的是空白。后来用GROUP_CONCAT把所有字段一次性查出来才恍然大悟原来真正的标识字段是另一个名字。整个过程虽然简单但这种“字段名猜错”的坑在真实题目里太常见了。4. 踩坑实录新手最常遇到的三类卡点与排查思路题解看着顺利但真正动手做的时候大多数人都会在某个地方卡住。这一节我把我在这个题目上以及带新人时经常见到的几个坑展开讲一讲完整链路。不是说直接给你们答案而是带你们走一遍“发现问题→分析原因→解决”的过程。4.1 注释符失效页面尾部多了一个单引号第一个高频坑是注释符不生效。典型表现是payload看起来完全正确但页面一直报错或者返回的结果不对。比如说?id1 ORDER BY 2--这个payload发过去页面报错提示near ORDER BY 2-- 之类的信息。注意看报错信息里ORDER BY 2后面还有一个单引号。这说明什么说明--这个注释符根本没把尾部的单引号注释掉。可能的原因有两个。第一个原因--注释符后面必须有空格或者用--、--%20这种URL编码方式表示空格如果后端对请求做了某种处理导致空格消失了注释就失效了。第二个原因这个环境后台拼接时用了两层引号或者括号单靠一个注释符闭合不了。排查思路是先换一种注释方式试试。比如?id1 ORDER BY 2##是MySQL里的注释符不需要尾随空格。如果#生效了说明问题出在--的空格处理上。如果#也不生效那就要考虑是不是有其他字符需要闭合。先观察报错信息中“剩余”的部分再决定是补一个单引号还是补一个右括号。我在这个题上实测推荐的直接方案是用--就行但如果你发现不生效立刻切#。多试两种不要在一个注释符上死磕。4.2 回显位置选错数据查出来了但页面上看不到第二个特别典型的坑是信息查出来了理论上应该回显在页面上但页面就是什么都没有或者输出位置不对。我见过最多的情况是这样新手用了?id-1 UNION SELECT 1,2,3--但页面依然显示了id-1对应的正常课程记录或者只显示了“1”没有“2”。原因不外乎两种。第一种原查询和UNION查询的字段数不一致数据库直接报错页面显示异常。这种情况回头用ORDER BY重新确认列数即可。第二种你选的回显位不在页面渲染的区域内。有些页面后端的SQL查询了多个列但前端只渲染了其中一列另外一列虽然查询出来但根本不会显示。这就是为什么要把database()、flag这些数据尝试放在不同位置的原因。我的建议是一开始测试时就养成好习惯所有位置都放上不同的占位符。?id-1 UNION SELECT 1,2,3--页面显示“1”和“2”说明第1列和第2列都是回显位那这两个位置都可以放数据如果只显示“1”那后面查数据时就要把数据放在第1个位置。当你拿到一个目标表发现放在第1列没显示时立刻换到第2列再试一次。这道题比较简单一般有一个回显位就够用了但如果你习惯性地把数据都放在同一个位置一旦那个位置不是有效回显位就会卡很久。4.3 字段名与表名的“想当然”第三个坑就是过于相信自己的直觉。很多新手在information_schema里查到表名、字段名之后会下意识地认为“flag肯定就叫flag”结果查出来是NULL或者空白。排查思路很简单查询时不猜直接把所有字段都列出再决定查哪个。比如?id-1 UNION SELECT 1,GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema库名 AND table_name表名--这样一次就能看到所有字段名完全不需要猜。在真实场景里目标系统为了混淆会故意把字段名起成跟业务相关的名字比如user_name、md5_pass甚至是一串随机字符串。养成“先列全再选查”的习惯能少踩很多坑。这道题的flag字段名其实不复杂但如果你从一开始就直接猜恰好猜错了页面返回空那就会陷入一个很尴尬的循环你觉得payload没错但又不知道为什么没数据。其实只需要停下来把字段列表拉出来看清楚一秒就解决了。5. 从靶场到实战这道题背后的SQL注入原理与防御思维如果你只是照着上面的步骤把flag拿出来那这道题只发挥了它一半的价值。另一半价值在于你借这道题真正理解SQL注入为什么能成功以及后端应该怎么防。这一节我会把原理和防御串起来讲因为这决定了你能不能把一道题的经验迁移到其他地方。5.1 注入的本质拼接导致的语义改变SQL注入为什么能成功归根结底一句话用户输入被当作SQL代码执行了而不是当作普通数据来处理。正常情况下后端查询课程信息的写法应该是参数化查询SELECT * FROM course WHERE id ?问号是一个占位符用户输入的值会被数据库驱动当作纯数据传进去不管输入什么都不会改变SQL语句的语义。这是最安全的写法。但这个题目以及大量真实世界的遗留系统为了省事用的是字符串拼接SELECT * FROM course WHERE id 用户输入问题就出在字符串拼接上。用户输入的内容不再是“纯数据”而是被塞进了一个SQL语句的语法上下文里。当你输入1 UNION SELECT database(),2--时最终的SQL变成了SELECT * FROM course WHERE id 1 UNION SELECT database(),2--这句话的语义已经彻底变了。前半句查出id为1的课程后半句把数据库名拼进结果里。数据库并不知道哪些字符是开发者写的哪些是用户写的它只知道这是一条合法的SQL语句然后忠实地执行。理解这一点很重要因为它直接解释了前面所有payload的设计逻辑闭合引号是为了打破原来的字符串字面量让自己输入的内容跳出“数据”的边界成为“语法”的一部分注释符是为了吞掉开发者原本写的剩余语句防止语法错误。所有这些操作本质上都是在操控SQL语句的语法结构。这道题虽然简单但它把这个“结构操控”的过程还原得非常清晰。5.2 常见的过滤方案及其绕过思路如果你去查资料会看到很多“防注入”的方案。比如过滤关键字把UNION、SELECT、database这些词给屏蔽掉比如对单引号做转义比如用WAFWeb应用防火墙来拦截恶意请求。这些方案在某种程度上有效但都不如参数化查询来得彻底。为什么因为过滤方案本质上是在“猜攻击者会输入什么”。只要你在黑名单里漏掉一个写法攻击者就可能绕过去。比如过滤了SELECT可以用SeLeCt混淆大小写有些系统不区分SQL关键字的大小写比如过滤了空格可以用/**/注释符替代空格比如过滤了单引号可以想办法用十六进制编码。说白了黑名单的思路是“你设一个路障我再找一条路”永远处于被动。这道题里没有过滤所以你感受不到绕过那些技巧的复杂程度。但你应该建立起这个概念SQL注入的攻防对抗本质上是“拼SQL的方式”与“过滤策略”之间的博弈。只有参数化查询才能从根本上切断“用户输入变成SQL代码”这条路。5.3 参数化查询为何是根治方案参数化查询的原理是把SQL语句的结构和参数分开传输。后端先把SELECT * FROM course WHERE id ?这个模板发给数据库数据库完成语法解析然后再把用户输入的值作为纯数据绑定到那个问号上。在这个过程里用户输入永远不可能改变已经解析好的语句结构。这就好比你去银行填单子单子的格式是固定的你只能在“金额”这一栏填数字。无论你在金额栏里写什么它都只是数字不可能突然变成“转账给XXX 100元并附加留言”这么复杂的指令。字符串拼接则相反它相当于把用户输入直接写成一句完整的话再念给柜员听那用户说“顺便把我余额查一下”柜员也只能照办。所以在真实的开发项目里只要你用参数化查询经典的联合查询注入、报错注入、盲注全都失灵。因为这个题目本身是教学用靶场才故意保留了最原始的拼接写法。看题的时候你可以把注意力放在“怎么打”上但如果你自己是写代码的人我更希望你把注意力放在“为什么参数化查询能彻底防住”上。5.4 从这一题延伸出去的学习路线做完BUU SQL COURSE 1之后你对联合查询注入应该已经有了一套完整的肌肉记忆。但SQL注入的世界远不止这一种形态我建议你按下面的顺序继续练先练报错注入核心是利用extractvalue、updatexml这类函数让数据库把我们要查的内容通过报错信息吐出来。适合页面没有回显位的场景。再练布尔盲注页面完全没有数据输出只有“正常”和“异常”两种状态用substr、ascii函数逐字符猜数据。然后练时间盲注连“正常/异常”都看不出来只能用sleep函数的延迟来判断条件真假。最后可以碰一下堆叠注入多条SQL语句用分号隔开一次执行多个操作和联合查询的注入思路又有区别。每一步都可以在BUUCTF平台或者其他训练靶场上找到对应的题目。你会发现所有花里胡哨的注入技巧底层都离不开这道题教给你的那两件事判断拼接方式构造闭合逻辑。把这两件事吃透剩下的都是在不同限制条件下变着花样做同一个动作。我在实际带新人时通常会让对方把这道题连做三遍第一遍看题解照着敲第二遍关掉题解自己走通第三遍故意制造一些错误比如注释符写错、列数猜错看看报错信息长什么样。三遍下来SQL注入的骨架基本就长在脑子里了。后面再碰复杂题目你会发现万变不离其宗。
阅读完成 · 觉得有帮助?