SQL注入绕过实战:利用MySQL冷门函数GTID_SUBSET突破WAF防护

SQL注入绕过实战:利用MySQL冷门函数GTID_SUBSET突破WAF防护
1. 项目概述一次针对证书查询站点的SQL注入绕过实战最近在参与一个SRC安全应急响应中心的漏洞挖掘项目目标是一个提供证书查询服务的网站。这类站点通常存储着大量用户认证信息一旦存在SQL注入漏洞后果不堪设想。在初步的探测中我发现了一个关键的查询接口存在注入迹象但直接使用常规的或and 11等Payload时请求被拦截了——目标站点部署了WAFWeb应用防火墙。这反而激起了我的兴趣一次经典的“Bypass WAF”挑战开始了。最终我成功绕过了防护完成了注入验证和数据提取。这篇文章我就来详细拆解这次“某证书站Bypass sql”的完整过程从思路到技巧希望能给同样在安全研究路上的朋友一些启发。2. 核心思路与WAF行为分析面对一个疑似存在注入但被WAF拦截的站点盲目尝试各种Payload是低效的。我的第一步永远是分析WAF的拦截逻辑。这就像解一道谜题你需要先理解规则才能找到规则的缝隙。2.1 初步探测与WAF指纹识别我首先对目标接口cert_query.php?id123进行了基础测试。单引号测试访问id123。页面返回了一个通用的“参数错误”提示而非详细的数据库报错。同时在Burp Suite的Logger中观察到了来自WAF的拦截日志状态码通常是403或一个自定义的拦截页面。这初步证实了WAF的存在。逻辑测试尝试id123 and 11和id123 and 12。两者均被拦截且返回内容无差异。这说明WAF的规则库很可能包含了and、or、等常见SQL关键词的检测。注释符测试尝试id123--和id123#。同样被拦截。WAF对注释符的检测也很常见。通过拦截返回的页面头信息或特征我初步判断这可能是一款国内常见的云WAF或硬件WAF产品。这类WAF通常基于正则表达式规则库对请求参数进行字符串匹配。它的弱点在于规则是静态的而构造Payload的方式是动态且无限的。2.2 绕过策略的选定从“混淆”到“利用特性”基于以上分析我制定了几个并行的绕过策略特殊字符与编码混淆利用URL编码、双重编码、Unicode编码等方式打乱关键词的呈现形式。例如将and写成%61%6e%64URL编码或%2561%256e%2564双重URL编码看WAF是否解码层数不够。空白符变异SQL语句中的空格是WAF重点检测对象。可以用/**/MySQL注释、%09Tab、%0a换行、%0c换页甚至号在某些场景下被解析为空格来替代。函数与语法特性利用这是更高级的技巧。利用数据库特有的、不常见的函数或语法结构来构造Payload因为这些冷门函数可能不在WAF的默认规则库里。这也是本次实战最终成功的关键。参数污染HPP提交多个同名的参数如id123id456。不同服务器端语言对同名参数的处理方式不同取第一个、取最后一个、拼接成数组等可能造成WAF检测的参数与实际被数据库执行的参数不一致。实操心得在测试初期不要只用一个Payload死磕。应同时开启多个测试线程分别尝试不同策略的基础Payload快速摸清WAF的“脾气”。例如用Burp Intruder的“Pitchfork”模式同时对空格替换、关键词编码等进行Fuzz。3. 关键绕过技术细节拆解在尝试了多种编码和空白符替换后我发现简单的混淆仍然会被拦截。WAF的规则比想象中严密。于是我将重点转向了利用数据库特性构造非常规Payload。3.1 利用MySQL的GTID_SUBSET函数进行布尔盲注这是本次绕过中最核心的技术点。我通过之前的错误信息片段提示MySQL版本号确定了后端数据库是MySQL 5.6。这让我想起了GTID_SUBSET这个函数。函数原理GTID_SUBSET(subset, set)用于判断一个GTID集合是否是另一个的子集返回1真或0假。在SQL注入中我们可以构造一个表达式让其返回值动态变化从而控制整个查询条件的结果。构造过程原始查询猜测根据上下文我猜测后台SQL类似SELECT * FROM certificates WHERE id $_GET[‘id’]。构造数字型注入Payload我不再使用and连接而是直接让id参数等于一个能返回1或0的表达式。如果查询id1有数据id0无数据那么我就能通过控制表达式返回值来实施布尔盲注。最终PayloadidGTID_SUBSET(concat(user(),‘:1’),‘00000000-0000-0000-0000-000000000000:1’)user()函数返回当前数据库用户。concat(user(), ‘:1’)将其构造成一个GTID格式的字符串。与一个不可能的GTID全零比较GTID_SUBSET必然返回0。因此整个表达式等价于id0页面应返回无数据假状态。验证注入我将user()替换为database()。访问idGTID_SUBSET(concat(database(),‘:1’),‘00000000-0000-0000-0000-000000000000:1’)。如果页面返回了数据即id1的效果说明database()的返回值使得GTID_SUBSET返回了1这证明我们的表达式被执行了注入存在为什么能绕过WAF冷门函数GTID_SUBSET是用于数据库复制的管理函数在常见的Web注入攻击中极少被使用因此绝大多数WAF的默认规则库不会包含它。无敏感关键词整个Payload中没有出现and,or,select,union,sleep,substr等任何高危关键词完美避开了基于关键词库的检测。语法合法它看起来就像一个普通的函数计算符合id函数()这种数字型参数处理的常见模式对基于语法分析的WAF也有一定迷惑性。3.2 反引号与注释符的妙用即使使用了GTID_SUBSET在后续构造更复杂的盲注Payload如逐字符猜解数据时仍然需要用到substr、ascii等函数这些函数可能被标记。技巧反引号包裹函数名在MySQL中反引号用于引用标识符如包含特殊字符的表名。我们可以利用这个特性来拆分函数名。原始Payloadsubstr(database(),1,1)绕过变形substr(database(),1,1)或substr(database(),1,1)原理许多WAF使用简单的字符串匹配。substr是一个触发词但substr带反引号可能就不是了。MySQL在执行时会正确识别被反引号包裹的substr。技巧内联注释分割MySQL支持/*!*/这种内联注释其中的代码在MySQL中会被执行。原始Payloadif(11,1,0)绕过变形/*!if*/(11,1,0)或if/*!*/(11,1,0)原理将函数名与括号用注释隔开破坏“函数名(”这个常见的匹配模式。3.3 布尔盲注的自动化实现思路确认GTID_SUBSET可用后就需要实现自动化的数据提取。布尔盲注的核心是“猜”我们需要一个字符一个字符地猜出数据库名、表名、字段内容。手工猜解示例以获取数据库名第一个字符为例 假设我们构造的Payload模板是idGTID_SUBSET(concat(if(ascii(substr(database(),1,1))[ASCII值],1,0),‘:1’),‘0:1’)判断长度idGTID_SUBSET(concat(if(length(database())[长度],1,0),‘:1’),‘0:1’)通过二分法改变[长度]看页面是否有数据来确定数据库名长度。猜解字符idGTID_SUBSET(concat(if(ascii(substr(database(),1,1))100,1,0),‘:1’),‘0:1’)。如果页面有数据说明第一个字符的ASCII码大于100如果无数据则小于等于100。通过二分法如与50、75、88比较最终确定准确的ASCII码再转换为字符。自动化工具辅助 手工操作极其繁琐。我通常会编写一个简单的Python脚本或者利用Burp Suite的Intruder工具的“Cluster bomb”攻击模式结合“Grep Match”功能来区分真/假页面状态实现半自动化猜解。更高效的方式是将我们精心构造的、能绕过WAF的Payload逻辑集成到sqlmap的tamper脚本中让sqlmap来执行后续的自动化注入。注意事项在实战中务必控制请求频率避免对目标服务器造成拒绝服务DoS影响。在脚本中加入time.sleep()是基本操作。同时布尔盲注会产生大量请求容易被运维监控发现异常流量需要根据情况调整节奏。4. 完整漏洞验证与利用流程有了绕过方法和思路接下来就是系统性地验证和利用漏洞。这个过程需要耐心和细致。4.1 信息收集与数据库指纹确认在成功执行GTID_SUBSETPayload后我首先需要确认更多数据库信息为后续查询做准备。数据库版本Payload:idGTID_SUBSET(concat(version(),‘:1’),‘0:1’)。通过布尔盲注猜解出版本号确认是MySQL 5.7.32。当前用户Payload:idGTID_SUBSET(concat(user(),‘:1’),‘0:1’)。猜解出用户为rootlocalhost这是一个高危信号意味着数据库权限很高。当前数据库Payload:idGTID_SUBSET(concat(database(),‘:1’),‘0:1’)。猜解出数据库名为cert_system。4.2 利用information_schema获取表结构MySQL的information_schema数据库存储了所有元数据是我们的“藏宝图”。猜解表名Payload核心部分(select group_concat(table_name) from information_schema.tables where table_schemadatabase())完整注入点构造idGTID_SUBSET(concat((select mid(group_concat(table_name),1,30) from information_schema.tables where table_schemadatabase()),‘:1’),‘0:1’)这里用了mid替代substrgroup_concat将多行结果合并成一行方便猜解。我依次猜解出了users,certificates,admin_log等表名。其中users表立刻引起了我的注意。猜解users表的字段名Payload核心部分(select group_concat(column_name) from information_schema.columns where table_schemadatabase() and table_name‘users’)这里需要注意‘users’这个字符串常量需要处理。我将其转换为十六进制0x7573657273来避免引号table_name0x7573657273最终猜解出字段id,username,password,email,phone,create_time。4.3 数据提取与漏洞证明目标是获取管理员或用户的敏感信息作为漏洞存在的铁证。提取数据Payload:idGTID_SUBSET(concat((select concat(username,0x7e,password) from users limit 0,1),‘:1’),‘0:1’)0x7e是~符号的十六进制用作分隔符。limit 0,1取第一条数据。通过布尔盲注我最终提取出了第一条用户记录的用户名和密码哈希值例如MD5。密码破解可选如果密码是弱哈希如MD5、SHA1可以尝试在彩虹表网站或本地进行碰撞破解。如果破解成功获得明文密码漏洞的危害性将得到极大凸显。整理报告将漏洞发现过程、涉及的URL、使用的Payload、获取的敏感数据需脱敏如展示前几位和后几位哈希值、漏洞可能造成的危害数据泄露、权限提升、服务器沦陷等清晰地整理成文档。特别注意在报告中所有提取的真实数据必须进行严格的脱敏处理只用于证明漏洞存在绝不能泄露。5. 深度防御思考与进阶绕过场景成功绕过并验证漏洞后我的工作并未结束。从防御者和更深入攻击者的双重视角进行复盘能获得更多价值。5.1 为什么预编译Prepared Statement没能防住这是一个常见的误区。开发者和防御者常常认为只要使用了参数化查询预编译SQL注入就高枕无忧了。但在这个案例中漏洞依然存在原因在于漏洞点不在“值”而在“标识符”或“结构”仔细回想我们的注入点是id参数。一个理想的预编译语句应该是SELECT * FROM cert WHERE id ?然后传入id的值。这确实能防止id值本身的注入。但是如果SQL语句的其他部分也是动态拼接的呢考虑一种情况String sql “SELECT * FROM ” tableName “ WHERE id ?”;这里的tableName如果来自用户输入虽然不常见但在一些动态查询中可能存在预编译对它是无效的。更常见的场景是排序ORDER BY和分页LIMIT字段。例如ORDER BYsortField这里的sortField无法使用预编译的占位符如果过滤不严就会产生注入。在本案例中需要进一步审计代码才能确定根本原因但作为攻击者我们无需关心底层实现只需找到可利用的输入点。给开发者的防御建议坚持使用参数化查询处理所有用户输入的值。对于表名、列名、排序字段等无法参数化的部分必须进行严格的白名单校验。例如只允许sortField是[‘id’, ‘name’, ‘time’]中的值。最小权限原则数据库连接账户不应使用root应遵循最小权限原则只授予应用必要的权限如SELECT,UPDATE而非FILE,EXECUTE。5.2 其他可能的高级绕过场景本次利用的是MySQL的冷门函数。面对其他数据库或更复杂的WAF思路需要拓宽JSON函数注入MySQL 5.7现代WAF可能开始关注extractvalue和updatexml但对json_extract,json_search等JSON函数检测较弱。可以利用JSON路径查询语法进行报错注入。利用数据库特性函数SQL Server可尝试WAITFOR DELAY ‘0:0:5’进行时间盲注或者利用OPENROWSET进行带外数据OOB查询。Oracle利用UTL_INADDR.GET_HOST_ADDRESS或HTTPURITYPE发起OOB请求或者使用CASE WHEN语句进行复杂的布尔盲注。PostgreSQL利用pg_sleep()进行时间盲注或利用COPY TO进行文件写入需高权限。二阶注入Second-Order Injection攻击Payload并非立即生效而是先被存储到数据库如注册用户名、留言内容当后端应用再次从数据库取出该数据并拼接到SQL语句中执行时触发注入。这种注入方式完全绕过了对输入参数的即时过滤。非常规注入点不要只盯着id、name这些参数。CookieCookie: user_id1; sessionabc’HTTP HeadersX-Forwarded-For: 1.1.1.1’文件上传文件名test’.jpg可能被记录到数据库。搜索功能搜索词的处理逻辑复杂容易出问题。5.3 自动化工具的谨慎使用在SRC漏洞挖掘中直接使用sqlmap等自动化工具进行大规模扫描是非常不推荐的原因如下高流量与高攻击性sqlmap的探测会产生大量请求极易触发WAF的CC防护或直接被封IP也会对服务器造成不必要的压力。特征明显sqlmap的Payload有固定模式容易被现代WAF识别和拦截。破坏性风险使用--dump等参数可能拖取大量数据违反安全测试原则甚至可能违法。正确的姿势手工验证像本文描述的那样先手工找到确切的注入点和可用的绕过方法。定制tamper脚本将手工验证成功的绕过技巧如GTID_SUBSET配合反引号编写成sqlmap的tamper脚本。限制性使用使用sqlmap时务必加上--threads1单线程、--delay1请求延迟、--risk1 --level1最低风险等级等参数控制攻击强度。并且绝对不要使用--dump-all或--os-shell等高风险功能除非在完全可控的测试环境。漏洞挖掘是一场与开发者、防御系统之间的智力博弈。每一次成功的绕过不仅是对自身技术的证明更是对系统防御体系的一次有效检验。作为安全研究者我们的目标不是破坏而是通过发现隐患推动系统变得更加坚固。保持好奇心深入理解原理谨慎行事这份“黑客”精神才能走在正确的道路上。