尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Burp插件结合ddddocr:实现本地验证码识别自动化测试方案

Burp插件结合ddddocr:实现本地验证码识别自动化测试方案 做授权测试时最烦什么不是逻辑漏洞难挖而是登录接口的验证码。每次想跑一个密码字典、测一下登录接口的重放和爆破先得停下来手输一次验证码输错还得刷新换一张一轮测试下来有一半时间耗在“看图识字”上。后来我把Burp插件和ddddocr组合起来在本地搭了个验证码识别服务右键一点识别结果自动复制到剪贴板整套流程五分钟左右就能跑通。这篇文章就完整拆一下这个方案从为什么这么选型、环境怎么搭、插件代码怎么写到实际联调和避坑记录给同样要面对验证码自动化识别的测试同学一个可直接抄作业的参考。这个方案的核心思路很简单Burp负责抓包上下文ddddocr负责图片识别中间用本地HTTP服务做桥接。ddddocr是目前开源圈子里口碑不错的验证码识别库基于深度学习的OCR模型对常见的扭曲、干扰线、背景噪声处理都挺稳而且纯本地运行、免费、识别速度快根本不需要打码平台。这套组合最适合的人群是用Burp做Web应用授权测试、需要频繁处理图形验证码的渗透测试和红队人员如果你只是偶尔要识别几张验证码也可以直接拿Python脚本调用ddddocr没必要上插件。1. 为什么非要搞验证码自动识别1.1 验证码是测试自动化最大的拦路虎做过登录接口测试的人都有体会验证码这个东西看着不大实际是自动化流程里最恶心的一环。假设你做一个登录接口的爆破测试字典准备了一百条每条请求都需要一个有效的验证码参数才能发出去。人工识别的话即便每次验证码输入只花三秒钟一百条请求就是三百秒中间还得加上验证码输错后的刷新重试实际耗时至少翻倍。更糟糕的是人的精力是有限的连续识别几十张扭曲验证码后注意力必然下降手一抖输错了又是重新来。用Burp的宏Macros和会话处理规则能解决一部分会话维持的问题但验证码本身是个独立的“人机校验”环节Burp原生并没有识别图片的能力宏只能帮你自动拉取新的验证码图片没法帮你把图片内容翻译成字符串。所以之前很多人的做法是Burp跑着测试旁边开着验证码图片查看器人盯着屏幕手动输入整个测试节奏被硬生生切成了碎片。这种模式不仅慢而且非常容易疲劳出错尤其当天要测多个系统的时候简直是折磨。1.2 为什么是Burp插件加ddddocr这个组合市面上处理验证码自动识别的方案其实有好几种我先列一下让大家知道我是怎么对比选型的。第一种是纯Python脚本加Selenium或者Requests自己模拟整个登录流程。这种方式自由度最高但等于把Burp里已经做好的拦截、重放、Intruder那一整套流程全部抛弃了所有请求逻辑都得用代码重写一遍开发成本和学习成本都高。而且一旦目标站点有反爬或风控策略你还需要额外处理Cookie、Header、访问频率等一堆琐碎问题容易陷入代码维护的泥潭。第二种是Burp宏配合打码平台比如接入第三方验证码识别API。这种方式接口简单识别率也说得过去但有两个让我不太舒服的点一是按次计费测试量大的时候成本不低二是验证码图片要上传到第三方服务器不管目标系统的验证码设计得再简单它本质上也是系统的一个安全边界把验证码数据发出去总归存在信息暴露风险。对于授权范围内的内部测试我更倾向于数据不出本机。第三种就是本地ddddocr加Burp插件也就是这套方案的思路。ddddocr是纯本地模型不需要联网图片直接进内存推理识别一次也就几十到一百多毫秒完全在可接受范围内。Burp插件负责跟抓包流程做衔接利用Burp的上下文菜单、响应分析这些已有的能力让验证码识别成为测试流程中的一个“嵌入式工具”而不是另起炉灶。这种组合兼具了自动化程度、速度、隐私和成本的优势所以最后我选了这个方案。1.3 整体架构设计这套架构做起来比想象中轻量本质上就三个组件Python识别服务、Burp插件、中间的HTTP交互。Python识别服务用Flask写一个极简HTTP接口监听127.0.0.1的8899端口接收POST过来的图片Base64字符串内部调用ddddocr识别再把结果以JSON格式返回。Burp插件用Java开发注册一个右键菜单项。当测试人员选中一个响应请求后插件自动提取响应体中的图片数据编码成Base64POST到本地服务拿到识别结果后弹出提示并复制到剪贴板。交互约定图片格式用Base64在JSON里传响应统一返回{code: 0, result: xxxx}这样的结构两边都省心。为什么要拆成“本地服务加插件”而不是直接在Java插件里调用ddddocr原因很简单ddddocr是Python生态的项目底层依赖ONNX Runtime推理引擎如果你非要在Java里直接调用要么走JNI绕一大圈要么找Java的替代实现稳定性和维护成本都不可控。拆成一个本地HTTP服务后Python那边怎么搞都行Java这边只需要发起HTTP请求两边逻辑解耦后续想换成其他识别引擎也只需要改服务端。2. 动手前的准备环境与工具选型2.1 前置条件清单实际操作前先把下面这些环境准备好。这套东西对版本不算挑剔但基础条件还是要有的Burp Suite社区版就够了专业版当然更好。新版旧版都能跑插件代码用的是Burp Extender经典API。Python 3.8及以上版本主要是为了ddddocr和Flask的兼容性。JDK 8及以上用于编译Burp插件。如果你不想自己编译也可以直接下载现成的Burp扩展Jar包但自己编译也不难几分钟的事。一个可以访问的目标测试环境我下面演示用的是本机搭的测试站点大家实际操作时请务必确认目标系统有明确授权。ddddocr的安装很简单直接pip即可。如果网络环境不佳记得指定国内镜像源。pip install ddddocr flask这里有个容易踩的坑ddddocr会自动带上onnxruntime这个推理引擎onnxruntime和numpy之间的版本有时会打架。如果安装后import报错先检查numpy版本一般pip install -U numpy就能解决。具体问题后面排查小节再详细说。2.2 ddddocr 使用要点ddddocr的官方文档内容不算多但核心概念就几个简单过一遍就够用了。它的核心方法是classification传入图片的字节流返回识别出的字符串。还有一个det方法用于目标检测加识别主要是处理带位置标注的复杂验证码。我们平时遇到最多的图形验证码也就是字母数字混合、有扭曲和干扰线那种用classification就够了。初始化时有个参数值得注意show_adFalse。ddddocr默认会在控制台打印一些广告信息如果不关掉开着服务的时候会不断刷屏实际上不影响识别但看着难受。建议初始化时果断关掉。import ddddocr ocr ddddocr.DdddOcr(show_adFalse) with open(captcha.png, rb) as f: image_bytes f.read() result ocr.classification(image_bytes) print(result)第一次运行的时候ddddocr会把内置的ONNX模型文件缓存到本地用户目录所以第一次加载会慢一点后续就都是毫秒级加载了。模型下载需要联网如果你的测试环境是内网隔离环境安装完ddddocr后最好先联网跑一次把模型缓存好然后再断网使用。2.3 搭建Python识别服务我直接给你一个能跑的最小实现就是下面这个ocr_server.py。为什么用Flask不用FastAPI因为这里只需要一个POST接口Flask单文件搞定、零配置丢到服务器上直接python ocr_server.py就能跑FastAPI虽然性能更好但对这个场景属于杀鸡用牛刀没必要。import base64 import json from flask import Flask, request, jsonify import ddddocr app Flask(__name__) ocr ddddocr.DdddOcr(show_adFalse) app.route(/ocr, methods[POST]) def ocr_captcha(): try: data request.get_json(forceTrue) if not data or image not in data: return jsonify({code: 400, msg: missing image field}) img_bytes base64.b64decode(data[image]) result ocr.classification(img_bytes) return jsonify({code: 0, result: result}) except Exception as e: return jsonify({code: 500, msg: str(e)}) app.route(/health, methods[GET]) def health(): return jsonify({code: 0, msg: ok}) if __name__ __main__: app.run(host127.0.0.1, port8899)启动之后先用curl做一个快速验证确保服务本身没问题curl -X POST http://127.0.0.1:8899/ocr \ -H Content-Type: application/json \ -d {\image\:\$(base64 -w0 captcha.png)\}正常返回的JSON大概是这样的{code: 0, result: a3f8}到这里识别服务已经就绪。接下来要做的是Burp插件这才是把验证码识别集成进测试流程的关键环节。3. 核心实现Burp插件接入OCR服务3.1 插件功能设计我在设计这个插件时给它定的功能很克制就三个核心点第一在Burp的右键菜单里加一个“识别当前响应中的验证码”菜单项。这个菜单项的作用范围是任意的HTTP消息选中一条带有验证码图片的响应就能触发。第二插件提取响应体内容整体作为验证码图片数据发送给本地OCR服务。这里有个前提就是当前响应本身就是一张验证码图片比如直接访问/captcha接口返回的那种。如果你遇到的是HTML页面里嵌的验证码可以先在响应体中找到图片地址再单独请求一次验证码图片拿到图片响应后再右键识别。第三识别结果不仅要展示在弹窗里还要自动复制到系统剪贴板。这个设计是为了方便配合Repeater或Intruder使用识别出来一个验证码马上粘贴到参数里手不离键盘就能完成整个操作。我把结果复制到剪贴板这件事做得比较重是因为实测过程中发现弹窗点击“确定”再手动复制虽然也就多两步但连续测试几十次之后这两步会非常消耗耐心。自动复制一步到位体验会好很多。3.2 插件源码解读Burp插件的Java代码整体不长核心逻辑集中在四个方法里。完整代码如下我会在代码后面逐段解释import burp.*; import javax.swing.*; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Base64; import java.util.List; public class BurpExtender implements IBurpExtender, IContextMenuFactory { private IBurpExtenderCallbacks callbacks; private static final String OCR_URL http://127.0.0.1:8899/ocr; Override public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { this.callbacks callbacks; callbacks.setExtensionName(Captcha OCR Helper); callbacks.registerContextMenuFactory(this); callbacks.printOutput([Captcha OCR] loaded. Right-click response - recognize captcha.); } Override public ListJMenuItem createMenuItems(IHttpRequestResponse[] requestResponse) { ListJMenuItem menus new ArrayList(); JMenuItem item new JMenuItem(识别当前响应中的验证码); item.addActionListener(e - recognizeFromResponse(requestResponse)); menus.add(item); return menus; } private void recognizeFromResponse(IHttpRequestResponse[] requestResponse) { if (requestResponse null || requestResponse.length 0) { JOptionPane.showMessageDialog(null, 未选中任何HTTP消息); return; } byte[] response requestResponse[0].getResponse(); if (response null) { JOptionPane.showMessageDialog(null, 当前消息没有响应内容); return; } IResponseInfo info callbacks.getHelpers().analyzeResponse(response); int bodyOffset info.getBodyOffset(); byte[] body java.util.Arrays.copyOfRange(response, bodyOffset, response.length); String bodyB64 Base64.getEncoder().encodeToString(body); String result callOcr(bodyB64); if (result null) { JOptionPane.showMessageDialog(null, OCR服务调用失败请确认127.0.0.1:8899已启动); return; } callbacks.printOutput([Captcha OCR] result: result); Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(new StringSelection(result), null); JOptionPane.showMessageDialog(null, 识别结果: result \n已自动复制到剪贴板); } private String callOcr(String imageBase64) { HttpURLConnection conn null; try { URL url new URL(OCR_URL); conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json); conn.setDoOutput(true); conn.setConnectTimeout(3000); conn.setReadTimeout(10000); String payload {\image\:\ imageBase64 \}; try (OutputStream os conn.getOutputStream()) { os.write(payload.getBytes(UTF-8)); } int code conn.getResponseCode(); if (code ! 200) { callbacks.printError([Captcha OCR] HTTP code); return null; } BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), UTF-8)); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } String json sb.toString(); int start json.indexOf(\result\:\); if (start 0) { start \result\:\.length(); int end json.indexOf(\, start); return json.substring(start, end); } } catch (Exception e) { callbacks.printError([Captcha OCR] e.toString()); } finally { if (conn ! null) { conn.disconnect(); } } return null; } }registerExtenderCallbacks是Burp插件的入口等价于程序的主函数。插件加载时Burp会调用这个方法把回调对象传进来。我在里面做了三件事设置插件名、注册右键菜单工厂、在Burp的Output标签页打印一行加载日志。createMenuItems是右键菜单的触发入口。每次用户在Burp的任意消息列表上点右键Burp都会调用这个方法返回的JMenuItem列表会显示在右键菜单中。这个插件只注册了一个菜单项点击后触发recognizeFromResponse。recognizeFromResponse是核心处理逻辑。先判断有没有选中HTTP消息然后从请求响应对象中取出响应字节流。这里用到helpers.analyzeResponse来定位响应体的起始偏移量再从这个偏移量开始复制出完整的响应体数据。对于一张验证码图片的响应来说响应体就是图片的二进制内容。最后把二进制数据Base64编码POST给本地OCR服务。为什么要把图片数据Base64编码后再传输因为HTTP的JSON载荷是文本协议直接塞二进制会有编码问题Base64是这类场景最通用的做法。实际跑通的识别链路时间基本在200到400毫秒之间其中绝大部分是ddddocr的推理时间。还有个小细节Base64字符集只包含大小写字母、数字、加号和斜杠不包含JSON中的引号和反斜杠所以我构造{image:...}这个JSON时没有做额外的转义处理也不会出错。如果你用其他语言实现建议还是用正规的JSON库去构造更稳妥。3.3 编译打包与加载插件的编译不需要复杂的构建工具直接javac就能搞定。前提是你的classpath里有Burp Extender的API库也就是burp-api的jar包。最简单的方式是去Burp的官方仓库或项目依赖里找到burp-apijar然后执行编译javac -cp burp-api.jar:. BurpExtender.java jar cf captcha-ocr-helper.jar BurpExtender.class编译完成后会得到一个captcha-ocr-helper.jar这就是可以直接加载到Burp的扩展插件包。加载过程分三步打开Burp进入Extender标签页点击Add按钮在弹出的对话框中选择插件类型为Java然后选中刚才打包的jar文件。加载成功后Output标签页里应该能看到[Captcha OCR] loaded这行日志。如果没看到说明加载过程中出了问题后面排查章节会讲。3.4 实战联调五分钟的完整流程现在把整个流程串起来从零开始完整跑一遍。假设目标是一个带验证码的登录接口验证码图片由/captcha地址提供登录接口是/login。第一步启动Python识别服务确认health接口返回正常python ocr_server.py运行后控制台会输出* Running on http://127.0.0.1:8899第二步用Burp浏览目标系统打开登录页面截获浏览器对/captcha的请求。如果不想等浏览器自动请求也可以直接手动访问http://目标地址/captcha把请求发送到Repeater里。第三步在Repeater里点击Send拿到验证码图片的响应。响应头里应该有Content-Type: image/png或image/jpeg响应体就是图片数据。第四步在响应内容区域点右键选择“识别当前响应中的验证码”。这个时候插件会执行三件事提取图片数据、调用本地OCR、把结果复制到剪贴板。第五步弹窗显示识别结果通常是类似a3f8的四位或六位字符串。点掉弹窗后验证码已经在剪贴板里了。第六步回到登录请求把验证码参数的值粘贴进去发送请求测试登录接口。如果想测试多组账号密码用Intruder跑每轮迭代前先去Repeater刷新一次验证码并识别然后手动粘贴到Intruder的payload位置。第七步重复第三步到第六步。熟练之后刷新验证码、右键识别、粘贴参数整个动作控制在五秒以内比人工看图输入快得多而且基本不会疲劳。实际测试中我用一个带彩色背景和干扰线的四位验证码做了连续20次识别正确率在85%到95%之间单次识别耗时约300毫秒。这个速度对于手动辅助测试完全够用。如果某一次识别结果明显不对刷新验证码再识别一次即可。4. 常见问题与排查技巧实录4.1 Python服务启动失败Flask服务启动时报错的情况主要分两种。一种是端口被占用报Address already in use通常是你之前启动的进程没有kill掉。解决办法是换个端口或者用netstat -ano | findstr 8899找出占用进程并结束它。另一种是ddddocr初始化时崩溃最常见的报错是DLL load failed或No module named onnxruntime。这种情况绝大多数是onnxruntime没装好或者版本不匹配。执行pip install -U onnxruntime numpy重新装一遍问题基本就能解决。还有Windows下偶尔会遇到Visual C运行库缺失导致的onnxruntime加载失败装上微软对应的VC运行库即可。4.2 插件连不上本地OCR服务插件弹出“OCR服务调用失败”提示时先从三个方向排查。第一个方向确认Python服务还活着。如果之前是前台启动的关掉终端窗口服务就停了重新启动一次就好。第二个方向确认Burp所在机器的网络能否访问到127.0.0.1:8899。如果你用的是Burp自带的浏览器它默认走Burp代理本地访问不受影响但如果你在Burp外层的系统代理里设置了某些代理规则可能会把127.0.0.1的流量也代理出去导致本地连接被转发到代理服务器上。解决办法是在代理规则中排除本机地址。第三个方向确认插件的超时设置。我代码里给的是连接超时3秒、读取超时10秒。如果你的机器CPU比较弱ddddocr推理比较慢10秒读取超时也可能不够可以把setReadTimeout调大到20秒。4.3 识别率忽高忽低怎么办ddddocr对常见的字母数字验证码识别率很高但它不是万能的遇到下面几种情况识别率会明显下降。第一种情况是验证码图片里有大量高饱和度的彩色噪点或复杂的背景花纹。这种图片的字符和背景粘连比较严重模型容易误判。一个有效的预处理手段是先对图片做灰度化和简单降噪再送进OCR。你可以在Python服务端加一段PIL预处理逻辑比如转灰度、增强对比度、二值化。from PIL import Image import io img Image.open(io.BytesIO(img_bytes)).convert(L) img img.point(lambda x: 0 if x 128 else 255) buf io.BytesIO() img.save(buf, formatPNG) img_bytes buf.getvalue()这段代码把图片压成黑白两色对背景和字符颜色对比明显的验证码有效。但如果验证码本身是浅色字符加深色背景二值化阈值可能要反过来调需要根据实际情况试。我自己的经验是先不做任何预处理直接识别如果发现识别率确实不行再考虑加预处理因为预处理参数调不好反而会降低效果。第二种情况是图片里带了边框、提示文字等额外元素。比如验证码图片整体是一个白色画布下面有一行“点击刷新”的小字这种多余信息会干扰识别。解决方式是先对图片按比例裁剪掉边缘区域只保留中间字符区域。裁剪范围需要通过观察原始图片尺寸来确定没有统一参数。第三种情况是验证码是GIF动图。ddddocr对gif的处理效果不稳定因为动图的每一帧都可能不同。这种场景可以先取gif的第一帧转成静态图再识别用PIL的Image.open加seek(0)就能做到。后续扩展里我会再讲更完整的思路。4.4 复杂验证码场景的进阶思路上面说的都是最普通的四位或六位字母数字图形验证码。实际测试中还会遇到三类更复杂的验证码场景这里简单给一下思路。第一类是算术验证码比如图片上写着“3 5 ”。ddddocr直接识别会得到一串“35”拿不到最终数字。但好消息是你可以在得到OCR文本之后用Python解析出算式并算出结果核心识别能力还是复用ddddocr的。我在实际项目中就做过这个扩展Python服务端加一段简单的正则替换就能处理加减法乘除法也同理。第二类是滑块验证码和点选验证码。这两类已经不是“图片内容识别”的问题而是“目标定位”和“轨迹模拟”的问题ddddocr虽然提供了det方法做目标检测定位但完整的滑块验证码流程还包括计算拖动距离、模拟人类轨迹、处理风控校验这个复杂度远超本篇范围。如果你遇到这类验证码建议优先考虑用Selenium或Playwright这类浏览器自动化工具去模拟真实操作而不是硬怼识别。第三类是接口返回的JSON里直接内嵌验证码的Base64数据。这种情况不需要从响应体提取二进制而是可以直接从JSON里解析出Base64字符串。对这类场景插件逻辑可以加一个分支如果检查到响应内容是JSON格式就尝试从JSON字段中提取captcha、img、image等常见字段的Base64值再送去识别。由于各家接口字段名不统一这个分支逻辑难免需要针对目标站点微调。4.5 关于合规与测试边界最后必须强调一点验证码自动识别技术是把双刃剑。在自己负责的授权测试项目里它是提升效率的利器如果被用在未授权的自动化攻击中比如对线上系统做批量注册、撞库、短信轰炸那就是明确的违规行为。所以使用这套方案之前务必先确认系统的测试授权范围和边界只在明确允许的测试环境或生产系统中使用。我在自己的项目里只在两种场景下用它一是授权渗透测试中对登录接口进行安全验证二是内部安全测试平台中做自动化巡检。这两个场景都提前获得了系统负责人的书面授权也约定好了测试时间段。技术本身没有好坏之分关键是使用的人有没有守住底线。4.6 顺手补充的几个小技巧插件加载后在Burp的Output标签页会有日志输出排查问题时先看这里的报错信息比瞎猜效率高。如果觉得弹窗烦人可以把JOptionPane.showMessageDialog那行注释掉只保留剪贴板复制功能降低操作打断感。识别服务启动时可以加一个--port参数解析逻辑这样不同项目可以起不同端口的服务避免多实例冲突。配合Burp的Intruder使用时可以给Intruder设置一个自定义Payload Processor每次请求前自动调用OCR服务识别新验证码实现“半自动”甚至“全自动”注入。这个扩展工作量不大但已经超出本篇的5分钟快速搭建范畴想深入做的可以自己研究。我在实际使用中最大的感受是这套工具最好用的点不在于“全自动”而在于“顺手”。它没有改变我原有的Burp测试习惯只是把验证码这个最打断节奏的环节变成了一个右键动作识别结果自动进剪贴板整个思考链路不断档。对于一周要测三四个系统、每个系统都有登录验证码的测试人员来说这套小工具的体验提升是非常明显的。
返回列表