SQL注入绕过技术全解析:从基础字符替换到高级WAF对抗

📅 2026/7/6 9:36:47
SQL注入绕过技术全解析:从基础字符替换到高级WAF对抗
1. 项目概述为什么绕不过去的“绕过”是核心干了这么多年安全测试我越来越觉得SQL注入的攻防本质上是一场“猫鼠游戏”。开发人员绞尽脑汁地部署各种过滤规则而攻击者则像解谜一样寻找这些规则中的缝隙。很多人学SQL注入上来就是union select 1,2,3但真到了实战尤其是面对一些有基础防护的站点这套标准拳法往往第一招就被挡回来了。这时候各种“绕过手法”就成了决定性的胜负手。它考验的不是你对SQL语法有多熟而是你对目标防护逻辑的理解深度、对数据库特性的掌握广度以及临场“变形”Payload的创造力。简单来说SQL注入绕过就是针对应用程序或WAF设置的过滤、拦截规则通过编码、等价替换、利用数据库特性等手段构造出能够成功执行恶意SQL语句同时又能“骗过”防护机制的Payload。这不仅仅是黑客的伎俩更是安全从业者、开发人员必须深入了解的领域。只有知道攻击者会怎么绕你才能写出真正“防得住”的代码。接下来我会结合十多年的实战踩坑经验把这些手法掰开揉碎了讲让你不仅知道“怎么绕”更明白“为什么能绕”。2. 绕过手法的核心思路与分类拆解在深入具体手法之前我们必须建立一个清晰的认知框架。所有的绕过手法本质上都是针对“检测点”和“执行点”的差异做文章。2.1 核心对抗逻辑检测 vs. 执行防护方WAF或应用层过滤的检测逻辑和我们最终要让数据库执行的SQL逻辑是两条线。绕过成功的关键就在于构造一个Payload让它在这两条线上表现出不同的“形态”。在检测线上Payload看起来是“无害”的或者不符合黑名单规则。例如过滤了select我就写成SeLeCt过滤了空格我就用/**/。在执行线上经过数据库解析引擎处理后Payload能“恢复”成原本有效的恶意SQL语句。例如%df%5c%27在PHP的addslashes函数看来是加了反斜杠的引号但在MySQL的GBK编码看来%df%5c被合并成了一个汉字后面的%27单引号就被成功“释放”了出来。2.2 手法分类全景图根据对抗的过滤类型我们可以把绕过手法分为以下几大类这能帮助我们在遇到具体场景时快速定位思路过滤/拦截目标常见绕过思路核心原理特定关键字(如 select, union, and)大小写混合、双写、等价函数/符号替换、编码利用检测规则的不完备性如只检测小写或字符串替换的缺陷。空格与注释符使用/**/、%09(Tab)、%0a(换行)、()括号、反引号利用数据库解析引擎能识别多种空白分隔符的特性。引号与特殊字符宽字节注入、十六进制编码、char()函数利用字符编码转换过程中的“吞并”效应或使用无需引号的表达方式。等号与逗号使用like、in、rlike、regexp使用from...for...代替substr()中的逗号寻找执行效果相同但字符不同的语法糖。内联数据过滤使用内联注释/*!...*/、注释符分割/**/利用数据库对注释内容的特殊处理规则如版本特异性执行。逻辑运算符and-,or- 注意没有任何一种绕过手法是万能的。它的有效性完全取决于目标系统具体的过滤规则、数据库类型、中间件版本和编码环境。实战中往往是多种手法组合使用。3. 基础字符绕过从空格与注释开始这是最古老也最经典的绕过场景。很多初级防护脚本喜欢直接过滤空格和常见的--、#注释符以为这样就能卡住注入。3.1 空格的“替身演员”们当空格 被过滤时数据库其实还认识很多其他“空白符”或具有分隔功能的符号。注释符/**/这是MySQL中最常用的空格替代品。select/**/user()完全等效于select user()。在URL中注意编码为%2f%2a%2a%2f。-- 原语句 union select 1,2,database() -- 绕过变形 union/**/select/**/1,2,database()制表符与换行符%09Tab和%0a换行在数据库解析时也常被视作分隔符。这在过滤了/**/但未过滤这些不可见字符时有效。http://target.com?id1%09union%09select%091,2,3--括号()括号不仅能改变运算优先级在特定语境下也能起到分隔作用尤其是在函数参数或条件判断中。例如mid(database()from(1)for(1))就避免了使用逗号分隔参数。-- 使用逗号 substr(database(),1,1) -- 绕过逗号也间接避开了空格 substr(database() from 1 for 1) -- 甚至可以全部用括号包裹 (select(user())from(dual))反引号在MySQL中反引号用于包裹数据库名、表名、字段名等标识符。在特定位置它也能起到分隔作用但适用场景较窄不如前几种通用。3.2 注释的妙用与内联注释绕过注释不只是用来注释。--、#、/*...*/这些注释符在绕过中扮演着关键角色。分割关键字当union select被整体过滤时可以用注释将其拆开。union/**/select可能被拦截但union/*任意内容*/select可能就绕过去了因为WAF的正则可能是/union\sselect/i。内联注释/*!...*/这是绕过云WAF如安全狗、云盾的利器。MySQL为了保持兼容性会执行/*!...*/中的语句。更妙的是/*!50001...*/这种格式只有当MySQL版本大于等于5.00.01时才会执行其中的内容。这常被用来欺骗WAFWAF可能因为语法奇怪或不识别而放行但目标数据库却能正常执行。/*!50000union*//*!50000select*/ 1,2,3 /*!union*//*!select*/ 1,2,user/*!()*/实操心得很多WAF的规则库是基于常见威胁特征对于这种数据库特有的、看似注释的语法解析不充分。在测试时可以尝试在关键字中间插入/*!*/成功率不低。4. 关键字绕过变形、伪装与替换过滤关键字是最直接的防护但也是绕过手法最丰富的地方。4.1 大小写绕过最简单的绕过。如果防护方只用preg_match(/select/i)那么SeLeCt、SELECT、SeLecT都能轻松绕过。但现在的防护很少犯这种低级错误通常都会用i标志不区分大小写。所以这招多用于一些自写的、不严谨的过滤函数。4.2 双写绕过这是针对字符串替换型过滤的经典克星。如果代码是$id str_replace(select, , $id);那么输入selselectect经过替换后中间的select被移除两边的sel和ect正好拼接成新的select。// 漏洞代码示例 $id $_GET[id]; $id preg_replace(/select/i, , $id); // 只替换一次 $sql SELECT * FROM users WHERE id$id; // 攻击者输入1 ununionion selselectect 1,2,3 -- // 替换后1 union select 1,2,3 --注意事项双写绕过的前提是替换只执行一次。如果代码使用了preg_replace的全局替换默认就是全局但正则写得不严谨或者替换逻辑是循环直到找不到关键字为止双写就会失效。不过很多程序员在写的时候确实只考虑了一次替换。4.3 等价函数与符号替换这是技术含量较高的部分需要你对SQL函数和语法有深入了解。逻辑运算符替换and-,or-||。在MySQL中和||的行为与and、or几乎一致但优先级略有不同使用时最好加括号。1 11 -- 1 || 12 --比较操作符替换被过滤试试like、rlike、regexp、in。-- 判断条件 or 11 -- // 被拦截 or 1 like 1 -- // 可能绕过 or 1 in (1) -- // 可能绕过 -- 盲注时判断字符 substr(database(),1,1) a -- // 被拦截 substr(database(),1,1) like a -- // 可能绕过 substr(database(),1,1) regexp ^a -- // 可能绕过函数替换这是盲注和报错注入时的关键。sleep(5)-benchmark(10000000,md5(test))。benchmark函数通过重复执行一个表达式来延时。substr(str,1,1)-mid(str from 1 for 1)。mid是substr的同义词且支持from...for...语法避免逗号。concat(str1,str2)-concat_ws(,str1,str2)。concat_ws用指定分隔符连接字符串当concat被过滤时可用。updatexml()和extractvalue()是报错注入的“哼哈二将”功能类似可以互相替换尝试。ascii()-hex()、bin()、ord()。用于将字符转换为数字形式进行比较避免引号。4.4 编码绕过编码是“瞒天过海”的好办法让Payload在传输和检测时一个样到数据库执行时又是另一个样。双重URL编码如果应用层只做了一次URL解码然后进行关键字检测那么双重编码可能绕过。例如select一次编码是%73%65%6c%65%63%74二次编码是%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34。检测时看到的是%25%37...可能不匹配select规则但经过服务器两次解码后又变回了select。十六进制编码这是绕过引号过滤的终极武器之一。MySQL中0x开头的字符串会被当作十六进制值。你可以把整个字符串用十六进制表示。-- 原语句需要单引号 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadvwa -- 绕过无需单引号 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema0x64767761你可以用Python或在线工具轻松转换dvwa-0x64767761。这招在magic_quotes_gpc开启或addslashes函数转义了引号时特别管用。Unicode编码主要针对IISASP/ASP.NETSQL Server的环境。IIS服务器会自动对URL中的Unicode编码进行解码。例如select中的s可以用%u0073表示。se%u006cect可能绕过对select的检测。5. 高级场景宽字节注入与特殊符号绕过5.1 宽字节注入字符编码的“魔术”这是我个人认为最精妙、也最体现底层原理的绕过手法。它发生在数据库连接使用GBK、GB2312等宽字符集而Web应用使用UTF-8等单字节字符集且开启了转义如addslashes的情况下。原理PHP的addslashes函数会在单引号前加一个反斜杠\变成\使引号失去作用。在GBK编码中一个汉字由两个字节组成。如果反斜杠\ASCII码5c前面碰巧有一个ASCII码大于128的字节比如%dfMySQL客户端就会认为这是一个GBK汉字的前半部分从而将%df和5c“吞并”成一个汉字导致后面的%27单引号被单独释放出来成功闭合。攻击者输入%df addslashes后%df\ URL编码表示%df%5c%27 MySQL (GBK) 解读%df%5c 被当作一个汉字例如“運”%27 被当作独立的单引号 最终语句... id運 ... // 单引号成功闭合可以继续注入实战利用在测试时如果发现输入单引号被转义成\可以尝试输入%df。如果页面报错或显示与\时不同很可能存在宽字节注入漏洞。后续的注入Payload都需要在关键字符前加上%df或其他高位字节如%81-%fe。5.2 逗号过滤的应对策略逗号在注入中太常见了substr(database(),1,1)、limit 0,1。过滤了逗号怎么办对于substr/mid函数使用from...for...语法。substr(database(),1,1) -- substr(database() from 1 for 1) mid(database(),1,1) -- mid(database() from 1 for 1)对于limit子句使用offset语法。limit 0,1 -- limit 1 offset 0对于union select如果union select 1,2,3中的逗号被过滤会比较麻烦。一种思路是使用join语法进行横向拼接但构造复杂。更常见的做法是在能够控制多个参数时将Payload拆分到不同参数中避免使用逗号。对于order byorder by 1,2如果被过滤可以尝试order by (select 1),order by (select 2)但这依赖于具体场景。6. 实战组合拳与WAF绕过思路在实际渗透测试或CTF比赛中很少只靠一种手法就能成功。你需要像搭积木一样组合使用多种技巧。6.1 一个综合绕过案例假设一个场景目标过滤了select、union、空格、和单引号。判断注入点id1和id2返回不同但id1被转义成1\页面正常可能是字符型且加了转义。尝试宽字节id%df页面报错确认存在宽字节注入。后续Payload前都加%df。绕过空格使用/**/。Payload变为%df/**/and/**/11--绕过使用like。Payload变为%df/**/and/**/1/**/like/**/1--绕过select尝试双写。但这里过滤可能是删除我们先用/**/分割试试%df/**/un/**/ion/**/sel/**/ect/**/1,2,3--。如果还不行尝试内联注释%df/*!50000union*//*!50000select*/1,2,3--获取数据最终Payload可能长这样%df/*!50000union*//*!50000select*/1,group_concat(table_name),3/*!50000from*/information_schema.tables/*!50000where*/table_schema0x64617461626173655f6e616d65--这里同时用到了宽字节、内联注释、十六进制编码、注释符代替空格。6.2 针对云WAF的试探性思路云WAF规则强大但并非无懈可击。除了上述内联注释还有一些试探方向参数污染同一个参数传多次如?id1id2。不同中间件处理方式不同取第一个、取最后一个、拼接可能绕过WAF的检测而到达后端数据库的却是拼接后的恶意参数。非常规请求方式WAF可能对GET请求检测严格但对POST、PUT甚至JSON格式的POST请求检测较弱。协议层面干扰如分块传输编码Chunked Encoding、畸形的HTTP头部等可能干扰WAF的解析引擎导致其无法正确提取出要检测的参数。利用数据库特性不同数据库MySQL、MSSQL、PostgreSQL、Oracle的语法和函数差异巨大。WAF的规则可能主要针对MySQL用一些MSSQL特有的语法如waitfor delay 0:0:5可能直接绕过。重要警告上述针对WAF的试探手法仅限于授权的安全测试和CTF比赛环境。未经授权对他人系统进行测试是违法行为。7. 防御视角如何构建更安全的防线作为开发者了解了攻击者的绕过手法就应该知道如何构建更坚固的防御。根本解决使用参数化查询预编译语句这是唯一能从根本上杜绝SQL注入的方法。让SQL语句和用户输入的数据在数据库驱动层面就彻底分离。无论是MyBatis的#{}还是PHP的PDO预处理都应该作为强制规范。MyBatis的#和$#{}是参数占位符会预编译${}是字符串拼接存在注入风险。严禁在${}中放入用户可控输入。如果必须拼接则严格过滤白名单优于黑名单对于id、type这类有限取值范围的参数使用白名单验证如只允许数字或只允许[news,blog]中的值。统一编码与转义确保整个应用栈前端、后端、数据库连接使用统一的字符编码如UTF-8并在将数据送入SQL前使用数据库驱动提供的专属转义函数如mysqli_real_escape_string而不是通用的addslashes。最小化数据库权限应用连接数据库的账号只赋予其最小必需的权限SELECT、UPDATE等切勿使用root或sa等高级别账号。避免动态拼接SQL尽量不要用字符串拼接的方式来构造SQL语句尤其是拼接WHERE、ORDER BY、LIMIT等子句。如果业务上确实需要动态排序或筛选应在应用层将用户输入映射为安全的枚举值或字段名。8. 常见问题与排查技巧实录Q1我试了所有绕过方法Payload还是被拦截怎么办A首先确认注入点是否存在。用最简单的and 11和and 12测试。如果都被拦截可能是WAF行为尝试在正常参数后添加无关注释如?id1/*nameadmin*/看WAF是否对参数解析有误。其次考虑是不是注入类型判断错误数字型、字符型、搜索型。最后可能是Payload中有某些固定特征被WAF规则命中尝试大幅度调整Payload结构或者将注入步骤拆解到多个请求中如布尔盲注。Q2宽字节注入测试总是不成功A确认几个必要条件1) 数据库连接字符集是GBK家族GBK, GB2312, CP936。2) PHP开启了magic_quotes_gpc或使用了addslashes等转义函数。3) 你的输入位置在转义函数执行后还会经过一次字符集转换。可以在参数中输入一个中文字符看是否乱码或通过数据库报错信息推测字符集。Q3使用/**/注释符也被过滤了A尝试其他空白符如%09、%0a、%0b、%0c、%0d、%a0。或者使用括号()来构造无空格语句。也可以尝试将整个Payload放在一行完全不用空格某些情况下可行如unionselect1,2,3。Q4如何判断过滤规则是删除关键字还是拦截请求A提交一个包含疑似关键字的正常参数如?id1select观察返回。如果页面显示正常但关键字消失了变成?id1是删除型。如果页面返回错误、空白或跳转到拦截页是拦截型。对于删除型双写绕过是首选。对于拦截型需要尝试编码、分割、混淆。Q5在MyBatis中#{}真的绝对安全吗A在绝大多数正确使用的情况下#{}是安全的。但有一种边缘情况order by ${field}。如果field参数用户可控且没有严格白名单过滤攻击者可以传入field值为1 and (select 1 from (select sleep(5))a)虽然不能直接联合查询但可能造成时间盲注。因此即使使用MyBatis对于order by、group by等子句的动态字段也必须进行严格的白名单校验。Q6CTF中遇到过滤了information_schema怎么办AMySQL 5.7提供了sys库和innodb元数据表作为替代。可以尝试查询sys.schema_table_statistics、sys.x$schema_table_statistics或mysql.innodb_table_stats来获取表名信息。这需要你对MySQL的系统库有深入了解。绕过手法的学习是一个螺旋上升的过程。从最基础的大小写、双写到理解编码原理的宽字节再到组合利用对抗WAF每一步都需要扎实的SQL基础和对HTTP、数据库底层原理的理解。最好的学习方法就是自己搭建一个像DVWA、SQLi-Labs、Pikachu这样的靶场手动修改其过滤规则然后尝试去绕过它。在一次次“攻”与“防”的对抗中你的技术才会真正内化。记住我们的目的不是为了破坏而是为了在真正的威胁到来之前让自己和系统变得无懈可击。