开了CTF的坑之后,SQL注入永远是躲不开的一座山。尤其是在线上竞赛平台碰上加了过滤规则的关卡,SQLMap直接跑往往拿不到数据,不是没注入点,而是请求在第一步就被WAF或过滤逻辑拦下了。我见过挺多选手卡在这一步:SQLMap明明报了“parameter is vulnerable”,但--dbs跑不出来,--dump更是直接超时。这个时候,最值得投入时间研究的,就是Tamper脚本——它比手工猜更效率,也比改一句SQL碰运气要稳健得多。这篇内容主要围绕某CTF训练平台上的5道SQL注入关卡来展开,从SQLMap的基础使用讲到如何用自定义Tamper脚本绕过过滤、拿到数据,适合已经知道SQL注入基本原理、想进一步在真实判题环境下提高效率的选手。要是你只是想看一眼“SQLMap无脑跑”的教程,这篇大概率帮不到你;要是你想理解过滤机制背后的改写逻辑,这5道题够你玩一下午。
1. SQLMap在CTF注入场景下的配置与入门
1.1 从识别到利用的基本工作流
SQLMap报“注入点”只是开始,真正有价值的是后续的枚举、读取、写入这些动作。CTF平台上的题目一般都会给出一个带参数的URL,可能是?id=1,也可能是?name=admin,还有的是POST请求带JSON数据。SQLMap支持从-u指定URL开始,但遇到POST请求就得加上--data,比如:
sqlmap -u "http://challenge.example.com/api/user" --data "name=admin" --dbs这条命令干的事是:先对name参数做一轮探测,尝试单引号、真/假条件、时间盲注这些Payload,判断是否注入成功。判定成功之后再进入枚举阶段,用--dbs拉数据库列表。这个过程在你本地跑的时候看起来是自动化的,但实际它内部跑了几十个甚至上百个HTTP请求,网络质量差或者目标响应慢的时候,整个流程会很磨人。
在CTF关卡里,更常见的是你已经知道字段大概长什么样、库名可能是啥,没必要让SQLMap跑全量字典。这时候用--tables -D "库名"这种指定库名的方式省时得多。另一方面,--batch参数建议要么不开,要么开着但预备好“是/否”的默认答案。不开--batch的好处是,当SQLMap问你要不要测试其他参数时,你可以手动选n跳过,缩短测试时间;坏处是脚本潮水一样的提示会把你淹没。我的习惯是开局--batch开着跑一轮,确认注入类型后再精细化操作。
1.2 几个提高命中率的参数组合
CTF题目和真实渗透场景最大的差异是:题目有预设的过滤规则,但范围小、特征明显,不需要像真实测试那样小心翼翼地控制请求频率。有些参数值得常备:
--level 3 --risk 2:提高测试深度和Payload风险等级。注意--risk到2时会包含大量基于OR的Payload,某些LAMP环境会触发报错,但CTF平台本来就允许这种测试,所以问题不大。--random-agent:换一个随机User-Agent。有些关卡会对默认的SQLMap指纹做拦截,换掉有奇效。--time-sec 3:时间盲注的延迟时间,默认是5秒。如果目标响应本身有点慢,5秒会让整个测试慢到无法忍受,改成3秒能省不少时间。--technique:手动指定注入技术。CTF里空格过滤、等号过滤、逗号过滤很常见,默认的ABC(布尔、报错、时间)经常在某个Payload上卡住,指定技术可以少跑很多冤枉请求。
比如说某一道关卡过滤了空格,你又确定是布尔盲注场景,那直接在命令里写--technique=B,让SQLMap跳过报错注入和时间注入的Payload,速度能快一倍以上。
1.3 绕不过去时,Tamper才是真正的主角
SQLMap自带的Tamper脚本大概有上百个,它的作用是在Payload发出之前对原始SQL注入Payload做一次“文本改写”。这种改写不是随意的,而是针对常见过滤规则的等价替换。举例来说:如果目标过滤了空格,那space2comment.py这个Tamper会把你原本的AND 1=1里的空格改写成/**/,变成AND/**/1=1;如果过滤了逗号,那commalesslimit.py之类的脚本会把LIMIT 0,1改写成LIMIT 1 OFFSET 0这种不含逗号的形式。
在老版本SQLMap里,Tamper是一个个Python文件,需要手动放到tamper/目录下;新版本支持--tamper="A,B,C"这种逗号分隔的写法,也可以直接指定自定义脚本的绝对路径。你甚至可以同时启用多个Tamper,SQLMap会按顺序依次执行改写。本质上,这就是一个流水线式的Payload加工系统。
我在实际操作中最常用的组合是space2comment,equaltolike,一个处理空格过滤,一个处理等号过滤。但组合不是越多越好,因为每个Tamper都会增加Payload体积,可能把一个原本一次请求就能完成的测试拆成两次,速度反而下降。后面第5道关卡会讲一个“冲洗”类的典型案例,到时候你就发现,Tamper加错比不加更惨。
2. Tamper脚本拆解:5个脚本与它们的适用场景
2.1 Tamper的本质是规则引擎
理解Tamper,不单是背脚本名单,而是要理解它内部的数据结构。SQLMap的Tamper脚本遵循一个非常固定的接口:接收一个Payload字符串,返回改写后的字符串。几乎所有脚本都是在做“查找-替换”或“正则替换”的工作。你可以自己把Tamper想成一种“翻译器”,把正常SQL翻译成目标环境能够接受的方言。这种“方言”必须跟目标环境对过滤规则的解析方式匹配,所以不同过滤规则对应不同Tamper。
有一点要注意:Tamper不是万能的。如果目标环境把--注释符、#注释符、/斜杠都过滤了,那么很多Tamper输出的Payload会直接语法报错。所以现实中的工作流应该是:先手工测试目标过滤了哪些字符,再选择对应的Tamper,最后用SQLMap实测。
2.2 五个高频Tamper脚本的逐个拆解
下表是我在CTF和实战中用得比较多的5个Tamper脚本,按使用频率排列:
| Tamper脚本名 | 改写效果 | 典型场景 | 注意事项 |
|---|---|---|---|
| space2comment.py | 空格转为/**/ | 过滤空格的关卡 | 如果目标还过滤了/和*就失效 |
| space2plus.py | 空格转为+ | 以GET参数形式拼接SQL的场景 | POST请求里+容易变成普通加号 |
| equaltolike.py | =转为LIKE | 等号被过滤的布尔盲注 | 只替换等号,不替换<>,需要结合场景 |
| between.py | 把>转成NOT BETWEEN 0 AND | 数字型条件被过滤的关卡 | 改写后Payload体积变大,注意超时 |
| concat2concatws.py | 把CONCAT()转成CONCAT_WS() | 过滤逗号、需要字符串拼接的场景 | 只对MySQL生效 |
每个脚本看起来简单,但用错地方就会出反效果。比如space2plus.py,在GET参数里空格被替换成+后,urllib解码时会把+还原成空格,服务器端收到的SQL还是带空格的,等于白改。但如果目标是POST表单且后端直接按字符串拼接查询,+就只是一个普通字符,这时候就能绕过过滤。这就是为什么必须理解脚本原理才能用得准。
2.3 组合使用的顺序陷阱
多个Tamper一起用的时候,执行顺序是按照--tamper="A,B,C"里的顺序来的。顺序不同,结果完全不同。举例来说:
- 先执行
space2comment再执行equaltolike:Payload先变成/**/版本,等号被替换成LIKE。 - 先执行
equaltolike再执行space2comment:等号先变成LIKE,再进行注释替换。
两种顺序在大多数场景结果等价,但如果目标要求某SQL关键字后必须跟空格再跟参数,那顺序就有可能影响语法正确性。我遇到过一个场景,必须先做等号改写再做空格改写,因为LIKE后面的空格如果先被改成/**/,有些正则校验会漏掉,但SQL语法依然正确。这类细节只能靠实测总结,刷题多了自然有感觉。
3. 五道CTF关卡实战:从直线跑通到手工绕过
3.1 关卡一:无过滤的入门注入
第一道题通常很简单,就是一个字符型注入,参数是?name=admin,后端直接拼SQL。我测试的时候直接跑:
sqlmap -u "http://challenge.example.com/q1" --data "name=admin" --dbs这里要注意,POST请求如果不加--data,SQLMap会用GET方式请求,很可能直接405。这一步就能筛掉不少粗心的选手。
跑完之后拿到几个库,再指定题目库名拉表,最后用--dump -T "flag"把表内容倒出来。这一关的目的不是炫技,而是让你确认三件事:SQLMap在你这台机器上的环境是否正常、目标URL是否封装正确、数据库类型是否能被SQLMap正确识别。如果连这一关都跑不通,先别急着上Tamper,检查代理设置、Python版本和SQLMap是否最新版。
3.2 关卡二:把空格“藏”进注释里
第二道题引入了过滤:空格直接没了。我手工发一个AND 1=1,响应是404,AND/**/1=1之后响应正常,基本确认“只过滤空格”。这时候直接用:
sqlmap -u "http://challenge.example.com/q2?id=1" --tamper="space2comment" --dbs如果第一遍跑不出来,多半是SQLMap的默认Payload里还有其他被过滤的字符,比如--注释符。这时候把--tamper="space2comment,versionedmorecomments"组合起来再试。versionedmorecomments.py会把MySQL版本的/*!注释风格混进去,很多简单过滤规则会忽略这种注释内容,等于直接绕过。
这一关的核心收获是:SQLMap的默认Payload未必适配所有过滤规则,命中不了就换个角度,用Tamper“组合拳”来贴合目标的过滤逻辑。
3.3 关卡三:报错注入里的大坑
第三道题是一个纯粹的报错注入场景,目标把union select过滤了,但报错Payload还能用。SQLMap理论上会自动识别报错注入,可我在实际测试中发现,它默认走的是基于UPDATEXML的报错Payload,有些平台会额外过滤掉UPDATEXML关键字,所以直接跑会一无所获。
这时候可以先手工触发一个报错,确认目标报错类型。然后指定SQLMap只走报错注入:
sqlmap -u "http://challenge.example.com/q3?id=1" --technique=E --tamper="space2comment,between"加了between.py是处理数字比较符号的,报错注入Payload里经常出现>这类符号,被过滤就会中断。这里还要留意一个细节:报错注入的输出长度通常很短,SQLMap要反复构造请求来逐字符“猜”数据,所以--dump阶段特别慢。建议先--dbs拿库名,再手工确认关键表,不要一上来就全量dump。
3.4 关卡四:布尔盲注配合LIKE改写
第四道题比较阴险:既过滤空格,又把等号给过滤了。如果你用space2comment硬跑,会发现SQLMap识别出注入点,但无法枚举表名,原因是table_name='users'这个条件里的等号直接让查询失效。正确的Tamper组合是:
sqlmap -u "http://challenge.example.com/q4?id=1" --tamper="space2comment,equaltolike" --technique=B --dbsequaltolike.py会把所有等号替换成LIKE,比如name='admin'变成name LIKE 'admin',语义等价但绕过了过滤。这样SQLMap的布尔盲注就能继续跑下去。
这个关卡值得多花点时间思考:为什么空格过滤了但注释符没过?为什么等号过滤了但LIKE没被过滤?因为过滤规则写得太宽松,只想着挡掉最常见符号,没考虑SQL语法的“等价改写”空间。这种贪方便留下的口子,正好是Tamper的发力点。
3.5 关卡五:堆叠注入与二次注入思路
第五道题开始有真正的“逃逸”味道了。目标不仅过滤了空格、等号,还过滤了分号,这意味着SQLMap的堆叠注入Payload会直接失效。但仔细观察后端逻辑会发现,它支持一次请求带多个SQL语句,只是分号被正则替换成了空字符串。
手工测试时,先用;和%3B分别测试。很多平台的过滤只检查原始字符,不检查URL编码后的字符,所以%3B有可能绕过。SQLMap这边,需要单独指定堆叠注入技术:
sqlmap -u "http://challenge.example.com/q5?id=1" --technique=S --tamper="space2comment,percentage.py"percentage.py这个Tamper会把所有字符做一次URL编码,让分号变成%3B,空格变成%20。如果目标在解码后再过滤,这个方法会失败;但很多题目恰好是在解码前用正则检查,从而被绕过去。
其实到这一步,Tamper的作用已经开始变得次要,更重要的是你能不能想到“分号可以编码”这个思路。SQLMap只是一个执行者,真正的决策者是你自己。每道关卡都可能同时存在多个过滤层,调试的关键是看目标到底是“匹配后拦截”还是“解码后拦截”,这决定了Payload改写策略的大方向。
4. 手写一个自定义Tamper脚本的完整过程
4.1 理解Tamper脚本的接口与结构
SQLMap的Tamper脚本接口极其轻量:一个Python文件里定义一个tamper(payload, **kwargs)函数,接收字符串,返回字符串。最简单的脚本长这样:
#!/usr/bin/env python from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL def dependencies(): pass def tamper(payload, **kwargs): return payload.replace(" ", "/**/")把这个文件保存为myspace.py,放到任意目录,然后用--tamper="/path/to/myspace.py"指定路径就行。__priority__影响多个Tamper同时使用时的默认排序,新手期不用太纠结,写PRIORITY.NORMAL就够用。
SQLMap官方Tamper里大量使用lib.core.common和lib.request等内部模块,但自定义脚本没必要依赖它们。保持纯字符串处理,脚本兼容性最好,也不会因为SQLMap版本更新而失效。
4.2 用自定义脚本改写逗号绕过LIMIT限制
我现在要分享的是我针对“逗号被过滤”场景写的一个脚本,它比SQLMap自带的commalesslimit.py更直接。思路是:把LIMIT 0,1改写为LIMIT 1 OFFSET 0,再把所有函数内的逗号改成JOIN语法。示例代码如下:
#!/usr/bin/env python from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL def tamper(payload, **kwargs): # 处理 LIKE 类函数中逗号,例如 SUBSTRING(x,1,2) -> SUBSTRING(x FROM 1 FOR 2) payload = payload.replace("SUBSTRING(", "SUBSTRING(") # 简单而有效:把 LIMIT a,b 改写成 LIMIT b OFFSET a import re payload = re.sub(r"LIMIT (\d+),(\d+)", r"LIMIT \2 OFFSET \1", payload) # 把函数逗号改写成 UNION 或 JOIN 的等价形式需要根据具体函数处理, # 这里以 SUBSTR 为例: payload = re.sub(r"SUBSTR\(([^,]+),([^,]+),([^)]+)\)", r"SUBSTR(\1 FROM \2 FOR \3)", payload) return payload用的时候要留意:LIMIT 0,1这种改写对MySQL、PostgreSQL都有效,但对Oracle、SQL Server不生效,跑之前要先确认目标库类型。更稳妥的做法是先用--fingerprint让SQLMap探测一下数据库版本,再决定要不要用这个脚本。
4.3 调试自定义脚本的实用技巧
调试Tamper脚本,不要直接拿SQLMap跑全流程,那样既慢又难定位问题。我在本地写脚本的时候,习惯先写一个微型测试函数:
if __name__ == "__main__": payload = "AND (SELECT 1 FROM users WHERE name='admin' LIMIT 0,1)" print(tamper(payload))在命令行跑一遍,输出确认无误后再挂到SQLMap上实测。这样能把“Python语法问题”和“SQL语义问题”分开排查。
另一点很关键:多留意目标响应的长度变化。SQLMap在布尔盲注时会根据页面长度判断真假,如果Tamper改写出错,所有请求的响应长度都会变成同一个值,表现为“注入点一直存在但什么都跑不出”。这时候八成是Payload在语法层就挂了,回到本地测试脚本里检查一下SQL拼接后的最终语句,往往一眼就能看到少了个引号或多个逗号。
5. 问题速查:SQLMap跑不出数据时的排查思路
5.1 识别出注入点但拿不到库名
此类问题最常见的原因有两个:第一个是Tamper改写不彻底,比如目标过滤了空格和注释符,你只用了space2comment,那Payload里残留的空格依然会导致失败;第二个是注入点的参数位置特殊,比如参数在JSON字段内,SQLMap的默认Payload会搞错拼接位置。前者通过加Tamper解决,后者需要配合--prefix和--suffix参数手动指定Payload前后缀,例如:
sqlmap -u "http://challenge.example.com/api" --data '{"name":"admin"}' --prefix="' OR " --suffix="-- -" --dbs5.2 加Tamper后速度反而变慢
Tamper每改写一次,都会让Payload的长度变大。space2comment还算克制,但between.py和percentage.py这类脚本会让Payload膨胀好几倍。网速差的时候,一个布尔盲注流程跑下来可能需要几十分钟。解决办法是缩小目标范围:先手动确认库名或表名的一部分,再用--where="name LIKE 'fl%'"之类的条件去限制SQLMap的枚举范围,不要让它从头猜到尾。
| 问题现象 | 可能原因 | 典型解法 |
|---|---|---|
报注入点但--dbs无结果 | Tamper与过滤规则不匹配 | 手工发一个测试请求,确认哪些字符被过滤,再选择Tamper |
| 响应全部等长 | Payload语法错误,布尔盲注失效 | 本地测试Tamper输出;检查引号闭合 |
| 报错注入能触发但枚举很慢 | 报错信息长度受限 | 指定--technique=E并缩小枚举范围 |
| HTTP 403/429频繁 | 请求频率过高被限流 | 加--delay 1、--random-agent降低特征 |
5.3 判断当前使用的Tamper是否生效
这个方法很多人不知道:用-v 3参数打开详细日志,SQLMap会打印出每个实际发出的HTTP请求Payload。看一眼最终发出去的Payload长什么样,就能立刻判断Tamper有没有真的改写成功。这个方法是我最常用的排查手段,几乎所有诡异的Tamper问题都能通过看日志定位,比瞎猜组合要高效得多。
写在题后
刷完这5道关卡,最大的感触是:Tamper不是魔法,它只是帮你把“手工改写SQL”这件事自动化了。真正决定你能不能逃逸过过滤的,是你对SQL语法等价改写规则的理解程度。空格没了可以用注释,等号没了可以用LIKE,逗号没了可以用FROM FOR或者JOIN,这些都是SQL本身允许的语义等价写法,过滤规则只是拦住了最常见的写法,没有拦住语法层面的表达自由。
我自己现在写SQLMap命令,已经把--tamper="space2comment,equaltolike"当成默认配置了,打CTF时能省掉大量探测时间。但遇到硬过滤的题目,还是会把-v 3日志开着,亲手看一眼Payload被改写成什么样,再决定下一步怎么走。SQLMap是很好用的工具,但它只会忠实执行你给它的规则,把规则设计好,才是拉开差距的地方。