1. 打开萌新22之前,你需要知道这道题在考什么
ctfshow的萌新系列,我一直觉得是给那些"刚知道SQL注入四个字怎么写,但不知道从哪下手"的人准备的。萌新22也不例外,它考的方向非常集中:SQL注入里的联合查询。如果你做过级客巅峰的web4,会发现这两道题的路子几乎是一个模子刻出来的——一个明晃晃的GET参数,一个看起来平平无奇的页面,剩下的全看你有没有一套标准的注入流程。
我第一次做这道题的时候,其实走了一段弯路。打开页面看到参数,第一反应是掏出sqlmap跑一把,结果跑半天不出东西,反倒是手工试了几下就拿到flag了。这也是我后来强烈建议所有萌新选手手工过一遍这类题的原因:工具永远替代不了你对注入原理的理解,尤其是当你面对的是专门用来训练新手的题目时,出题人根本不会上太复杂的过滤,所有答案都藏在最基础的判断流程里。
动手之前,我建议你先确认自己掌握了三样东西。第一,SQL的基本语法,至少要明白SELECT语句是怎么拼出来的;第二,information_schema这个系统库是干什么用的,很多新手卡在这一步,不知道去哪找表名和列名;第三,HTTP请求的基本概念,至少要知道GET参数是跟着URL一起传的,表单提交的数据最终会变成什么样的参数键值对。这三样东西不需要多深入,但是缺一样,后面每一步都会走得很痛苦。
这道题和级客巅峰web4相似的点在于,它考察的就是最纯粹的联合注入链路:判断注入点、测列数、找显示位、爆库名、爆表名、爆列名、拿数据。整个流程没有花里胡哨的绕过,就是SQL注入的"标准教程题"。正因为如此,它其实是一道非常适合用来检验自己基础扎不扎实的题——如果你能不看任何writeup独立做出来,说明你对联合查询的掌握已经到了"条件反射"的级别。
1.1 萌新系列出题的底层逻辑
萌新系列的题目,从命名就能看出来,定位是给完全零基础的人练手的。这里的"练手"不是说题目简单到没有技术含量,而是说每一道题都严格对应一个明确的知识点,你只要把那个知识点吃透了,就一定做得出来,不会出现"我明明知道是注入但不知道怎么绕过"的挫败感。
萌新22对应的知识点就是标准的union联合注入。整个题目环境会模拟一个非常典型的"把用户输入直接拼进SQL语句"的漏洞场景,参数没有任何过滤或者过滤得很浅,目的就是让你把注意力集中在注入手法的完整链路上,而不是被各种WAF规则带偏节奏。
1.2 动手前建议准备的东西
做这类题,我建议至少准备三样东西。一个是Burp Suite,用来抓包和反复修改请求,比在浏览器地址栏里改参数效率高得多;一个是浏览器的开发者工具,至少要看明白网络面板里的请求和响应;再一个是本地的命令终端,如果题目环境允许直接用curl发请求,很多调试过程会更快。至于sqlmap,我只建议在手工链路全部跑通之后拿来验证结果,不建议一上来就依赖它。
2. 注入点的确认:先用手,再用工具
拿到题目后的第一步,永远是确认注入点,而不是猜测过滤规则。这道题的页面结构很简单,一个参数传进去,返回的内容会直接反映在页面上。这时候你只需要做三组测试,就能非常清楚地判断出注入类型和闭合方式。
2.1 第一组测试:正常参数与异常参数的响应差异
先传一个正常参数,比如?id=1,记录下页面返回了什么内容,内容里有哪些字段是来自数据库的。然后传一个不存在的值,比如?id=9999,看看页面是不是变成了空内容或者提示无数据。这个对比很重要,它告诉你一个核心信息:参数的值确实被查询用来过滤数据了,而且查询结果会直接反映到前端。
传了?id=9999没有正常数据之后,我心里就有底了——既然查询结果会显示在页面上,只要能用union select构造出一个"什么东西都查不到、然后执行我们指定的查询"的状态,就能拿到所有想拿的数据。这就是联合查询注入的基本前提。
2.2 第二组测试:单引号判断字符型闭合
接着传?id=1',重点观察页面的反应。如果出现了数据库报错信息,说明后端的SQL语句被破坏了,而且报错内容通常会把拼好的SQL语句片段暴露出来,这时候你就能直接看到开发者把参数包在了什么符号里。如果页面没有任何反应,只是返回空内容,说明报错被屏蔽了,但注入依然是存在的。
这道题传了单引号之后是有报错回显的,SQL语句大概是select * from xxx where id='1''这样的形态。看到双单引号紧挨着,基本可以确定后端是用'把参数包起来的。这是字符型注入最典型的特征。
2.3 第二组测试的延伸:判断数字型的场景
顺带说一句数字型的判断方法,因为虽然这道题是字符型,但很多同类题目用的是数字型。数字型的标志是你传?id=1 and 1=1有数据,传?id=1 and 1=2没有数据,因为数字型注入里参数直接拼在SQL语句中,and是作为逻辑运算符参与查询的。而字符型注入里,如果你只传and 1=1而不闭合前面的单引号,这个and会变成字符串内容的一部分,页面不会有任何变化。
很多人分不清这两种类型,其实就是没有做这组对比测试。你先分别传一个恒真条件和一个恒假条件,看返回内容是否出现差异,差异存在就是数字型,完全没差异就是需要闭合引号的字符型。
3. order by确定列数,union select拿到回显位
确认了注入类型之后,接下来的流程就非常标准化了。先测列数,再找显示位,然后才是爆数据。我见过不少新手跳过测列数直接union select,然后死活出不来数据,原因就是前后列数不一致导致union语法错误。这道题里,测列数的方法就是最经典的order by递增法。
3.1 order by递增探测的正确姿势
在id参数后面依次拼接order by 1、order by 2、order by 3……每拼一个就刷新页面看反应。如果当前列数等于或者大于实际列数,页面正常回显;一旦递增到超过实际列数,数据库就会报错,提示类似Unknown column的信息。这道题我在order by 3的时候一切正常,到order by 4立刻报错,所以查询结果一共是3列。
这里有一个细节值得注意:order by 4报错的时候,报错信息可能是中文提示也可能是英文提示,但无论提示内容是什么,页面返回的状态都跟前几次有明显不同。我用BurpSuite对比几次响应内容时看得特别清楚——前3次响应都是完整的正常页面,第4次响应直接变成了错误页面或者空响应。这个前后差异本身就是最可靠的判断信号。
3.2 union select前面为什么要跟一个假条件
列数确定之后,就要构造union select了。这中间有一个关键动作,是在union前面的原查询上拼接一个恒为假的条件,最常见的写法是and 1=2或者字符串类型的' and '1'='2。这么做的原因很简单:让原查询查不到任何数据,这样后面的union结果就有机会占用回显位,否则页面只会显示原查询的结果,你拼接的查询数据会被淹没在正常数据下面。
我在写payload的时候会把前面的参数值改成不存在的值,比如?id=1' and '1'='2,这样前面的select结果集为空,union select的内容就会直接作为第一行数据显示出来。有些新手习惯用-1作为参数值,这在数字型注入里也完全可行,本质思路一样,就是让前部分查询落空。
3.3 information_schema标准四步走
显示位确定之后,剩下的就是固定套路了。我在浏览器里反复测试之后,确定第2个和第3个显示位都能正常回显数据,接下来就是标准四步走:
第一步爆数据库名,payload是?id=1' and '1'='2' union select 1,database(),3--+,看到返回了数据库的名字,通常是ctfshow之类的名字,也可能是web开头或者随机的库名,反正从这一步开始,你就能切实感受到数据在手的感觉了。
第二步爆表名,payload是?id=1' and '1'='2' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+,这里用group_concat把多行表名拼成一行,页面显示才会完整。我在这一步看到的表名单里通常有一个跟flag相关的表,类似flag或者ctf_flag这样很直白的命名。
第三步爆列名,payload是?id=1' and '1'='2' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='刚才看到的表名'--+,注意表名要用单引号包起来,因为这本身是一个字符串条件。
第四步爆数据,payload是?id=1' and '1'='2' union select 1,flag字段,3 from 表名--+,flag就直接显示在页面上了。
这四步之间每一步的输入都依赖于上一步的输出,所以做题的时候我习惯开两个窗口,一个窗口放payload,另一个窗口记录上一步拿到的东西。新手经常在这一步栽跟头,就是在第一步和第二步之间反复横跳,表名忘记加引号、库名写死成database()的下划线拼错之类的低级错误,反而拖慢了速度。
4. 过滤拦截:萌新22里最常见的两道坎
联合查询跑通之后,很多新手会觉得自己已经会SQL注入了,其实真正的分水岭在过滤绕过这一步。萌新22这道题,说实话过滤很浅,但它确实设置了两个新手最容易卡住的点,这两个点如果你没想明白,后面的题目基本寸步难行。
4.1 空格与注释符的替换思路
第一个坎是空格被过滤。我在构造payload的时候,一开始老老实实用空格分隔SQL关键字,结果页面返回异常。试了几次之后意识到,后端很可能把空格直接替换成了空字符串。这种过滤的绕过思路非常固定:用SQL支持的注释符代替空格,最常见的就是/**/,比如union/**/select。
这个替换的前提是你要理解为什么/**/能代替空格。SQL标准里,注释符在语句中起到的是分隔符的作用,MySQL会把/**/当成一个词法单位忽略掉,但它前后相邻的关键字仍然会被识别为独立的token。说白了,数据库在解析的时候根本不关心关键字之间是空格还是注释,它只关心token是否被正确分割。所以union/**/select和union select在MySQL看来是完全等价的。
实际测试中我在这个点耗费了不少时间,因为payload里不只一处空格,如果只替换一个地方,其他地方照样报错。我的排查方法是把整个SQL语句拆成几段,每一段单独测试,确认这一段能跑通了再拼接下一段。比如先测试1'/**/union/**/select/**/1,2,3--+能不能正常回显,能回显就说明空格过滤和引号闭合都解决了,再一步步往后面加条件。
4.2 关键字被过滤时的绕过思路
第二个坎是部分关键字被过滤,最常见的是过滤select或者union。在这里我要啰嗦一句:看到过滤之后别直接慌,先测试一下过滤是怎么实现的。常见的过滤实现方式有大小写不敏感的替换、大小写敏感的精确匹配、只过滤一次等,不同的实现方式对应不同的绕过手段。
如果后端只是替换了一次关键字,那最简单的办法就是双写,比如ununionion,替换掉中间那个union之后就还原成了union。如果后端是大小写不敏感地替换,那就试试内联注释/*!select*/这种MySQL特有的写法,把关键字放在/*!和*/之间,MySQL会当作正常代码执行。如果连注释里的内容都被过滤了,那就要考虑用等价函数或者语法来替代,但萌新22这个阶段基本用不上那么复杂的姿势。
我在实际测这道题的时候,卡在select关键字被过滤这个问题上整整绕了好几分钟,后来才意识到后端过滤的是小写的select,我改成SELECT就不拦了。这种"大小写绕过"在真实CTF里已经很少见了,但在萌新题目里反而很常见,因为出题人的目的是让你理解"绕过"这个动作本身,而不是让你去挑战复杂的WAF规则。
4.3 用报错信息反向推断过滤规则
这里分享一个很有用的调试思路:当你发现payload被过滤的时候,可以利用页面的报错信息反推过滤规则。具体做法是故意构造一个能触发SQL语法错误的payload,然后观察报错内容里到底保留了哪些字符。比如你提交sel/**/ect,如果报错信息里出现了select,说明/**/被还原成了空格或者被解析掉了;如果报错信息里出现了selct,说明中间的内容被整个删掉了。
这个反推方法听起来土,但实际做题时非常管用。报错信息是后端给你的"内部反馈",比任何猜测都靠谱。我在这道题里就是用这种方法确认了空格过滤是替换而非删除,然后才放心地用/**/来绕过。
5. 做题过程中真正容易卡住的地方
写完payload整个链路,该说点实在的了。我做这道题时踩过几个坑,这些坑未必是题目设计的难点,但确实是很多新手反复折腾的地方。我把它们单独列出来,方便你对照排查。
5.1 注入点明明存在,为什么加单引号没反应
第一种情况是传了?id=1'之后页面完全没变化,既没有报错也没有数据异常。这时候先别急着怀疑注入点找错了,第一步应该去看页面返回的HTTP状态码和响应长度。如果是200并且长度和正常请求完全一致,那可能单引号被后端函数处理掉了,比如addslashes或者mysql_real_escape_string这类转义函数。如果是302跳转或者500,那说明确实有问题,只是错误信息被屏蔽了。
我在做萌新22的时候没遇到转义的情况,因为这道题的参数是干净地拼进SQL语句的。但如果你在类似的萌新题目里遇到了单引号无反应的情况,我的建议是换一种闭合思路,比如试试双引号?id=1",或者试一下URL编码后的单引号%27,有时候后端只是做了一层简单的字符替换,编码绕过就直接通了。
5.2 union select一直报错的排查顺序
第二种情况是query的列数明明用order by测出来了,但union select一直报错。我自己就犯过这个低级错误:测出3列之后,随手写了union select 1,2,3,却发现返回的是原查询的数据而不是我构造的数据,最后才想起来前面的查询没有用假条件置空。这个错很多人都会犯,排查顺序应该是:先看前段查询是否为空,再看列数是否一致,再看显示位是否在回显范围内。
还有一种可能,是显示位只有2个,但你在union select里写了3列,中间那列对应不上回显。这种情况在真实题目里很常见,因为显示位取决于页面模板怎么渲染字段,不一定跟数据库的真实列数一一对应。拿到显示位之后先测试每个位置的渲染情况,比如union select 1,2,3,看页面上哪几个数字被回显了,再决定把数据写到哪个位置。
5.3 Burp Suite调试和浏览器直接访问的差别
最后说说工具选择。做这类注入题,我非常不建议把全部message依赖在浏览器地址栏里。浏览器会自己做URL编码、会处理一些特殊字符,还会把空格自动编码成%20,有时候过滤规则是发生在Web层还是数据库层,在浏览器里根本分辨不出来。用BurpSuite的Repeater,你能看到完整的原始请求报文,能自己控制每个字符的编码方式,也能直接对比多个请求的响应差异。
我在做这道题时就是用BurpSuite的Repeater反复发送payload,通过观察响应数据包的长短来判断注入是否成功。响应长的说明数据被正常查出来了,响应短的说明查询结果为空或者语法报错被吞掉了。这个"看响应长度"的习惯,在做盲注题的时候尤其重要,建议现在就养成。
6. 从萌新22往后:这套套路能迁移到哪些题
萌新22做完之后,别急着做下一道题,先停下来把整套流程在脑子里过一遍。这道题最大的价值不是让你拿到flag,而是帮你建立一条完整的SQL注入攻击链路。在之后的刷题路上,你会发现不管题目怎么变,这条链路的骨架几乎不会变,变的只是过滤规则和闭合方式。
6.1 一套通用的手工注入流程
我自己在赛后复盘时整理的流程是这样的,你可以参考:第一步,找到所有可能和数据库交互的参数,包括GET参数、POST参数、Cookie、请求头;第二步,用单引号、双引号、反引号这些特殊字符去测试闭合方式;第三步,用order by测列数;第四步,构造union select找显示位;第五步,从information_schema拿库名、表名、列名的层级数据;第六步,如果union被过滤了,考虑盲注,用substr配合ascii逐字符判断。
这套流程听起来简单,但我见过太多人拿到一道新题就开始瞎试payload,完全没有流程意识。比如拿到题先跑' or 1=1--+,发现没反应就慌了,其实问题可能只是闭合方式是双引号而不是单引号。把流程固化成肌肉记忆,看到一道题先分类再走流程,效率会翻好几倍。
6.2 和级客巅峰web4这类题的共同规律
说回标题里提到的级客巅峰web4。这道题我做完之后专门去对比过,发现两题的核心逻辑完全一样:一个参数注入点,一个字符型闭合,一个标准的联合查询流程,甚至过滤规则都差不多。这说明了一个问题:很多CTF平台在基础web题目的设计上是有"通用模板"的,出题人会在同一个模板上换不同的参数名、换不同的表名列名,甚至换一层不同的过滤,但核心考点不变。
所以我认为,萌新22这道题值得反复做两到三遍。第一遍正常做拿到flag,第二遍故意给自己加难度,比如禁用group_concat,模拟空格过滤,强行写成不用union的盲注版本。这样练下来,面对同一类型的题目你会有完全不同的掌控感。
最后再分享一个小技巧:做完萌新22之后,我习惯把整个注入语句的完整版本记录在本地笔记里,包括每个payload的注释说明和适用场景。这不算什么高深的方法,但当你刷到第三十道、第五十道sql注入题的时候,回头看这些基础笔记,会发现当初那些费劲理解的东西,早就是条件反射级别的内容了。