SQL注入绕过技巧:空格与逗号过滤的实战解决方案

发布时间:2026/7/6 9:35:49

SQL注入绕过技巧:空格与逗号过滤的实战解决方案 1. 项目概述当SQL注入遇上字符过滤在渗透测试和CTF比赛中我们经常会遇到一些“加了料”的靶场或真实环境它们为了增加难度或模拟初级防护会对SQL注入语句中的特定字符进行过滤。其中空格和逗号可以说是最常被“关照”的两个。空格用于分隔SQL关键字和子句逗号则广泛用于函数参数分隔、LIMIT子句以及UNION SELECT的字段枚举。一旦这两个字符被过滤很多新手可能会觉得无从下手经典的注入语句直接“哑火”。但实际上这种过滤往往只是增加了门槛远未达到彻底防御的效果。今天我就结合自己多年的实战和打靶经验系统性地梳理一下当空格和逗号被过滤时我们手里还有哪些“牌”可以打以及这些绕过技巧背后的原理和适用场景。无论你是正在攻克CTFHub、DVWA、Pikachu靶场还是研究MyBatis、Oracle等特定环境的注入理解这些底层技巧都能让你事半功倍。2. 核心绕过思路与原理拆解在深入具体技巧之前我们必须先建立一个核心认知绕过过滤的本质是寻找功能等效的替代方案。WAFWeb应用防火墙或简单的字符串过滤逻辑通常是基于正则表达式匹配并替换或拦截特定的“危险字符串”比如/\s/匹配所有空白字符包括空格、制表符、换行符或者直接过滤逗号,。我们的目标就是构造出能实现相同SQL语义但“长相”却不在黑名单里的payload。2.1 空格过滤的绕过不只是/**/一提到绕过空格过滤很多人第一反应就是使用注释符/**/。这确实是最经典、最有效的方法之一因为/**/在SQL中是多行注释但在此处被我们用作“填充物”起到了分隔关键字的作用。例如union select被过滤可以写成union/**/select。但我们的武器库远不止于此。原理层面SQL解析器在解析语句时需要分隔符来区分不同的词元Token。空格是最常见的分隔符但并不是唯一的。任何能被解析器忽略的“空白”或具有分隔功能的字符都可以尝试。这包括了注释符/**/、/*!*/内联注释MySQL特有。其他空白字符Tab键%09、换行%0a、回车%0d。在URL编码中这些字符有时能绕过简单的基于空格的过滤。括号()括号本身具有很高的语法优先级也能起到分隔作用尤其在函数调用或子查询中。其他特殊符号例如在SQL中作为字符串连接符时在某些数据库如MSSQL中select可能被拼接成select但更常用于绕过关键字过滤间接解决空格问题。一个关键的注意事项是上下文。/**/在绝大多数情况下是可靠的但在一些极端严格的过滤场景或者某些数据库的特定版本中连续的/**/也可能被加入黑名单。因此掌握多种方法并灵活组合才是王道。2.2 逗号过滤的绕过重构语句逻辑逗号在SQL注入中扮演着多个角色因此它的绕过需要根据其出现的具体场景来针对性处理主要分为三类函数参数分隔如substr(database(), 1, 2)中的逗号。LIMIT子句如limit 0,1。UNION SELECT字段枚举如union select 1,2,3。对于第1点和第2点SQL标准或特定数据库提供了无需逗号的替代语法。对于第3点我们则需要改变注入的整个思路可能转向基于布尔或时间的盲注或者在UNION注入时利用JOIN等技巧。核心思路是查阅官方文档寻找功能相同但语法不同的表达方式。这要求我们对SQL语法有更深入的了解而不仅仅是死记硬背payload。3. 空格过滤的实战绕过技巧详解下面我们进入实战环节看看具体有哪些方法可以把被过滤的空格“变”回来。3.1 注释符大法/**/ 与 /!/这是最直接的方法。用注释符代替所有空格位置。-- 原始语句 union select 1,2,3 from admin -- 绕过后 union/**/select/**/1,2,3/**/from/**/admin在MySQL中还可以使用内联注释/*!*/其中的代码会被MySQL执行但其他数据库可能视为普通注释。这有时可以绕过一些针对/**/的简单过滤。union/*!50000select*/1,2,3这里的50000表示MySQL版本号大于等于5.00.00时才执行其中的语句可用于版本条件判断但作为空格替代时通常不写版本号或写一个很低的版本号如/*!0*/。实操心得在某些靶场如一些CTF题目中可能会过滤/**/中的/或*。这时可以尝试双重编码或大小写变形如%2f%2a%2a%2fURL编码或/*\**/但成功率取决于过滤逻辑的严谨程度。3.2 利用空白符编码Tab、换行与回车当应用层过滤了空格字符ASCII 32但未考虑其他空白字符时我们可以尝试Tab (%09):union%09select换行 (%0a):union%0aselect回车 (%0d):union%0dselect多个空格有时过滤规则是/\s/但只替换一次用两个空格 可能绕过。或者使用%a0HTML中的不换行空格在某些解析环境下也能被当作分隔符。这种方法在GET请求的URL参数中尝试较多因为参数值通常会被URL解码后再送入SQL查询。在POST请求的Body中也可以直接插入这些字符的原始字节。3.3 括号的妙用括号()在SQL中优先级极高可以用来包裹参数或子查询自然形成分隔。在函数中select(user())from(admin)。这里from (admin)的写法是合法的括号将表名包裹与前面的函数形成了分隔。在条件中or(1)(1)。这等价于or 11。结合SELECT子查询这是非常强大的一种技巧尤其当union和select被分开过滤时。例如需要union select 1,2,3可以尝试union(select(1),(2),(3))或者更复杂地利用SELECT语句本身可以返回结果集的特性?id1 union select * from ((select 1)a join (select 2)b join (select 3)c)这条语句通过JOIN构造了一个包含三列的虚拟表完全避免了在顶层SELECT后使用逗号。虽然引入了join关键字但很多时候join并不在过滤名单中。3.4 利用数学运算符或字符串连接符在极少数情况下甚至可以用、-、||等符号来“粘合”语句但这非常依赖于数据库类型和上下文。MSSQLselect可以拼接成select但这主要用于绕过关键字过滤。对于空格可以尝试unionselect这里的在URL中表示空格但传入MSSQL后可能被解释为字符串连接结果不可预测需测试。Oracle||是字符串连接符同样主要用于关键字拆分。这种方法风险较高极易造成语法错误通常作为最后的选择并且需要配合大量测试。4. 逗号过滤的针对性绕过方案现在我们来攻克更棘手的逗号过滤。我们必须分场景讨论。4.1 绕过函数参数中的逗号常见于substr()、mid()、if()等函数。substr()和mid()的from ... for语法-- 原语句 substr(database(), 1, 2) mid(database(), 1, 2) -- 绕过语句 substr(database() from 1 for 2) mid(database() from 1 for 2)这是SQL标准语法在MySQL、PostgreSQL等数据库中普遍支持。from指定开始位置for指定长度。limit的offset语法-- 原语句 select * from users limit 0,1 -- 绕过语句 select * from users limit 1 offset 0limit 1 offset 0表示从第0条记录之后取1条记录效果与limit 0,1完全相同。if()函数的替代if(condition, value_if_true, value_if_false)被过滤时可以使用case when语句。-- 原语句用于时间盲注 if(ascii(substr(database(),1,1))100, sleep(2), 0) -- 绕过语句 case when ascii(substr(database() from 1 for 1))100 then sleep(2) else 0 endjoin绕过union select中的逗号如前所述当union select 1,2,3中的逗号被过滤时可以尝试?id-1 union select * from ((select 1) a join (select 2) b join (select 3) c)这里通过给每个子查询设置别名a, b, c然后用join将它们横向连接形成了一个三列的临时结果集。union后面跟的是一个完整的select * from ...语句完美避开了在select后直接列举字段。4.2 绕过比较操作符与逗号的组合过滤在盲注中我们经常使用substr(database(),1,1) a这样的比较。如果比较符,,和逗号同时被过滤情况就复杂了。使用greatest()/least()函数绕过比较符-- 原语句判断第一个字符的ASCII码是否大于64 ascii(substr(database(),1,1)) 64 -- 绕过语句 greatest(ascii(substr(database() from 1 for 1)), 64) 64如果greatest(值, 64)的结果等于64说明第一个参数我们的目标值小于或等于64。如果想判断是否大于64可以结合least()或使用 64的逆否逻辑但通常需要调整payload逻辑。使用in()或between and绕过等号-- 原语句 id1 -- 绕过 id in(1) id between 1 and 1这对于过滤等号非常有效但需要注意in后面的括号里如果只有一个值仍然需要逗号虽然这里没有其他值所以如果逗号被彻底过滤in的单值用法也受限。between and则完全不需要逗号。使用like或regexp进行模糊匹配在盲注中可以逐位用like匹配字符。substr(database() from 1 for 1) like a% substr(database() from 1 for 1) regexp ^a这不需要等号但通常需要配合substr而substr的逗号问题需要用from for解决。4.3 无逗号盲注的终极方案位运算与逻辑重构当所有涉及逗号的函数和语法都被封死时我们可能需要回归盲注的本质通过布尔条件真/假或时间延迟来推断数据。这时可以彻底抛弃substr采用基于位运算的方法。核心思想任何一个字符ASCII码都可以用8位二进制表示0-255。我们可以通过多次查询用位运算,|,,来逐位判断其二进制值。-- 假设我们要判断database()第一个字符的ASCII码 -- 判断第1位最高位是否为1 and (ascii(substr(database() from 1 for 1)) 7) 1 1 -- 由于有逗号和比较符需要改写。假设只能用 from for 和 greatest and greatest(ascii(substr(database() from 1 for 1)) 7, 1) 1 -- 但优先级可能有问题且仍有逗号。终极方案使用十六进制和ord函数组合但非常复杂。实际上在如此严格的过滤下手工构造payload极其困难通常会借助自动化工具如sqlmap的tamper脚本来生成。但理解原理有助于我们编写自己的tamper脚本。一个更可行的思路是利用make_set()或elt()函数MySQL-- elt(n, str1, str2, ...): 返回第n个字符串。需要逗号。 -- make_set(bits, str1, str2, ...): 根据bits的二进制位返回对应的字符串组合。也需要逗号。当逗号被过滤时这些函数也失效了。因此在极端情况下最稳健的方法是转向基于时间的盲注并利用if()的case when替代和substr的from for语法构造一个不含逗号的延时判断。虽然payload会变得冗长但理论上是可行的。5. 综合案例与实战演练让我们结合几个常见靶场和场景看看如何综合运用上述技巧。5.1 案例一CTFHub技能树之“过滤空格”这类题目通常只过滤空格。我们的策略很直接用/**/或%0a等替换所有空格即可。原注入点?id1 union select 1,2,database()过滤后?id1/**/union/**/select/**/1,2,database()如果/**/也被过滤尝试%0a?id1%0aunion%0aselect%0a1,2,database()。或者使用括号?id1 union(select(1),(2),(database()))。5.2 案例二DVWA SQL Injection (Impossible) 的变种假设一个变种关卡过滤了空格和逗号。我们需要获取users表中的第一个用户名和密码。判断注入点与列数使用order by判断列数时order by 3中的逗号是数字的一部分没问题。但order by本身需要空格用/**/绕过?id1/**/order/**/by/**/3#。联合查询union select需要空格用/**/。字段枚举的逗号是难点。我们可以使用join法。?id-1/**/union/**/select/**/*/**/from/**/((select/**/1)a/**/join/**/(select/**/2)b/**/join/**/(select/**/3)c)#成功联合后确定回显点在2和3。爆数据我们需要从users表查user和password。由于select后不能直接跟user,password有逗号我们需要再次借助join或者使用子查询拼接。方法AJOIN法将两个字段分别放在不同的子查询中然后join。但join默认是横向拼接增加列而我们需要的是将两个字段放在同一列回显这需要调整思路。实际上我们可以分别注入一次爆一个字段。-- 爆user字段 ?id-1/**/union/**/select/**/*/**/from/**/((select/**/user/**/from/**/users/**/limit/**/1/**/offset/**/0)a/**/join/**/(select/**/2)b)#这里limit 1 offset 0替代了limit 0,1。我们让user字段作为第一列第二列固定为2回显位。这样在页面回显位置1就能看到用户名。方法BCONCAT函数如果concat函数可用且其参数内的逗号未被过滤有时过滤是针对SQL语法层面的逗号函数参数内的逗号规则可能不同但通常一致可以concat(user,0x3a,password)但这里仍有逗号。如果concat的逗号也被过滤则concat不可用。方法C双查询报错注入如果报错注入可用且updatexml或extractvalue函数的第二个参数XPath中的逗号可以用from for语法吗不行这两个函数的参数列表必须用逗号分隔没有替代语法。所以报错注入在此场景下可能受阻。踩坑记录在一次内部攻防演练中遇到一个过滤了空格、逗号和union的环境。最终方案是使用基于时间的盲注配合case when和substr() from for语法并利用benchmark()或sleep()函数。Payload长得像天书但最终成功跑出了管理员密码。工具如sqlmap在这种情况下往往不如手工构造灵活因为需要自定义tamper脚本处理多重过滤。5.3 案例三绕过MyBatis中#{}的过滤这是一个特殊的场景。MyBatis中#{}是预编译参数占位符能有效防止SQL注入。但题目说“绕过MyBatis#号进行sql注入”通常指的是开发错误地使用了${}字符串拼接而非#{}或者在某些动态SQL标签如if、foreach中由于拼接不当引入了注入。此时注入点的处理方式和普通注入一样空格和逗号过滤的绕过技巧完全适用。关键在于找到那个使用${}的脆弱参数。6. 防御视角与总结反思从攻击中学习防御。了解了这么多绕过技巧作为开发或安全人员我们应该如何构建更坚固的防线永远使用预编译语句Prepared Statements这是唯一从根本上解决SQL注入的方法。无论是MyBatis的#{}、JPA的命名参数还是JDBC的?占位符只要正确使用攻击者输入的SQL语法永远不会被数据库引擎执行。上述所有绕过技巧在预编译面前都无效。严格的输入验证与白名单如果业务上某些参数只能是数字那就用正则/^\d$/严格校验非数字直接拒绝。对于表名、列名等如果必须动态拼接应使用白名单机制。避免动态拼接SQL这是万恶之源。尽量不要用字符串拼接的方式来构造SQL语句尤其是在Java中拼接where条件。WAF的局限性本文讨论的很多技巧恰恰说明了基于正则匹配的黑名单WAF很容易被绕过。WAF可以作为一道补充防线但绝不能作为主要或唯一防线。最小权限原则数据库连接账户不应具有DROP、FILE等高危权限且只授予访问必要数据库和表的权限。这样即使发生注入危害也能被限制。回过头看空格和逗号过滤的绕过是一场关于SQL语法深度的博弈。攻击者不断寻找语法中的“等价替换”而防御者则需要理解真正的安全不是过滤几个字符而是采用正确的编程范式。对于安全研究者而言掌握这些技巧是为了更好地测试和加固系统对于开发者而言理解这些攻击手段则能时刻提醒自己写出更安全的代码。在实战中面对过滤保持冷静系统性地尝试各种替代方案从最简单的/**/开始逐步深入到join、from for、位运算你总能找到那条通往数据之路的缝隙。

相关新闻