Burp Suite验证码自动识别实战:captcha-killer集成与调优指南
1. 这不是“自动打码”,而是把验证码识别真正嵌入渗透流程的实操闭环
你有没有遇到过这样的情况:在用Burp Suite做登录爆破时,刚跑完字典,页面弹出一个扭曲的验证码图片,整个自动化流程戛然而止?你点开图片放大、手动输入、再点提交——三秒后又来一张。反复十几次,手酸眼花,效率归零。更糟的是,有些验证码根本不是纯文字,而是带干扰线+旋转字符+背景噪点的组合体,人工识别都费劲,更别说写脚本绕过了。这时候,很多人第一反应是去搜“Burp 验证码插件”,结果下载一堆名字带“bypass”“crack”“auto”的jar包,双击加载,发现要么报错“NoClassDefFoundError”,要么识别率低得离谱,输十个错八个,最后干脆放弃,退回手工操作。
这恰恰暴露了一个被长期忽视的事实:验证码识别在渗透测试中从来不是“有没有工具”的问题,而是“能不能稳、准、快地融入现有工作流”的问题。“captcha-killer”这个插件之所以在实战圈里被反复提及,并非因为它能识别所有验证码,而在于它把OCR识别、模型调用、HTTP请求重放、响应判断这几个关键环节,用Burp原生扩展机制串成了一条可调试、可观察、可中断的流水线。它不承诺100%识别率,但保证每一次识别失败,你都能立刻看到是图片预处理出了问题,还是模型输出格式没对齐,抑或是服务端返回了新的校验逻辑。我去年在给一家金融客户做红队评估时,就靠它把原本需要3人天的手动登录遍历,压缩到4小时完成——不是靠“黑科技”,而是靠把识别过程变成可复现、可审计、可回溯的操作单元。
这篇指南不讲理论,不堆参数,只聚焦一件事:如何让captcha-killer从一个“能加载的插件”,变成你Burp工作台里像Intruder一样顺手的常规武器。它适合两类人:一是已经会用Burp基础功能(Proxy、Repeater、Intruder),想把验证码环节自动化掉的渗透测试人员;二是正在学Web安全的新人,需要理解“识别验证码”这件事在真实攻击链路中到底卡在哪、怎么解。下面所有步骤,我都按自己实际搭环境、调参数、踩坑、修复的顺序来写,连报错截图里的堆栈信息都还原成了文字描述——因为真正的实战,从来不是照着文档点几下就能成功的。
2. 插件本质拆解:为什么它不叫“captcha-bypass”,而叫“captcha-killer”
2.1 它不是OCR引擎,而是OCR的“调度员”和“翻译官”
很多人第一次加载captcha-killer时,会下意识认为:“哦,它内置了Tesseract或者EasyOCR”。这是最大的误解。打开它的源码(GitHub上开源)你会发现,核心逻辑只有三段:
- 第一段是“截”:监听Burp的HTTP响应,用正则匹配
<img src=".*?captcha.*?">或data:image/png;base64,这类标签,把图片二进制数据从响应体里抠出来; - 第二段是“送”:把抠出来的图片base64编码,通过HTTP POST发给一个外部服务(默认是
http://localhost:5000/captcha),这个服务才是真正的OCR识别器; - 第三段是“填”:拿到服务返回的识别结果(比如
{"text":"aB3x"}),再用Burp的IHttpRequestResponse接口,把captcha=xxx这个参数动态注入到原始请求的POST body或URL query里,重新发送。
提示:它本身不包含任何机器学习模型。所谓“识别能力”,完全取决于你后面配的那个外部服务。你可以用Python写的Flask服务调Tesseract,也可以用Docker跑一个PaddleOCR容器,甚至可以对接商业API——只要它能接收base64图片、返回JSON格式的text字段,captcha-killer就能用。
这就解释了为什么安装时总卡在“无法连接识别服务”。不是插件坏了,是你没启动那个“背后干活的人”。我见过太多人反复重装插件、换JDK版本、查Burp日志,最后发现只是忘了在终端里敲python app.py启动本地服务。
2.2 架构设计的精妙之处:解耦带来的调试自由度
传统思路是把OCR逻辑全塞进Java插件里,好处是“开箱即用”,坏处是:
- 一旦识别不准,你得改Java代码、重新编译、再加载,循环耗时;
- Tesseract在Java里调用不稳定,尤其Windows下常因DLL路径报错;
- 模型升级(比如从Tesseract 4换成PaddleOCR)意味着整个插件重写。
captcha-killer反其道而行之,用HTTP协议做“胶水”,把Burp(Java)、识别服务(Python/Go/Node)、模型(C++/CUDA)彻底隔开。这意味着:
- 你想换模型?只改一行Python代码,重启服务即可,Burp插件完全不用碰;
- 识别结果错了?直接在Burp的Logger里看它发了什么base64、收到了什么JSON,比翻Java堆栈快十倍;
- 服务挂了?Burp里会明确提示“Connection refused”,而不是静默失败。
我去年帮一个团队做内部培训时,让学员现场改识别服务:把Tesseract换成一个自己训练的CNN小模型(PyTorch),只用了20分钟——改3行代码(加载模型、预处理、预测)、启服务、在Burp里点“Send to Intruder”验证。如果逻辑全在Java里,光环境配置就得半天。
2.3 它解决的不是“识别”,而是“上下文同步”这个隐形瓶颈
最常被忽略的一点是:验证码不是孤立存在的。它和Session ID、CSRF Token、时间戳强绑定。你手动识别一次,可能要刷新三次页面才能拿到新图;自动化时,如果插件只管“填验证码”,不管“同步Session”,那填进去的永远是上一轮的无效token。
captcha-killer的处理逻辑是:
- 先抓取当前请求的完整Cookie头;
- 把图片请求(通常是GET /captcha.jpg)单独发一次,确保拿到最新图片的同时,也更新了Burp的Session上下文;
- 识别完成后,把结果塞回原始请求,并复用同一个Cookie头发送。
这个细节决定了它能否在真实业务系统里跑通。我测试某政务系统时,发现验证码接口必须携带X-Requested-With: XMLHttpRequest头,否则返回空图。我在插件配置里加了这一行自定义Header,问题当场解决——这种微调,在紧耦合的插件里几乎不可能实现。
3. 从零部署:避开90%新手会踩的环境陷阱
3.1 Burp端:别急着双击jar,先确认三个隐藏条件
很多教程一上来就说“下载captcha-killer.jar → Extender → Add → 选中加载”,然后就结束了。但实际中,至少70%的加载失败源于这三个被忽略的前提:
第一,Burp版本必须≥2021.7。
原因很实在:captcha-killer用到了IBurpExtenderCallbacks.getHelpers().stringToBytes()这个方法,它在2021.7之前叫getHelpers().bytesToString(),签名不同。如果你用的是2020.12版Burp,加载时控制台会报NoSuchMethodError,但错误日志藏得很深,只在“Output”标签页底部闪一下。解决方案只有两个:升级Burp,或找老版本插件(GitHub上有v1.2分支,但已停止维护)。
第二,Java运行时必须是JDK 11或JDK 17(推荐17)。
Burp官方支持JDK 11+,但captcha-killer的构建脚本(pom.xml)指定了maven-compiler-plugin版本为3.8.1,它在JDK 8下会编译失败。更隐蔽的问题是:如果你系统里同时装了JDK 8和JDK 17,而Burp启动脚本(burpsuite_pro.vmoptions)里没指定-vm路径,它可能默认用JDK 8加载插件,导致java.lang.UnsupportedClassVersionError。我的做法是:在Burp安装目录下建个jre文件夹,把JDK 17的jre复制进去,然后在burpsuite_pro.vmoptions第一行加-vm ./jre/bin/server/jvm.dll(Windows)或-vm ./jre/lib/server/libjvm.dylib(macOS)。
第三,必须关闭Burp的“Project options → Connections → SSL Pass Through”里的通配符规则。
这是最反直觉的坑。当你的识别服务跑在http://localhost:5000时,Burp默认会把所有localhost流量走代理,导致插件发出去的HTTP请求被自己拦下来,形成死循环。解决方案:进Project options → Connections → SSL Pass Through,删掉*这一行,或者加一条127.0.0.1:5000的白名单。
注意:以上三步做完,再加载jar包。加载成功后,Burp的Extender → Extensions列表里会出现“Captcha Killer”,右下角状态栏显示“Ready”,这才是真正就绪。
3.2 识别服务端:用Python Flask搭一个“最小可用”服务
插件默认指向http://localhost:5000/captcha,我们就按这个路径搭。不用Docker,不用复杂框架,就一个app.py文件,20行代码搞定:
# app.py from flask import Flask, request, jsonify import base64 from io import BytesIO from PIL import Image import pytesseract app = Flask(__name__) @app.route('/captcha', methods=['POST']) def solve_captcha(): try: data = request.get_json() img_b64 = data.get('image') if not img_b64: return jsonify({'error': 'No image provided'}), 400 # 解码base64为PIL Image img_bytes = base64.b64decode(img_b64) img = Image.open(BytesIO(img_bytes)) # 简单预处理:转灰度、二值化 img = img.convert('L') threshold = 128 img = img.point(lambda p: p > threshold and 255) # OCR识别 text = pytesseract.image_to_string(img, config='--psm 8 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') text = text.strip().replace(' ', '').replace('\n', '') return jsonify({'text': text}) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': app.run(host='127.0.0.1', port=5000, debug=False)安装依赖只需三行:
pip install flask pillow pytesseract # Windows用户额外执行: # tesseract.exe需提前安装(官网下载),并把安装路径加到系统PATH # macOS用户:brew install tesseract # Linux用户:apt-get install tesseract-ocr启动服务:python app.py。终端出现* Running on http://127.0.0.1:5000即成功。
实测心得:Tesseract的
--psm 8(单行文本模式)比默认psm 3对验证码识别率高20%以上,因为验证码基本就是一行字符。tessedit_char_whitelist参数强制限定字符集,能大幅降低误识率——比如去掉l和1、O和0的混淆,这对数字字母混合验证码至关重要。
3.3 插件配置:三个必调参数决定90%的成功率
加载插件后,右键任意含验证码的HTTP请求 → “Extensions” → “Captcha Killer” → “Configure”,你会看到配置界面。这里只有三个参数真正影响实战效果:
1. Recognition URL(识别服务地址)
默认是http://localhost:5000/captcha,如果你的服务换了端口(比如5001),必须手动改。注意:不能加斜杠结尾,写成http://localhost:5000/captcha/会导致404。
2. Parameter Name(验证码参数名)
这是最关键的字段。它告诉插件:“在原始请求里,把识别结果填到哪个参数名下?”
- 登录表单常见名:
captcha、verify_code、code、authcode - JSON接口常见名:
captchaCode、verificationCode - 动态生成名:有些系统用
captcha_123456(后缀是时间戳),这时你得先抓包看规律,再在Burp里用Match and Replace规则把参数名固定住。
3. Image Regex(图片提取正则)
默认是<img[^>]+src=["']([^"']+captcha[^"']+)["'],它匹配HTML里的<img src="/api/captcha?r=123">。但如果验证码是base64内联图(<img src="data:image/png;base64,iVBORw...">),这个正则就失效了。此时要改成:<img[^>]+src=["']data:image/[^"']+base64,([^"']+)["']
然后在插件设置里勾选“Decode Base64”。
踩坑记录:某电商后台的验证码接口返回的是
Content-Type: image/jpeg,但响应体是二进制流,没有HTML标签。这时正则完全无用。我的解法是:在Burp Proxy的Options → Match and Replace里,添加一条规则,把该URL的响应重写成<img src="data:image/jpeg;base64,XXX">格式,再让插件去匹配——用Burp自身的规则引擎补足插件能力边界。
4. 渗透测试实战:从单次识别到Intruder爆破的完整链路
4.1 单次识别验证:用Repeater确认端到端流程是否通畅
别急着上Intruder。先用最简单的Repeater验证整个链路:
- 在Proxy历史里找到一个含验证码的登录请求(比如POST
/login); - 右键 → “Send to Repeater”;
- 在Repeater的Request标签页,确认Body里有
captcha=参数(值先随便填,如aaa); - 切到Response标签页,确认返回的是“验证码错误”页面(通常含
<img>标签); - 右键Response里的
<img>标签 → “Extensions” → “Captcha Killer” → “Solve in Repeater”;
这时会发生三件事:
- Burp自动在后台调用识别服务,拿到结果(比如
K7m9X); - Request Body里的
captcha=aaa被替换成captcha=K7m9X; - 新请求自动发送,Response里应该出现登录成功跳转(如
302 Found+Location: /dashboard)。
如果失败,按顺序检查:
- Repeater的Response里是否真有
<img>?没有说明正则没匹配; - Logger里是否有
[Captcha Killer] Sending image to http://...?没有说明插件没触发; - Logger里是否有
[Captcha Killer] Received response: {"text":"..."}?没有说明服务没通; - 替换后的Request里
captcha=值是否正确?错误说明Parameter Name填错了。
实操技巧:在Repeater里按
Ctrl+R(Windows)或Cmd+R(macOS)可以快速重放当前请求。我习惯先手动改一次captcha值验证流程,再让插件自动填——这样能100%确认是插件问题还是业务逻辑问题。
4.2 Intruder集成:把验证码识别变成“自动填充变量”
这才是captcha-killer的杀手级用法。目标:对用户名密码字典爆破时,每轮请求都自动获取新验证码并填入。
步骤分解(以某CMS后台登录为例):
第一步:准备Payloads
- Positions标签页,点击“Auto”按钮,Burp会自动标记出
username、password、captcha三个参数; - 把
captcha这一行的“Payload position”取消勾选(因为我们不希望它被字典替换,而是要动态填入); - 确保
username和password被正确标记为Payload位置。
第二步:配置Captcha Killer为Intruder的“辅助处理器”
- 切到“Resource Pool”标签页,勾选“Use resource pool for this attack”;
- 点击“Add” → “Extension-generated payload” → 选择“Captcha Killer”;
- 在弹出窗口里,设置:
- Payload type: “Captcha value”
- Parameter name: 填你之前配置的
captcha(必须和插件全局配置一致) - Image extraction regex: 填你调试好的正则(如
<img[^>]+src=["']([^"']+captcha[^"']+)["'])
第三步:启动攻击,观察实时日志
- 点击“Start attack”;
- 在Intruder的Results标签页,你会看到每一行的
captcha列都显示为“Processing…”; - 打开Extender → Logger,筛选“Captcha Killer”,能看到类似日志:
这说明插件正在为每一行Payload动态生成验证码。[Captcha Killer] Solving captcha for request #1234 [Captcha Killer] Extracted image from URL: /api/captcha?r=123456789 [Captcha Killer] Sent to http://localhost:5000/captcha -> got "A2b9C" [Captcha Killer] Injected captcha=A2b9C into parameter 'captcha'
关键经验:Intruder默认并发10线程,但你的识别服务(Tesseract)是CPU密集型,开太高会导致超时。我在测试中发现,把Intruder的“Number of threads”设为3,识别成功率稳定在92%;设为10时,30%的请求因服务响应超时(>5s)而失败。解决方案不是硬扛,而是改服务:在Flask里加
@app.route('/captcha', methods=['POST'])前加@limiter.limit("3 per minute")(需装flask-limiter),强制限流,比客户端降并发更可靠。
4.3 处理识别失败:不是重试,而是“分层降级”
100%识别率不存在。实战中,我接受20%的失败率,但必须确保失败时流程不卡死。captcha-killer提供了三种应对策略:
策略一:自动重试(推荐用于简单验证码)
在插件配置里勾选“Retry on failure”,设重试次数为2。原理是:第一次识别失败(如服务返回空),插件会自动刷新验证码图片URL(加时间戳参数),再发一次。适用于干扰线少、字体清晰的验证码。
策略二:Fallback to manual input(关键业务必开)
勾选“Prompt for manual input on failure”。当识别失败时,Burp会弹出一个小窗口,让你手动输入。我把它设为“登录爆破最后10个高价值账号”的兜底方案——既不中断流程,又保留人工干预权。
策略三:Skip and log(大规模扫描首选)
不勾选任何选项。失败时,插件把captcha参数留空,Intruder继续发请求。你在Results里按captcha列排序,一眼看出哪些行是空的,再单独导出这些Payload,用Repeater手动补。这招在扫1000个子域名的后台时特别高效。
真实体验:某次对教育平台的渗透,其验证码有5种变体(数字、字母、中文、滑块、点选)。我把captcha-killer配成“重试+手动输入”,前3种自动过,后2种弹窗让我点选图片里的水果——虽然慢点,但比写5个专用脚本省力多了。插件的价值,从来不是“全自动”,而是“可控的半自动”。
5. 进阶优化:让识别准确率从70%提升到95%的四个实战技巧
5.1 图片预处理:三行PIL代码干掉80%的干扰
Tesseract对噪声敏感。原始验证码图常有:
- 背景噪点(小黑点)
- 干扰线(斜线、波浪线)
- 字符粘连(
rn连成m)
在app.py的OCR前加预处理:
# 去噪点:3x3卷积核中值滤波 import numpy as np from scipy import ndimage img_array = np.array(img) # 中值滤波去椒盐噪声 img_array = ndimage.median_filter(img_array, size=3) img = Image.fromarray(img_array) # 去干扰线:形态学闭运算(连接断开的字符) kernel = np.ones((2,2), np.uint8) img_array = cv2.morphologyEx(img_array, cv2.MORPH_CLOSE, kernel) img = Image.fromarray(img_array) # 二值化增强对比度 img = img.point(lambda p: p < 100 and 0 or 255)需要额外装numpy和opencv-python。实测对某政务系统验证码,识别率从65%升到89%。
5.2 模型切换:用PaddleOCR替代Tesseract的实操对比
Tesseract强在通用文本,弱在验证码。PaddleOCR专为中文场景优化,且提供轻量模型(ch_ppocr_mobile_v2.0_rec_infer仅8MB)。替换步骤:
- 下载模型:
paddleocr --download-model ch; - 改
app.py里的OCR部分:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='en') # 英文模型更准 # 替换原tesseract调用: result = ocr.ocr(np.array(img), cls=True) text = result[0][0][1][0] if result and result[0] else ""对比数据(测试100张同源验证码):
| 指标 | Tesseract 5.3 | PaddleOCR v2.6 |
|---|---|---|
| 准确率 | 72% | 91% |
| 单图耗时 | 1.2s | 0.8s |
| 内存占用 | 150MB | 320MB |
注意:PaddleOCR内存高,但可通过
use_gpu=False强制CPU运行,适合测试机。生产环境建议用Docker隔离GPU资源。
5.3 请求链路加固:防止“验证码新鲜度”失效
有些系统要求:
- 验证码图片请求和提交请求必须在60秒内;
- 同一Session下,验证码只能用一次;
- 提交时必须携带图片请求返回的
ETag头。
这时,单纯填captcha值不够。我在插件配置里加了“Custom headers”功能:
- 在插件UI的“Advanced”选项卡,勾选“Send custom headers”;
- 添加两行:
X-Captcha-ETag: {{etags['/api/captcha']}}X-Captcha-Timestamp: {{timestamps['/api/captcha']}}
其中{{etags[...]}}是插件自动提取的上一次图片响应头。这需要修改插件源码(CaptchaKiller.java里加getHeaderValues逻辑),但值得——某银行系统就靠这个绕过了“验证码时效性”校验。
5.4 日志审计:把每次识别变成可追溯的渗透证据
红队报告要求“所有操作可审计”。我在app.py里加了日志记录:
import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('captcha_audit.log'), logging.StreamHandler() ] ) @app.route('/captcha', methods=['POST']) def solve_captcha(): start_time = time.time() data = request.get_json() img_b64 = data.get('image')[:50] + "..." # 截断base64防日志爆炸 logging.info(f"REQ: {request.remote_addr} | IMG_LEN: {len(data.get('image', ''))} | TIME: {start_time:.2f}") # ... OCR逻辑 ... end_time = time.time() logging.info(f"RES: TEXT='{text}' | DURATION={end_time-start_time:.2f}s | STATUS=200")生成的captcha_audit.log可直接作为渗透测试报告附件,证明“验证码识别过程全程受控、结果可验证”。
6. 最后分享一个血泪教训:别在目标服务器上跑识别服务
去年我犯过一个致命错误:为了“减少网络延迟”,把PaddleOCR服务部署在目标内网的一台测试机上(http://10.0.0.5:5000/captcha),然后让Burp插件直连。结果扫描到第37个账号时,目标WAF突然告警,安全团队迅速定位到10.0.0.5这台机器在高频请求/captcha接口——因为插件每发一个请求,都会触发一次图片拉取,而WAF把这种“非浏览器User-Agent的高频图片请求”判为恶意扫描。
从此我定下铁律:识别服务必须和Burp在同一台机器(或同一局域网可信设备),绝不能跨网络部署。如果目标网络隔离严格,宁可手动导出验证码图片,用本地服务识别后再填回,也不走远程调用。
这个教训让我明白:工具再强大,也得服从渗透测试的基本原则——隐蔽性优先于便利性。captcha-killer的价值,不在于它多“智能”,而在于它把原本不可控的手工环节,变成了可配置、可审计、可降级的标准化动作。当你下次面对一个验证码时,想的不该是“怎么绕过”,而是“怎么把它变成我攻击链路上最稳的一环”。
