BUUCTF Web SQL注入实战从入门题到绕过手法的完整拆解做CTF的朋友应该都清楚BUUCTF的Web方向是新手入坑首选而SQL注入永远是绕不过去的一道坎。不管是校内赛还是HW面试SQL注入都是最基础也最能拉开差距的知识点。这篇博文我直接以BUUCTF上的SQL注入题目为主线从信息收集、注入点判断、数据脱取到常见的过滤绕过把一套完整的手工注入流程和踩坑记录整理出来。不管你是刚开始接触Web安全还是在刷题时卡在某个过滤环节这篇文章都能给你一条可复现的解题路径。先说下背景BUUCTFBuuctf平台收集了各大CTF赛事的真题Web分类下的SQL注入题覆盖了联合注入、报错注入、布尔盲注、时间盲注、堆叠注入、文件读写等几乎所有常规考点。很多题目是直接拿真实赛题改的所以刷这些题约等于用真题练手性价比非常高。1. 为什么SQL注入是BUUCTF Web的门票题SQL注入能够常年霸占CTF Web题目的C位本质上是因为它考察的不是死记硬背的漏洞库而是你对数据库执行逻辑的理解深度。这类题目通常给定一个带参数的查询点比如?id1你需要通过构造输入让后端拼出来的SQL语句执行你想要的操作。1.1 SQL注入的核心本质后端代码最典型的漏洞写法是直接拼接字符串类似这样$sql SELECT * FROM users WHERE id . $_GET[id];问题在于$_GET[id]本应是一个数字但如果你传入1 and 11最终拼出来的是SELECT * FROM users WHERE id 1 and 11这条语句不仅合法而且会让后面的额外条件生效。当你传入1 and 12时如果页面表现不同就说明你输入的内容被带进了SQL逻辑里注入点就确认了。这是整个SQL注入解题的基础逻辑也是所有后续操作的地基。理解这个本质之后你就明白为什么我建议新手不要在BUUCTF上直接上sqlmap。手工注入一遍能帮你把查询逻辑、注释符用法、报错函数这些细节全部过一遍远比自己当工具人收获大。1.2 注入题常见的三种形态BUUCTF上的SQL注入题目表面花样再多内核也无非几种。第一是普通联合注入页面有回显位直接用union select把数据带出来。第二是报错注入回显位被过滤了或者没有回显位但数据库的错误信息会直接打印到页面上用updatexml、extractvalue这类函数强行把查询结果塞进报错信息里输出来。第三是盲注页面既不回显数据也不报错只能看到正常或者异常两种状态要靠逐字符猜解。我在刷题时遇到过不少人一上来就猜是不是堆叠注入、是不是二次注入结果绕了大半天发现就是最基础的联合注入。经验之谈先用最简单的判断方法后面会展开讲确认注入类型再往复杂的方向走否则容易把时间浪费在错误的方向上。2. 手工注入的五步标准化流程我在BUUCTF上刷SQL注入题基本固定走五步发现参数点、判断注入类型、确定字段数、寻找回显位置、脱取数据。这套流程看起来简单但每一步都有细节坑下面逐个拆解。2.1 第一步发现参数点与初步探测拿到一个题目首先用浏览器访问观察URL和页面交互。BUUCTF上的题目大多是非常明显的?id1这种GET参数但也不排除POST提交、JSON格式提交、请求头注入的情况。我一般会先用Burp Suite把请求完整抓下来看着原始请求找参数而不是只看浏览器地址栏。找到参数后做最基础的探测。拿?id1举例?id1 ?id1 ?id1 ?id1 -- -加单引号后如果页面报错或者表现异常就说明单引号被拼接进了SQL语句。很多新手在这里分不清报错和被过滤我的判断标准是如果返回了数据库相关错误比如You have an error in your SQL syntax说明单引号是有效的如果页面干干净净但内容没变化多半是被过滤或转义了。注意这里有个细节BUUCTF有些题目会在传入参数后加上LIMIT 1这类尾巴所以注释符非常重要。MySQL里--注意后面有空格和#都能注释掉后面的内容但--后面不加空格在某些版本里不生效我习惯用-- -或者#。2.2 第二步判断注入类型确认参数可控后需要判断是数字型还是字符型注入。数字型不用引号包裹字符型则会在SQL语句里以$id形式出现。判断方法非常简单。对于?id1输入1 and 11页面正常输入1 and 12页面异常说明是数字型注入。如果两个都异常尝试1 and 11和1 and 12如果前者正常后者异常就是字符型注入。这里有一个非常容易踩的坑有些题目虽然参数是数字型但数据库或者后端框架做了类型转换导致1 and 11中的字母被剥掉。BUUCTF里我记得有几道题就是典型的高防型单引号和空格全被过滤这时候需要在后面的绕过环节处理先不纠结。确认注入类型后还需要确认数据库类型。虽然BUUCTF的Web题90%以上是MySQL但有个别题会使用SQLite或PostgreSQL不同数据库的注释符、函数、系统表完全不一样。判断数据库最直接的方法是用版本函数比如MySQL的version()、database()如果在后续联合注入时这些函数能正常回显基本就是MySQL没跑了。2.3 第三步确定字段数ORDER BY的妙用确定字段数是联合注入的前置条件因为UNION SELECT左右两边的字段数必须一致否则MySQL会直接报错。?id1 ORDER BY 1 ?id1 ORDER BY 2 ?id1 ORDER BY 3当ORDER BY n正常时说明查询的字段数大于等于n当ORDER BY n报错时说明字段数小于n。依次增加n第一次报错那一次就是字段数。比如说ORDER BY 3正常、ORDER BY 4报错那字段数就是3。BUUCTF上有个细节有些题目在后端拼SQL时用了括号比如WHERE id($id)这时候ORDER BY的位置在括号内部直接拼接ORDER BY会导致语法错误。遇到这种情况可以尝试1) ORDER BY 3 -- -把括号闭合掉再排序。这就是为什么我一直在强调理解后端SQL拼接场景比死记payload更重要。2.4 第四步定位回显位置字段数确定后把对应id的值改成不存在的数比如-1然后拼接UNION SELECT 1,2,3。这样做的目的是让前面的查询结果为空把我们UNION出来的数据推到页面的显示位置上。?id-1 UNION SELECT 1,2,3 -- -页面显示的数字就是回显位。假设页面输出了2和3那说明第2、3字段可以显示数据后面的数据脱取就从这两个位置注入。如果是3单独显示就只能用第3字段。这个环节有一个BUUCTF常见变体回显在JSON数据里。页面源码看起来是正常的HTML但数据其实通过AJAX接口返回。我见过不少人在这个坑里卡住因为把HTML源码翻遍了都找不到回显数据。解决办法是直接看Burp里响应包的原始内容JSON数据一般在最前面或单独一段。2.5 第五步脱取数据库信息拿到回显位置后信息收集的顺序一般是当前数据库名 → 表名 → 字段名 → 字段值。原因很实际flag通常在某个表里得先知道有哪些表、哪些字段才能精准命中。获取当前数据库名的payload?id-1 UNION SELECT 1,database(),3 -- -假设当前数据库是ctf接下来获取该库下所有表名?id-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemactf -- -这里用group_concat把多行结果合并成一行显示效率远高于一行一行猜。如果页面显示被截断还可以用substr截断输出?id-1 UNION SELECT 1,substr(group_concat(table_name),1,100),3 FROM information_schema.tables WHERE table_schemactf -- -拿到表名后把表的列名拧出来?id-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schemactf AND table_nameflag -- -最后读取数据?id-1 UNION SELECT 1,flag,3 FROM flag -- -这套流程跑通后BUUCTF上至少三分之一的基础注入题可以直接秒掉剩下的就是在这个流程外加上跟过滤对抗的内容。3. 报错注入当回显位消失后的救命稻草联合注入最怕两件事回显位被过滤、结果不直接显示。BUUCTF里不少题就是这样页面只有正常和报错两种状态这时候就得请出报错注入。3.1 updatexml与extractvalue原理updatexml和extractvalue是MySQL的XML处理函数本意是操作XML文档但它们有个副作用当传入的XPath表达式格式错误时MySQL会抛出包含错误参数的报错信息。利用这一点把SQL查询结果拼进错误的XPath表达式里就能让查询结果跟着报错信息一起输出。extractvalue的经典payload?id1 AND extractvalue(1, concat(0x7e, (select database()), 0x7e))0x7e是~的十六进制编码用来制造XPath格式错误同时把查询结果包在中间方便识别。updatexml的payload类似?id1 AND updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schemadatabase() limit 0,1), 0x7e), 1)这里有个重要区别extractvalue报错信息默认最多显示32个字符updatexml也是类似限制。所以查询多个表名时不能用group_concat一把梭得配合limit 0,1一行一行取。从我踩坑的经验看很多新手在报错注入时卡住不是因为不会写payload而是没意识到输出长度限制导致查出来的表名被截断了。3.2 报错注入的进阶技巧把完整数据分段取报错注入要拿到完整数据思路是分段截取。假设我们要查flag表里的flag字段值可以这样构造?id1 AND updatexml(1, concat(0x7e, substr((select flag from flag limit 0,1), 1, 16), 0x7e), 1) ?id1 AND updatexml(1, concat(0x7e, substr((select flag from flag limit 0,1), 17, 16), 0x7e), 1)第一次取第1到16位第二次取第17到32位以此类推。这里副作用的帮助是即使flag全是大写字母和花括号每段16个字符也足够我们拼出来。BUUCTF里我记得有题的后端对substr做了过滤但mid和right大概率没过滤换个函数就能绕过。这个思路在后面过滤绕过部分会再次提到。3.3 用floor报错的场景除了函数报错MySQL里还有一种经典的报错方式floor(rand(0)*2)与group by碰撞导致主键冲突报错。这个报错技巧比较老但在某些过滤场景下比如过滤了updatexml和extractvalue依然好用。?id1 AND (select 1 from (select count(*), concat((select database()), floor(rand(0)*2)) x from information_schema.tables group by x) y) -- -原理不展开细讲了核心是利用对group by分组时对rand()的重复计算导致临时表主键重复报错信息里会带出查询内容。这类payload看起来长但实际只需要背一个模板把中间的子查询换成你要执行的SQL就行。4. 盲注看不到报错也要把数据一点点抠出来BUUCTF上真正拉开差距的是盲注题。页面既没有回显也没有报错只有查询成功和查询失败两种状态。这时候核心思路是把数据对不对转化成页面正不正常。4.1 布尔盲注的逐字符猜解布尔盲注的核心是利用substr加ascii函数把数据库内容转成ASCII码再逐字符比较。判断数据库名第一个字符的ASCII是否大于99?id1 AND ascii(substr(database(),1,1))99 -- -页面正常说明第一个字符ASCII大于99继续二分页面异常说明小于等于99往小猜。不断缩小范围直到精确匹配。这个过程手工跑起来非常痛苦但BUUCTF的初级盲注题数据量小手工猜十几轮也能出结果。真正刷题效率高的方式是用Python脚本二分法这里给一个我常用的模板import requests url http://example.com/?id result for i in range(1, 50): low, high 32, 127 while low high: mid (low high) // 2 payload f1 AND ascii(substr(database(),{i},1)){mid} -- - r requests.get(url payload) if 正常页面特征 in r.text: low mid 1 else: high mid result chr(low) print(result)注意两个细节一是二分法初始范围设成32到127覆盖可打印ASCII字符二是代码里的正常页面特征要换成目标页面真实存在的标识性内容比如某个固定的HTML标签或者特定的提示文字否则脚本的判定逻辑就是错的。4.2 时间盲注没有布尔特征时的最后一招BUUCTF里有些题目更狠不管条件真还是假页面内容完全一样。这时候只能借助时间差异来判断。MySQL里的sleep()函数可以让查询暂停指定秒数配合if函数就能构造时间盲注。?id1 AND if(ascii(substr(database(),1,1))99, sleep(3), 0) -- -如果页面响应时间明显多了3秒说明条件为真否则条件为假。如果sleep被过滤还可以用benchmark(10000000, md5(test))这种CPU密集函数制造时间差。时间盲注写脚本时重点是用响应时间阈值来判定而不是布尔特征。我会用requests的timeout配合time.time()计算耗时阈值设在2~4秒之间比较稳。注意网络波动同一个payload多测两次再下结论。4.3 脚本自动化与数据回显盲注的最后一公里是把数据抠出来。除了逐字符猜还可以用ord函数和位运算一次通过6次HTTP请求对应每个bit位来精确定位字符。具体思路是用ascii(substr(data,i,1)) 1、 2、 4等依次获取字符的每一位然后组合成完整字符。这种位运算猜解法比二分法更快但脚本稍微复杂一些。对于新手我建议先掌握二分法能跑通、能出数据、能拿到flag就已经赢了。位运算是进阶玩法等熟练了再优化也不迟。5. BUUCTF过滤绕过那些让你卡关的坑在哪里BUUCTF的SQL注入题不可能让你一路趟过去很多题目会加过滤最常见的过滤目标就是空格、关键字、单引号、逗号。应对思路不是死记payload而是理解每种过滤能拦住什么、放过了什么。5.1 空格被过滤的替代方案空格被过滤时最常用的是注释符替代也就是/**/。比如?id1/**/union/**/select/**/1,database(),3/**/---MySQL里/**/可以完全替代空格作为分隔符。如果/**/也被过滤还能用括号把所有关键词和参数包起来比如union(select(database()))。实际上MySQL在语法解析时关键词后面跟左括号时不需要空格。实测下来union(select(1),2,3)很多场景下直接就能过。更有意思的是使用Tab和换行符。MySQL默认把\t和\n当作空白处理但很多过滤正则只匹配一个空格没有去除空白字符。利用这个特性在SQL语句里插入%09Tab、%0a换行也能起到分割作用。5.2 关键字过滤的绕过等价函数与编码遇到select、union被直接替换成空字符串的情况如果只替换一次可以试试双写绕过。比如selselectect经过一次替换变成了selectPUT到后端后反而变成合法的关键词。这个技巧在BUUCTF上好几道题都出现过。还有一类是等价函数替换。substr被过滤用mid或substringsleep被过滤用benchmarkdatabase()被过滤用schema()group_concat被过滤用concat_ws搭配group by。这非常考验函数库的熟悉程度我建议提前背下MySQL里常用的等价函数表。比较另类的思路是利用字符串处理绕关键词过滤。MySQL支持把字符串拆开再拼接比如sel||ect在开启了PIPES_AS_CONCAT模式下会被拼成select。但CTF里更常见的是用char()函数把关键词转成ASCII码再拼接?id-1 union select 1,char(100,97,116,97,98,97,115,101),3 -- -这里的char(100,97,116,97,98,97,115,101)就是database加上后面的()就成了database()。这个技巧在输入长度有限制时特别有用。5.3 引号过滤的思路引号被过滤是BUUCTF新手最容易卡死的地方之一。字符串常量不能写了很多人就慌了。但MySQL里有很多办法绕。最经典的是用十六进制表示字符串。比如ctf可以写成0x637466完全不需要引号。前面报错注入里的0x7e就是这个原理。表名和库名都可以用十六进制代替只要把字符串转成hex即可。另一个技巧是ANSI_QUOTES模式下的双引号。如果后端只过滤了单引号而数据库开启了ANSI_QUOTES那双引号也能用来包围字符串字面量。但这个依赖服务端配置不确定性比较大实测BUUCTF上很少用。5.4 综合场景一道题同时过滤了空格、select和引号BUUCTF上有几道经典的综合性题目过滤挺凶狠的。我的处理思路是把过滤项列出来然后逐个击破。先用/**/替代空格再用selselectect双写绕过select再用0x...十六进制绕过引号。看起来是一套很复杂的组合拳实际拆解下来就是每个环节各自解决一个问题没有一步是超纲的。这里分享一个排查技巧如果某个改写后的payload还是报错先别怀疑是过滤导致用报错信息定位到底是语法错还是拦截错。语法错说明payload本身不对没报错但页面没变化说明输入被吞了。两者的应对思路完全不同。6. BUUCTF SQL注入题目实战记录与经验总结最后分享一些在BUUCTF上刷SQL注入题时沉淀下来的实操经验和常见问题排查方法不一定来自某一道特定题目但都是我反复踩过、确认有效的经验。6.1 常见问题速查表我把刷BUUCTF过程中比较有代表性的问题整理成了一张表方便遇到类似情况时快速定位。现象可能原因排查方向单引号加进去页面无变化参数被转义或过滤尝试宽字节注入或hex编码ORDER BY3正常4报错字段数为3直接进入UNION阶段UNION SELECT后页面只显示第一个结果前面的id值没改成不存在的数用-1或大数绕过报错注入返回内容被截断函数输出长度限制用substr分段截取information_schema查不到表数据库权限不足或是非MySQL改用当前库直接猜表或确认数据库类型sleep函数执行但时间没变后端禁用了sleep换benchmark或其他耗时操作空格被过滤但/**/也没用正则过滤了注释符尝试%0a、Tab、括号包围6.2 手工注入的纪律性绝不盲目堆payload刷BUUCTF时我见过不少人的通病在一个payload失败后立刻换另一种注入方式完全不分析失败原因。比如联合注入没出数据不是去检查字段数有没有搞对直接就转报错注入最后浪费大量时间。我自己的习惯是每个阶段结束后先确认这个阶段的产出是否正常再进入下一阶段。判断注入类型时如果1 and 11和1 and 12页面没有差异先回头看看是不是参数没闭合、是不是注释符没生效。基础没打牢后面全白搭。6.3 合规提醒与后续学习建议BUUCTF是一个良好的授权CTF练习平台所有注入测试都在题目给定的靶机环境内进行这是合规且有价值的。但在实际业务系统里未授权扫描和注入测试可能涉及法律风险这个边界一定要分清楚。刷题归刷题别把payload随便打到线上系统上。后续想继续深入建议按这个顺序学先把BUUCTF Web里所有SQL注入题刷完包含联合、报错、布尔、时间、堆叠然后去pikachu和sqli-labs靶场过一遍完整流程再做CTFshow的SQL注入专题练习。刷完这些之后你对SQL注入的理解已经超过大多数初级安全工程师了。BUUCTF的SQL注入题刚上手时可能会因为各种小细节问题卡很久这太正常了。我之前刷一道布尔盲注题卡在脚本判断条件上后来发现是响应里混了时间戳导致特征判断不稳定。这种经验没法从书本上学到只有自己动手踩坑才能积累下来。希望这篇博文能帮你少走一些弯路也欢迎在评论区交流你在BUUCTF上碰到的奇怪过滤规则大家一起看看怎么解。祝刷题顺利flag拿到手软。
