SQL注入绕过实战:从基础闭合到堆叠注入的CTF闯关笔记

SQL注入绕过实战:从基础闭合到堆叠注入的CTF闯关笔记
1. 项目概述一次完整的SQL注入实战复盘最近在带新人入门网络安全发现很多朋友在CTFshow的Web入门系列里尤其是从web171到web175这几关卡在了SQL注入的绕过上。这几关设计得非常巧妙它不像基础靶场那样直接给你一个显眼的注入点而是层层设防考察你对SQL注入本质的理解和各种绕过技巧的灵活运用。我花了点时间把这五关的核心绕过思路和实战操作重新梳理了一遍形成这篇详细的复盘笔记。这篇文章不仅会告诉你每关的payload是什么更重要的是拆解背后的过滤逻辑、绕过原理以及我在实战中踩过的坑和总结出的通用思路。无论你是正在刷题的新手还是想巩固SQL注入绕过知识的朋友相信都能从中获得一些直接的启发。简单来说这五关是一个难度递进的“闯关游戏”每一关都在前一道过滤的基础上增加新的限制逼迫你跳出常规的union select套路去思考更底层的注入方法。我们会遇到空格过滤、关键字过滤、引号逃逸、堆叠注入等多种场景。通过这个系列你能深刻体会到SQL注入的对抗本质是“数据”与“指令”的混淆而绕过就是寻找过滤规则的盲区让我们的“指令”重新被数据库引擎正确解析。2. 环境准备与核心思路解析在深入每一关之前我们先统一一下实验环境和基本认知。我是在CTFshow的在线靶场环境下进行测试的你也可以在本地搭建类似环境复现。核心工具就是浏览器和Burp Suite或者HackBar插件手动注入更能锻炼理解。面对一个可能存在SQL注入的点我的通用思路是四步走探、判、查、取。探探测注入点与闭合方式通过输入单引号‘、双引号”、反斜杠\等观察页面回显错误或行为变化判断是否存在注入以及SQL语句的闭合方式如‘$id’、“$id”、($id)等。判判断数据库类型与过滤规则通过报错信息、延时函数如sleep(5)、布尔逻辑如and 11/and 12来判断后端数据库这里通常是MySQL以及初步感知是否有关键字过滤、空格过滤等。查查询数据库结构在确认注入点并找到绕过方法后系统性地查询数据库名、表名、列名。这里会用到information_schema数据库它是MySQL的“元数据图书馆”。取获取目标数据最终查询目标表里的数据比如flag。web171到web175的挑战核心都卡在第二步“判”和第三步“查”的绕过上。它们预设了过滤规则我们的任务就是像玩解谜游戏一样找到规则的漏洞。3. web171基础闭合与联合查询注入web171通常作为这个系列的开胃菜过滤相对简单。我们的目标是获取flag。3.1 注入点探测与闭合判断首先尝试在输入框提交一个单引号‘。如果页面返回了SQL语法错误比如You have an error in your SQL syntax...这强烈暗示存在字符型注入并且原SQL语句可能是类似SELECT * FROM table WHERE id‘$input’这样的结构。我们的输入被直接放进了单引号里。为了确认我们可以使用经典的布尔逻辑测试提交1‘ and ‘1’‘1 如果页面正常显示。提交1‘ and ‘1’‘2 如果页面无内容或错误。 这就能证明and后面的语句被执行了注入点存在且为字符型单引号闭合。3.2 绕过空格过滤与联合查询在web171中常见的过滤是将空格替换为空。如果我们直接使用union select 1,2,3其中的空格被过滤后变成unionselect1,2,3这无法被SQL解析。这里引入第一个关键绕过技巧使用注释符/**/代替空格。在MySQL中/**/是多行注释符但在词法分析时它同样起到了分隔关键字的作用。因此我们的payload可以构造为-1 union/**/select/**/1,2,3---1‘使得前一个查询结果为空让union后的结果能显示出来。/**/完美替代了所有空格。--或#是MySQL的单行注释符用于注释掉原SQL语句中剩下的那个单引号避免语法错误。提交后我们会在页面某个位置比如显示2和3的地方看到回显位假设是2和3。3.3 获取数据库信息接下来就是标准的信息收集流程查数据库名-1 union/**/select/**/1,database(),3--查表名-1 union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schemadatabase()--group_concat()函数将多行结果合并成一行方便查看。information_schema.tables是系统表存放所有表的信息。table_schemadatabase()条件限定只查当前数据库的表。假设查到表名为flag接着查列名-1 union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name‘flag’--这里注意‘flag’是一个字符串需要单引号。如果题目过滤了单引号可能需要用十六进制0x...或char()函数绕过但web171通常不需要。假设查到列名为flag最终取数据-1 union/**/select/**/1,flag,3/**/from/**/flag--实操心得使用/**/绕过空格过滤时要确保注释符中间没有空格。有时题目可能会过滤/**/可以尝试用括号()、换行符%0a、制表符%09等作为替代。在web171中/**/通常有效。4. web172引号逃逸与十六进制编码从web172开始难度升级。当你尝试使用web171的方法时可能会发现查询表名或列名时失败了。问题往往出在where table_name‘flag’这一步。4.1 过滤规则分析经过测试你会发现这一关过滤了单引号‘。当我们提交包含单引号的payload时它可能被直接删除或转义导致where table_name‘flag’变成了where table_nameflag。在SQL中不加引号的flag会被当作列名而非字符串从而引发错误。4.2 关键绕过技巧十六进制编码既然不允许使用引号来表示字符串我们就需要换一种方式告诉数据库“flag”是一个字符串值。这里最常用的方法就是十六进制编码。在MySQL中可以直接使用0x开头的十六进制数来表示字符串。字符串flag的十六进制编码是0x666C6167。你可以用Python快速计算‘flag’.encode().hex()得到‘666c6167’前面加上0x即可。也可以使用MySQL的hex()函数动态转换但前提是你能在注入点执行它。因此查询表名的payload需要改造-1 union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schemadatabase()--这一步还能用因为database()返回的是数据库名不是字符串字面量。但查询列名时就需要用到十六进制了-1 union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name0x666C6167--这样即使单引号被过滤0x666C6167也能被正确解析为字符串‘flag’。4.3 完整利用链判断注入点与闭合同web171。使用/**/绕过空格过滤确定回显位。获取数据库名database()。获取表名... where table_schemadatabase()。获取列名... where table_name0x666C6167。获取数据-1 union/**/select/**/1,flag,3/**/from/**/flag--注意最后的from flagflag是表名不是字符串所以不需要引号或编码。注意事项十六进制编码不区分大小写0x666C6167和0x666c6167是一样的。在实际渗透测试中遇到过滤引号的情况十六进制编码和char()函数如char(102,108,97,103)是两种非常可靠的绕过手段。5. web173关键字过滤与双写绕过web173在之前过滤的基础上增加了新的挑战关键字过滤。你可能发现当你提交union select时页面没有任何回显或者返回了错误。5.1 探测过滤规则首先需要探测具体过滤了哪些关键字。常见的测试方法是提交一些包含可能关键字的payload观察其与纯文本的区别。例如提交union看是否被拦截。提交select看是否被拦截。提交union select看是否被拦截。一种常见的过滤方式是将关键字替换为空字符串。例如将union替换成“”。那么union select经过过滤后就变成了select。5.2 关键绕过技巧双写绕过针对这种“替换为空”的过滤经典的绕过方法就是双写关键字。因为过滤函数通常只执行一次替换。如果我们输入ununionion中间的union被删除后剩下的字符恰好又组成了一个新的union。假设题目过滤了union和select通常还会过滤or,and,from等我们的payload就需要写成-1 ununionion seselectlect 1,2,3--经过过滤后ununionion- 删除中间的union- 剩下unionseselectlect- 删除中间的select- 剩下select最终数据库执行的语句依然是union select 1,2,3。5.3 构造完整Payload在web173中我们需要将双写绕过与之前的空格绕过/**/结合。并且在查询information_schema时from、where等关键字也可能被过滤需要一并双写。查询表名的payload示例-1 ununionion/**/seselectlect/**/1,group_concat(table_name),3/**/frfromom/**/information_schema.tables/**/whwhereere/**/table_schemadatabase()--这里将from双写为frfromomwhere双写为whwhereere。后续查询列名、数据的payload也需遵循同样的规则。踩坑记录双写绕过时一定要确认过滤规则确实是“替换为空”且只执行一次。有些WAF可能会递归过滤或使用正则表达式全局匹配双写可能失效。此时需要尝试其他方法如大小写混淆UnIoN、内联注释/*!union*/、等价函数替换等。在CTFshow这个系列里双写通常有效。6. web174堆叠注入与Handler语句web174的过滤可能更加严格或者改变了过滤策略使得联合查询注入变得困难。这里我们引入一种更底层、更强大的注入技术堆叠注入Stacked Injection。6.1 什么是堆叠注入堆叠注入的原理是利用SQL语句的分隔符通常是分号;在一次数据库调用中执行多条SQL语句。例如原本的查询是SELECT * FROM users WHERE id‘$id’如果我们输入1‘; SELECT “hello”; --最终执行的SQL就是SELECT * FROM users WHERE id‘1’; SELECT “hello”; --这相当于先后执行了两条独立的SELECT语句。6.2 堆叠注入的利用条件堆叠注入并非总是可用它严重依赖于后端数据库的连接驱动和配置。PHP中使用mysqli_multi_query()函数可以支持多语句查询而mysql_query()或PDO默认配置下通常不支持。CTF题目为了考察这个知识点往往会开启多语句支持。6.3 关键绕过技巧使用HANDLER语句在MySQL中HANDLER ... OPEN和HANDLER ... READ语句提供了一种直接访问表存储引擎的接口它像游标一样逐行读取数据。最关键的是HANDLER语句不依赖information_schema也不需要使用SELECT关键字这可以绕过对select、from、where等关键字的过滤。使用HANDLER的基本步骤HANDLER table_name OPEN;打开一个表句柄。HANDLER table_name READ FIRST/NEXT;读取第一行/下一行。HANDLER table_name CLOSE;关闭句柄。6.4 实战利用过程假设我们已经通过其他方式如报错知道了数据库名ctfshow_web和表名flag在CTF中这有时是默认或提示的并且存在堆叠注入漏洞。我们的注入payload可以这样构造1‘; handler flag open; handler flag read first; --或者更简洁地利用分号连续执行1‘; handler flag open; handler flag read first; handler flag close; --注意表名flag如果与关键字冲突需要用反引号包裹。read first获取第一行数据。提交后如果页面会显示查询结果那么HANDLER ... READ的结果可能会被直接打印出来从而看到flag。6.5 未知表名时的探索如果不知道表名怎么办在支持堆叠注入且过滤不严的情况下我们甚至可以尝试用show tables;来查看表名。但若show和tables被过滤HANDLER语句就需要盲猜表名这在CTF中通常表名是预设的如flag,ctfshow_flag等。核心原理堆叠注入的强大之处在于可以执行任意SQL语句包括INSERT、UPDATE、DROP甚至SET修改连接变量。HANDLER语句是其一种高级用法它绕过了标准的SQL查询解析路径直接从存储引擎读取数据是绕过复杂WAF的利器。但务必注意在真实环境中堆叠注入的利用条件更为苛刻。7. web175综合过滤与盲注或二次注入思路web175作为这个系列的收官之题往往集大成过滤规则可能最为全面可能联合查询、堆叠注入、常见关键字都被封死。这时需要更灵活的思维。这里提供两种可能的突破方向基于布尔或时间的盲注或者寻找二次注入的机会。7.1 方向一盲注Blind Injection当页面没有直接的数据回显但会根据SQL语句执行的真假返回不同的页面状态布尔盲注或者执行时间有明显差异时间盲注时盲注是唯一的选择。布尔盲注通过substr(),ascii(),length()等函数逐位猜测数据。 例如猜测数据库名第一个字符的ASCII码是否大于1001‘ and ascii(substr(database(),1,1))100 --如果页面返回“正常”状态说明为真返回“错误”或无内容说明为假。通过二分法可以快速定位字符。时间盲注通过if()函数和sleep()函数根据条件是否成立来触发延时。 例如如果数据库名第一个字符是‘c’则休眠5秒1‘ and if(ascii(substr(database(),1,1))99, sleep(5), 1) --通过观察页面响应时间来判断条件真假。在web175中如果前端过滤了and、or可以用、||代替过滤了sleep可以用benchmark()函数进行耗时操作。7.2 方向二二次注入Second-Order Injection这是一种更隐蔽的注入方式。攻击者将恶意载荷输入到系统中该载荷被存储在数据库里例如注册用户名、留言内容。之后当另一个功能点从数据库取出该数据并不加过滤地用于SQL查询时注入发生。在CTF题目中可能会提供一个注册和登录功能或者一个插入数据和查看数据的功能。插入阶段注册一个用户用户名为admin‘ --。后端在存入数据库时可能进行了转义变成admin\‘ --存入。触发阶段当某个功能如密码重置、信息查询执行SQL语句如UPDATE users SET password‘newpass’ WHERE username‘$username’时从数据库取出的用户名admin‘ --被直接拼接进SQL。由于从数据库取出时转义符\可能被去除或不再起效导致SQL变为UPDATE users SET password‘newpass’ WHERE username‘admin’ -- ’这样就能修改admin的密码。对于web175需要仔细审计题目给出的所有功能点看是否存在“存储-再调用”的流程。7.3 实战中的混合策略面对web175这样的综合关卡我的步骤通常是全面Fuzz用Burp Suite的Intruder模块测试各种关键字、符号、编码的过滤情况。尝试盲注如果发现输入and 11和and 12页面有差异立即转向布尔盲注脚本。寻找隐藏功能查看页面源码、JS文件、robots.txt寻找其他可能触发查询的API或参数。思考非常规利用比如利用updatexml()或extractvalue()进行报错注入即使没有显式错误回显也可能在页面某处留下痕迹或者利用into outfile写文件需要绝对路径和FILE权限。排查技巧实录当所有常规注入手段都失效时不妨回归最简单的测试。提交一个单引号‘观察是返回空白页、500错误还是自定义错误。空白页可能意味着语法错误导致查询失败500错误可能意味着触发了后端异常自定义错误则可能泄露信息。有时候题目可能仅仅过滤了空格和某些关键字但、%0a换行没有被过滤。尝试union%0aselect可能会成功。永远不要忽视最基础的符号测试。

最新新闻

日新闻

周新闻

月新闻