SQL注入空格过滤绕过技巧:从注释符到内联注释的实战指南

发布时间:2026/10/14 13:54:46

SQL注入空格过滤绕过技巧:从注释符到内联注释的实战指南 1. 场景拆解空格在SQL注入里到底扮演什么角色1.1 为什么目标要过滤空格我在某次授权测试中碰到了一个很有意思的案例一个面向学生的成绩查询页面输入学号就能看到自己的成绩单。照惯例丢一个单引号进去页面返回了500错误再试1 and 11依然报错。当时第一反应是存在某种输入过滤于是换成1/**/and/**/11页面直接返回正常结果。这个现象说明问题不在服务端逻辑而在中间某一层做了关键字处理把空格字符直接替换成了空字符串。很多自研的过滤函数都爱这么干因为它实现起来最简单——str_replace( , , $input)一行代码完事什么SQL注入、命令注入都能挡掉一部分。可问题在于这种“无脑删空格”的过滤思路并没有真正理解SQL语法本身。1.2 空格在SQL语句里的作用是什么先明确一个概念空格不是SQL语法的一部分它只是词法层面的“分隔符”。SQL解析器在把字符串拆成token时需要某种边界来区分不同的关键字、字段名和参数值。比如select username from users where id 1这里的空格把select、username、from、users、where、id、1这几个token区分开。如果没有空格这一整串字符会被解析成一个完整的标识符完全没法识别。所以“过滤空格”的本质是破坏了SQL语句的token边界。但SQL标准里并不是只有空格这一种分隔方式注释符、括号、换行符在特定位置同样能起到边界作用。换句话说过滤空格不是堵死了注入路只是逼你换一种姿势。1.3 先判断过滤逻辑在哪里新手遇到报错往往容易慌但排查思路其实很固定。我用一个简单方法区分过滤层的位置把注入语句拆开分步测试。先提交1看是否报错确认存在闭合问题再提交1 1这时候如果中间的空格被过滤服务端收到的就是11页面可能恢复正常因为变成了两个连续的单引号在某些数据库里会被当成字符串拼接最后提交1/**/1如果页面正常返回数据说明/**/没被过滤问题集中在空格上这一步很关键。因为有的过滤在WAF层有的在代码层还有的在数据库访问中间件里。它们的绕过逻辑不完全一样WAF层往往有完整的规则库不仅要滤空格还会匹配union、select等关键字代码层则简单得多大多数只做了字符串替换。我这次遇到的情况比较友好只过滤了空格和少数几个关键字没有触发WEB应用防火墙式的深度检测。所以后面绕过思路可以放开一些但如果遇到全套规则就得用到更复杂的编码、分割、数据库特性技巧了。2. 空格过滤的常见绕过姿势与适用场景2.1 注释符/**/最基础也最通用的替代方案只要过滤逻辑没有把*和/也纳入黑名单/**/就是最好用的空格替代品。原因很简单在SQL标准中/和*之间的内容是注释解析器会直接跳过所以/**/本质上等于什么都没有。用它来分割token从语法角度是完全合法的。一个典型的注入语句1/**/and/**/11/**/union/**/select/**/username,password/**/from/**/users除了性能和长度稍有瑕疵这种写法在大部分数据库里都能直接执行。我测过的MySQL、PostgreSQL都支持Oracle也兼容唯一要注意的是Oracle里--注释后面必须跟一个空格或控制字符否则会注释失效这是另一个坑。2.2 括号的魔法不需要空格的另一种写法很多时候我们会忽略一个事实SQL的语法解析是有层次的。union select和(select)在语义上几乎等价但括号在大部分数据库里可以替代空格完成“边界”作用。举个例子判断字段数的经典语句1/**/order/**/by/**/3但如果order和by也被一并过滤了这条路就走不通。这时候可以换成group by1/**/group/**/by/**/3group by和order by在报错行为上类似都能用来判断字段数量。更狠的姿势是直接用子查询替代1/**/and/**/(select/**/1)/**/and/**/11这里(select 1)本身是一个完整的表达式不需要额外的空格来分割因为左括号和select之间本来就不需要边界。这一点很多新手没意识到当注入点周围没有空格也能保持语法正确时括号就是最轻盈的绕过工具。2.3 内联注释/*!*/MySQL专属的分词器后门/*!...*/是MySQL独有的语法正经名字叫“版本化注释”或“可执行注释”。正常情况下它的用途是让低版本MySQL忽略高版本语法但在注入场景里它变成了一个“能被MySQL执行、却被普通过滤器忽略”的隐藏通道。比如1/*!union*//*!select*/username,password/*!from*/users在MySQL里/*!union*/会被解释成union而普通的安全规则在匹配关键字时往往只关注肉眼可见的union、select遇到这种包裹形式就直接放过了。实际测试中只要目标不检测/*!这个特征成功率会高得离谱。不过要注意版本限制内联注释里的内容要求是MySQL能识别的版本号之后的语法如果目标数据库版本过低可能导致解析失败。稳妥起见可以先在本地环境验证一遍目标版本对应的语法。2.4 换行符与制表符被大多数人忽略的控制字符常用但容易翻车的替代方案是换行符%0a、制表符%09、回车符%0d甚至垂直制表符%0b。在HTTP请求层这些字符会被URL编码后传输如果后端没有做严格的字符白名单校验它们可能原样进入SQL语句。在MySQL里这些控制字符和空格一样都能作为词法边界1%0aunion%0aselect%0ausername,password%0afrom%0ausers但它有个致命弱点如果后端代码用了参数化查询接口预处理语句在编译时会把特殊字符直接转义处理同时很多框架在接收HTTP数据时会自动剥离控制字符。所以这个方案只对“半吊子”过滤系统有效在严谨的防护下会直接失败。2.5 绕过的组合策略不要死磕单一方案我踩过的最大坑是把所有希望寄托在某一种替代字符上。实际上真实环境里的过滤规则往往是好几层叠加的先换掉空格再换掉union和select有的甚至把/*和*/也过滤掉了。这时候的思路应该是“维度重组”你有没有办法让关键字在语义上不变、但在字面上变了大小写混写、注释切片、编码解码有没有办法让过滤规则漏掉某一个维度比如把union拆成un/**/ion再配合内联注释有没有办法完全绕开这个注入点换一个参数、换一种报错方式、走另一个接口实战经验是先收集目标用的是什么数据库再针对性选绕过组合。MySQL用内联注释优先Oracle考虑chr()拼接SQL Server可以尝试N前缀和十六进制编码没有一种银弹能通吃所有场景。3. 完整实操手工注入一次被过滤空格的靶场3.1 环境准备与注入点确认为了讲清楚整个流程我在本地搭了个模拟环境一个PHPMySQL的靶场脚本接收id参数并直接拼接进SQL查询中间加了一层str_replace( , , $id)的过滤函数。靶场地址http://localhost:8081/sqli.php?id1数据库MySQL 5.7过滤规则空格替换为空字符串其余字符不限制第一步是确认注入点。先用id1正常访问返回用户名为admin的结果。然后提交id1页面报错说明单引号进入了SQL语句并破坏了原有结构。再用id1 and 11测试发现返回为空——因为空格被过滤后实际执行的语句变成了and 11的无限拼接导致逻辑异常。当我改用id1/**/and/**/11时页面恢复正常返回的是用户名为admin的原始记录。到这里已经确认空格被过滤但/**/可以正常工作。3.2 判断字段数量order by 的替代写法常规情况下用order by N判断字段数但order未被过滤的情况下可以直接写成1/**/order/**/by/**/1 1/**/order/**/by/**/2 1/**/order/**/by/**/3从字段1到字段3都正常问题在字段4上1/**/order/**/by/**/4页面返回500错误说明查询只有3个字段。这一步的意义在于union select后面的字段数量必须和原查询一致否则语句直接报错。3.3 确定回显位置找“显示”出来的字段知道有3个字段后下一步是确认哪些字段能直接展示在页面上。使用联合查询的经典玩法1/**/union/**/select/**/1,2,3但这个语句在我测试时返回了空的记录原因是原查询的第一行数据被union去重了得让前半段查询的结果为空。这里有一个很常见的手工小技巧把原始查询的id改成不存在的值比如-1/**/union/**/select/**/1,2,3或者999/**/union/**/select/**/1,2,3这样原始查询返回空结果集联合查询的数据就会直接显示出来。页面最终展示的是2和3两个数字说明字段2和字段3是回显点。3.4 枚举数据库与数据表知道了回显位置接下来就能拉数据了。先确认当前数据库名称-1/**/union/**/select/**/1,database(),3页面显示数据库名sqli_test。接着枚举所有表名用group_concat把多行结果拼成一行方便展示-1/**/union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schemadatabase()注意这里有个细节table_schemadatabase()的等号左右没有空格但依然可以正常执行。因为SQL的语法并不要求等号周围必须有空格。这个特性就是绕过空格过滤的天然优势——很多运算符、括号、点号天然具备分隔能力。结果查出两张表users和logs。正常业务肯定先看users继续枚举列名-1/**/union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_nameusers这里有一个新的问题where table_nameusers中包含的空格被过滤后只剩where/**/table_nameusers必须用/**/补齐位置。最终结果拿到id,username,password三列。3.5 导出账号密码数据剩下的就是常规操作了直接把目标列搬出来-1/**/union/**/select/**/1,group_concat(username,0x3a,password),3/**/from/**/users其中0x3a是冒号的十六进制编码用于在拼接字段之间插入分隔符。显示结果里能看到账号和密码的哈希值。整个注入流程到此结束。3.6 自动化辅助用Burp Suite改造请求手工操作适合理解原理实际做测试时效率太低。我习惯用Burp Suite的Repeater功能保存一个模板请求然后把id参数值改成不同的Payload直接调试。比如在Burp里抓取原始GET请求替换id参数为上面的-1/**/union/**/select...每次改一下group_concat表达式就能快速遍历表名和列名。也可以把SQLMap拉进来做半自动测试但需要在请求配置里指定--tamperspace2comment这个tamper脚本的作用就是自动把空格替换成/**/。不过SQLMap的自动化替换有时不够聪明如果遇到多层过滤还得配合--tamper自定义脚本。我建议新手先手工跑通一遍流程再用工具提速不然出了问题完全不知道怎么排查。4. 实战高频问题与排查思路4.1 绕过之后还是报错问题出在哪第一次用/**/替代空格时最常见的报错是说SQL语法错误。排查时先别急着改Payload把最终执行的SQL语句打印出来看一看。靶场里可以临时加一句var_dump($sql)在生产环境里则可以用MySQL的general_log或者DBMS的查询日志来捕获实际语句。我遇到过一个案例注入点在LIKE子句里直接用/**/替代空格时发现LIKE后面必须跟着非通配字符否则解析器不认识。这里的解决办法是在LIKE子句内构造%和关键字或者把注入点挪到WHERE子句本质上是通过换位置绕开语法限制。4.2 关键字也被过滤了怎么办如果过滤规则不仅滤空格还滤了union和select那么上一节的方法全部失效。这时候需要分层突破大小写混写UnIoN、关键字内嵌注释un/**/ion、数据库特有的编码函数MySQL的char()、concat()甚至是完全放弃union路线改用报错注入或布尔盲注。举个报错注入的例子MySQL里用extractvalue报错信息携带数据1/**/and/**/extractvalue(1,concat(0x7e,database()))这条语句不依赖union也不依赖select只靠函数报错时把参数内容显式打印出来。配合concat(0x7e,...)绕过了关键字过滤直接拿到数据库名。4.3 注入点有多个如何选最优的一个页面往往有不止一个参数可以操作。我习惯把所有可能传参的位置都列出来逐一测试过滤强度。比如同是id和page两个参数可能id走了“严格过滤参数化查询”但page直接拼接进了SQL并且没有清理空格。实际测试中我在某次授权项目里遇到过username被严格过滤、但nickname只过滤了空格的情况。最终利用nickname完成了写入数据的过程。多参数测试的价值就是避免在单一参数上撞得头破血流。4.4 分号、逗号也被过滤的极端情况过滤分号会影响堆叠注入过滤逗号则会影响union select 1,2,3这种写法。应对思路是换个数据提取方式利用join语句代替逗号分隔的列列表利用limit代替子查询中的逗号参数。比如列数判断可以不依赖order by改用报错函数或直接观察回显变化取数据时可以用group_concat把多行并成一行减少逗号的出现次数。极端情况下还可以用case when来做布尔盲注几乎不涉及任何敏感字符。4.5 踩坑实录URL编码惹的祸新手最容易犯的错误是把Payload直接填进浏览器的地址栏却忘了浏览器会自动做一次URL解码而服务端又做了一次解码导致%0a变成了真正的换行符%27变成了单引号。这看起来是好事但有时候双重重编码会让%本身也被过滤掉比如目标对%做了替换。我做过一次排查同样的Payload在Burp里能成功放到Python脚本里却一直报错最后发现requests库默认不对路径参数做编码我把%27手写成了%2527服务端拿到的就变成了字面上的%27而不是单引号。这个问题特别隐蔽排查了半小时才发现。5. 从攻防视角再想一遍这种过滤到底防住了什么5.1 为什么简单过滤管不住注入空格过滤在防御者看来属于“输入黑名单”的思路它假设攻击者输入的字符可控、模式固定。可SQL注入的变形能力远远超出字面匹配的范畴一个空格可以变成注释符、括号、控制字符、运算符周边语义等无数形态。黑名单永远在追赶攻击者这也是为什么安全性理论一直在强调白名单和参数化查询。从防守方角度看这个案例的教训是空格过滤不能作为独立的防御手段。如果没有参数化查询仅仅“删空格”就等于给SQL注入加了一道薄薄的伪装反而容易让开发者误以为系统已经是安全的从而忽略更深层的漏洞。5.2 防御方真正应该做的事正确做法是把过滤升级为“结构化处理”使用预处理语句PreparedStatement绑定参数让数据永远无法改变查询结构对输入做类型校验数字参数就强制intval处理字符串参数就走转义机制最后才考虑加一层WAF规则用于在数据库层之外拦截明显的攻击特征。我见过不少自研CMS明明在关键查询里做了参数化却在某个附属模块里图省事直接拼SQL结果前面对空格的过滤形同虚设。这里的经验是防御不仅要考虑单点的规则还要做全局的代码审计确认所有SQL查询都走同一条安全通道。5.3 学习建议与后续扩展如果你刚开始接触SQL注入我的建议是先把本章的靶场例子在本地完整跑一遍把每种绕过方式都试一遍观察最终SQL语句如何在服务端呈现。之后再进阶到时间盲注、报错注入、DNS外带、堆叠注入每一类技术背后都对应不同的解析逻辑和过滤思路。从本项目往后还可以扩展到NoSQL注入、ORM注入、存储过程注入等场景。记住一个核心判断任何输入只要有机会影响执行语句的结构就存在被利用的可能。过滤空格只是其中一道关卡你的目标是不断思考“如何让字符串在语法层面保持完整”。我个人在实际操作中的体会是SQL注入的实战能力不是靠背Payload堆出来的而是靠对数据库词典、执行计划、查询逻辑的深入理解。每当你觉得某个过滤无解时回到SQL语句本身问一句“这里真的需要一个空格吗”往往答案就藏在语法的细节里。
延伸阅读

更多相关文章

2026/10/14 13:54:46

SpringBoot+MyBatis解决Mapper扫描问题全攻略

1. 问题背景与现象分析在基于SpringBootMyBatis的技术栈开发中,我们经常会遇到一个经典问题——明明已经正确编写了Mapper接口并添加了Mapper注解,但项目启动时却报"找不到Bean定义"的错误。这种情况在实际开发中出现的频率相当高,…

2026/10/14 13:49:45

Python音频标签提取:MP3与M4A元数据解析实战

简介:这是一份面向 Java 开发者的音频元数据解析工具包,专注于从 MP3 与 M4A 文件中提取标签信息,适合需要批量读取音乐标签、构建媒体库或做音频处理应用的初中级开发者。核心抽象类 AudioInfo 提供统一入口,MP3Info 与 M4AInfo …

2026/10/14 15:15:12

Qt实现Windows远程控制:TCP自定义协议与抓屏编码实战

简介:这是一套基于Qt框架实现的Windows平台远程控制软件源码与可执行程序,面向具备一定C与Qt基础的开发者、课程设计或毕业设计参考者,用于学习网络通信、屏幕图像采集与远程桌面控制等核心机制。压缩包共40个文件,约6.08MB&#…

2026/10/14 15:15:12

CAS+SpringMVC+Shiro单点登录集成实战与避坑指南

简介:本资源是一套将CAS、SpringMVC与Shiro三者整合实现单点登录(SSO)的完整实践示例,面向具备一定Java Web基础、希望深入理解SSO认证流程与安全框架集成的开发者。内容围绕CAS服务端与客户端配置、Shiro的CasRealm与CasFilter定…

2026/10/14 15:15:12

Tomcat与国产中间件技术选型对比与实践

1. 中间件技术选型的时代背景在数字化转型浪潮中,Web中间件作为企业级应用的基石,其技术选型直接影响着系统稳定性、开发效率和运维成本。过去十年间,以Tomcat为代表的开源中间件与国产中间件形成了明显的技术分野。我在实际项目中发现&#…

2026/10/14 15:15:12

基于Transformer的聊天机器人Python源码实战:环境搭建、训练与推理

简介:这是一份基于Transformer模型构建的聊天机器人Python源码包,面向具备一定Python基础、希望动手实践对话系统或完成课程设计的开发者。使用者输入文本后,系统即可根据语义生成相应回答,实现日常聊天功能。资源包共367个文件&a…

2026/10/14 15:15:12

Tomcat与国产中间件技术选型及性能对比分析

1. 中间件技术选型的核心考量在构建企业级应用系统时,Web中间件的选择往往直接影响系统的稳定性、性能表现和长期维护成本。作为从业十五年的架构师,我见证过从传统商业中间件到开源解决方案的技术演进,也参与过多次国产化替代的技术评估。To…

2026/10/14 15:10:11

SpringBoot电商系统开发:实体专辑购买平台实战

1. 项目背景与核心价值最近帮几个计算机专业的学生指导毕业设计,发现不少同学对电商类项目情有独钟。其中"环星实体专辑购买系统"这个选题特别有意思——既结合了当下音乐消费的热潮,又能完整覆盖SpringBoot技术栈的实践要点。作为一个经历过三…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/14 0:00:33

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:33

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:33

SCORM课件演示与测试全解析:从API通信到自动化验证

简介:这是一套遵循SCORM标准的交互式课件演示与测试资源,面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者,用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件,约12.07MB,以swf动…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑