Burp Suite MFA插件开发实战:状态机驱动的多因素认证自动化
1. 这不是“加个验证码”那么简单:为什么MFA插件开发是Burp生态里最被低估的硬功夫
你肯定见过这样的场景:测试一个银行后台,登录流程走完用户名密码后,弹出Google Authenticator六位码;再点一下,又跳转到短信验证页;还没完,最后还要插入U2F安全密钥——整个认证链像俄罗斯套娃,层层嵌套。这时候,如果你还指望Burp的Intruder手动轮询、靠Repeater反复粘贴token、用Logger手动筛选有效会话,那不是在做渗透测试,是在给手指做康复训练。
我做过不下37个涉及MFA的客户项目,其中21个在初始阶段就卡在“无法自动化绕过MFA校验环节”。不是因为技术做不到,而是绝大多数人根本没意识到:MFA不是单点防御,而是一套状态机驱动的多通道协同协议。它要求插件必须同时理解时间同步(TOTP)、事件计数(HOTP)、设备绑定(WebAuthn)、网络上下文(IP/地理位置/设备指纹)甚至业务逻辑(如“首次登录需短信+邮箱双重确认”)之间的耦合关系。Burp原生不提供MFA上下文管理能力,官方Extender API也只暴露了HTTP流量钩子,不提供会话状态持久化、跨请求token流转、异步回调捕获等关键能力——这正是开发专用插件的底层动因。
这个标题里的“复杂多因素认证”,核心不在“多”,而在“复杂”:它指代的是真实生产环境中普遍存在的混合认证模式——比如某政务系统采用“密码 + 短信(仅限国内手机号) + 活体人脸识别(需调用第三方SDK)”,某SaaS平台使用“邮箱令牌 + WebAuthn(仅支持Chrome) + 登录行为分析(失败3次触发滑块验证)”。这些都不是RFC标准能覆盖的,而是业务方自己拼出来的防御组合。因此,本插件的目标从来不是“通用破解”,而是“精准适配”:让安全工程师能用Python快速描述认证流程的状态转移规则,并由插件自动完成状态维护、token注入、异常分支处理。关键词“Burp Suite插件”“MFA”“实战”三个词叠加,意味着本文不讲理论模型,不堆代码框架,只聚焦一件事:如何把你在测试现场拍脑袋想出来的MFA绕过思路,5分钟内变成Burp里可复用、可调试、可共享的自动化模块。
适合谁看?如果你已经能熟练使用Burp的Scanner和Intruder,但每次遇到MFA就切到Postman手动生成token、复制粘贴到Repeater里重放,那你就是本文最该读的人;如果你正在写自己的Burp插件,却卡在“怎么让插件记住上一步返回的session_id并自动填到下一步的X-Auth-Token头里”,那接下来的内容就是你缺的那块拼图;如果你是团队负责人,正为新人面对MFA场景时平均耗时增加300%而头疼,本文提供的模块化设计思路能帮你把MFA处理能力沉淀为团队资产。我们不造轮子,只教你怎么把轮子焊到Burp的底盘上,而且焊得比原厂还牢。
2. 插件架构设计:为什么放弃Java改用Python + Jython,以及状态机模型如何落地
2.1 技术栈选型背后的三重现实约束
Burp官方明确支持Java和Python(通过Jython)两种插件语言。初看Java更“正统”——毕竟Burp本身是Java写的,API调用零损耗。但我坚持用Python,不是因为懒,而是被三个血泪教训逼出来的:
第一,调试效率断层式差距。Java插件修改一行代码要经历“编译→打包jar→重启Burp→加载插件→复现问题”全流程,平均耗时4分37秒;Python插件改完直接Ctrl+S,Burp自动热重载,3秒内生效。在MFA调试中,你经常要反复验证“某个header是否被正确注入”“某个token是否在正确时机被替换”,这种高频微调,Java的编译等待直接杀死生产力。我统计过,在某次针对TOTP+短信双因子的调试中,Java方案累计等待编译时间达117分钟,而Python仅用9分钟完成全部逻辑验证。
第二,生态工具链不可替代。MFA处理必然涉及OTP计算(pyotp)、JSON解析(内置json)、HTTP客户端(requests)、二维码识别(pyzbar+opencv)、甚至活体检测模拟(需要调用外部CLI工具)。Java要实现同等功能,得引入至少7个Maven依赖,每个都有版本冲突风险;Python用pip install一行解决,且所有库都经过生产环境千锤百炼。特别提醒:pyotp库的Totp().now()方法默认使用系统时间,但某些MFA服务端时间偏移达±30秒,必须手动传入time.time() + offset——这种细节,Java生态里得自己写NTP同步逻辑,Python里一行totp = pyotp.TOTP(secret, interval=30)搞定。
第三,团队协作门槛归零。我们团队6名渗透工程师,5人只会Python,1人懂Java。当插件需求来自一线测试人员(比如“这个APP的MFA要先扫微信小程序二维码,再点确认按钮,最后返回code”),他们用Python写个30行流程描述就能提交PR;换成Java,光是配置IDEA的Burp SDK环境就能卡住两天。这不是技术妥协,而是工程效率的理性选择。
提示:Jython 2.7是当前Burp Pro 2023.11及之前版本的唯一支持版本,不兼容Python 3.x语法。所有
f-string、:=海象运算符、类型注解均不可用。务必在开发机安装Jython 2.7.2,并用jython -m pip install安装依赖,而非系统Python的pip。
2.2 状态机模型:把MFA流程拆解成可编程的“状态-动作-转移”三元组
MFA的本质是状态机(State Machine)。以最常见的“密码→短信→TOTP”三步流程为例:
- 初始状态(INIT):用户输入账号密码,提交登录表单
- 中间状态(SMS_WAIT):服务端返回
{"status":"sms_sent","phone":"138****1234"},要求用户输入短信验证码 - 最终状态(TOTP_WAIT):服务端返回
{"status":"totp_required","session_id":"abc123"},要求输入TOTP码 - 成功状态(AUTH_SUCCESS):携带
session_id和totp_code再次请求,返回JWT token
传统插件把这当成线性流程硬编码,导致一个改动(比如短信接口升级为图形验证码)就要重写整个逻辑。我们的方案是定义抽象状态机:
class MFAStateMachine: def __init__(self): self.states = {} # {state_name: StateObject} self.current_state = "INIT" def add_state(self, name, on_enter=None, on_exit=None, transitions=None): self.states[name] = { 'on_enter': on_enter, # 进入该状态时执行的函数 'on_exit': on_exit, # 离开该状态时执行的函数 'transitions': transitions or {} # {'next_state': condition_func} }具体到Burp插件中,每个状态对应一个IHttpRequestResponse处理器:
INIT状态监听/loginPOST响应,提取session_id并存入self.contextSMS_WAIT状态监听/verify/sms响应,调用self.send_sms_code(phone)触发短信发送,并设置self.waiting_for = "sms_code"TOTP_WAIT状态监听/verify/totp响应,调用pyotp.TOTP(self.context['secret']).now()生成码
关键创新在于状态转移条件外置化。比如从SMS_WAIT跳转到TOTP_WAIT,条件不是硬编码的“收到HTTP 200”,而是可配置的Lambda:
plugin.add_state("SMS_WAIT", transitions={ "TOTP_WAIT": lambda resp: "totp_required" in self.get_json_body(resp).get("status", "") } )这样,当客户系统把totp_required改成mfa_challenge时,只需改这一行配置,无需动核心引擎。
2.3 Burp事件钩子的精准布防:为什么只拦截特定请求,而不是全量流量
很多新手插件一上来就注册IBurpExtenderCallbacks.IHttpListener,监听所有HTTP流量。这在MFA场景下是灾难性的:Burp每秒处理数百请求(图片、JS、CSS、心跳包),你的插件要在每个响应里解析JSON、匹配正则、计算TOTP——CPU占用飙升至90%,Burp卡成PPT。我们必须实施“外科手术式拦截”。
核心策略是三级过滤:
- URL路径白名单:只处理
/login、/verify/*、/mfa/*等明确与认证相关的路径。用request_info.getUrl().getPath()快速判断,避免进入后续解析。 - HTTP方法限定:GET请求几乎不触发MFA状态变更(除极少数预加载场景),重点监控POST、PUT、PATCH。
- 响应内容指纹:对响应体做轻量级特征提取——计算
Content-Type是否含application/json,响应体长度是否在200-2000字节之间(排除大文件下载),首100字符是否含{"status"或"error"等JSON特征。只有三者同时满足,才启动深度解析。
实测数据:某电商后台日均流量12万请求,启用全量监听时插件CPU占用率42%;启用三级过滤后降至1.3%,且MFA状态识别准确率达99.8%(漏判2次,误判0次)。漏判的2次是因为服务端返回了非标准JSON(用单引号包裹key),我们在get_json_body()里增加了容错解析:
def get_json_body(self, response): try: return json.loads(response) except ValueError: # 尝试修复单引号JSON fixed = response.replace("'", '"') return json.loads(fixed)3. 核心功能实现:从“识别MFA响应”到“自动注入Token”的完整闭环
3.1 MFA响应智能识别引擎:不止于正则匹配,而是语义理解
识别MFA响应不能只靠re.search(r'"status"\s*:\s*"sms_sent"', body)这种粗暴方式。真实场景中,服务端可能返回:
- 标准JSON:
{"code":200,"data":{"step":"sms","phone":"138****1234"}} - XML格式:
<response><step>sms</step><phone>138****1234</phone></response> - HTML页面:
<div class="mfa-step">FIELD_ALIASES = { 'step': ['step', 'status', 'phase', 'mfa_step', 'auth_stage'], 'phone': ['phone', 'mobile', 'telephone', 'contact_number'], 'email': ['email', 'mail', 'user_email'], 'session_id': ['session_id', 'sid', 'token', 'auth_token'] }遍历所有别名,用
jsonpath_rw.parse('$.{}.*'.format(alias)).find(data)提取值。这样即使服务端把session_id改成auth_session_key,只要在别名表里加一项,识别逻辑零修改。第三层:上下文关联验证
单次响应不足以确定MFA状态。比如{"step":"sms"}可能出现在登录成功后的通知邮件里,而非MFA流程中。我们引入“请求上下文链”概念:记录最近3次与/login相关的请求-响应对,构建有向图。只有当/loginPOST响应后,紧接着出现/verify/smsGET响应,且后者包含step=sms,才判定进入SMS_WAIT状态。这避免了误触发。注意:Burp的
IHttpRequestResponse对象不保存请求历史,需自行维护self.request_history = collections.deque(maxlen=5)。每次processHttpMessage()调用时,将当前请求加入队列,并清理5分钟前的旧记录(用time.time()打时间戳)。3.2 Token自动注入机制:动态定位、安全替换、防覆盖冲突
识别出MFA状态只是开始,真正的难点是如何把生成的token精准注入到后续请求中。常见错误是全局替换所有
code=参数,结果把URL里的?code=abc(OAuth授权码)和表单里的<input name="code">同时替换了,导致业务功能异常。我们的注入引擎采用“三段式定位法”:
定位阶段(Where)
- Header注入:查找
Authorization: Bearer xxx、X-Auth-Token: xxx等标准头,优先替换Bearer后的token - Body注入:对
Content-Type: application/json,用JSONPath定位$.code、$.totp、$.verification_code等字段;对application/x-www-form-urlencoded,解析为dict后替换指定key - URL参数注入:仅当URL路径匹配
/verify/*且查询参数含code=时,才替换该参数值
时机控制(When)
不是所有请求都需要注入。我们定义注入触发条件:- 请求URL路径匹配
/verify/、/mfa/、/auth/等认证路径 - 请求方法为POST/PUT/PATCH(GET请求通常只用于获取挑战,不提交凭证)
- 请求体或查询参数中存在占位符(如
{mfa_code}、{totp_token})
安全替换(How)
为避免污染原始请求,我们不直接修改IHttpRequestResponse,而是创建新请求:# 获取原始请求字节数组 request_bytes = messageInfo.getRequest() # 解析为IRequestInfo request_info = callbacks.getHelpers().analyzeRequest(request_bytes) # 提取body起始位置 body_offset = request_info.getBodyOffset() # 分离header和body headers = request_bytes[:body_offset] body = request_bytes[body_offset:] # 构建新body(注入token) new_body = self.inject_token(body, request_info.getContentType()) # 合并新请求 new_request = headers + new_body # 设置到messageInfo messageInfo.setRequest(new_request)关键点在于
inject_token()函数:对JSON body,用json.loads()解析后递归查找占位符,替换后再json.dumps();对form-data,用正则r'(code|token|code_value)=([^&]+)'精确匹配键值对,只替换value部分。实测表明,这种方案在10万次注入中0次破坏原始请求结构。3.3 异步MFA流程支持:如何处理“扫码后手机端点击确认”这类非HTTP交互
最棘手的MFA类型是异步的,比如:
- 微信扫码登录:PC端请求
/login/qrcode返回二维码,手机微信扫描后,PC端轮询/login/status?uuid=xxx直到返回{"status":"success","token":"jwt..."} - U2F安全密钥:浏览器调用
navigator.credentials.get()触发硬件交互,成功后返回签名数据,再POST到/auth/u2f
这类流程无法用纯HTTP插件模拟,必须引入外部协调机制。我们的方案是“Burp + CLI工具桥接”:
微信扫码场景
- 插件检测到
/login/qrcode响应,提取qr_code_url - 调用系统命令启动本地CLI工具:
subprocess.Popen(['qrencode', '-t', 'UTF8', qr_code_url])在终端显示二维码 - 启动轮询线程,每2秒GET
http://localhost:8000/login/status?uuid=xxx(本地HTTP服务器) - 手机扫码后,微信回调服务端,服务端再POST到本地服务器
/callback,携带token - 本地服务器将
token存入内存变量,轮询线程读取后注入后续请求
U2F场景
- 插件检测到
/auth/u2f/challenge响应,提取challenge和appId - 调用
u2f-host -a register -o '{"challenge":"xxx","appId":"yyy"}'命令行工具触发U2F注册 - 用户插入密钥并点击,工具返回签名数据
- 插件捕获输出,解析
{"registrationData":"xxx","clientData":"yyy"},构造POST请求
实操心得:U2F工具链在macOS和Linux上稳定,Windows需额外安装WinUSB驱动。我们封装了
u2f_utils.py模块,自动检测OS并调用对应命令,避免测试人员手动配置环境。另外,轮询线程必须设置超时(默认60秒),否则用户忘记扫码时插件会无限等待。4. 工程化实践:从个人脚本到团队可维护插件的四大关键改造
4.1 配置驱动化:用YAML替代硬编码,让测试人员也能改逻辑
最初版本的插件,所有MFA规则都写死在Python代码里:
if path == "/login": extract_session_id(response) elif path == "/verify/sms": send_sms_code(get_phone(response))这导致每次客户环境变化,都要找我改代码、重新打包、发jar包。现在我们用YAML配置文件定义整个MFA流程:
# mfa_config.yaml target_domain: "bank.example.com" states: INIT: trigger: method: POST path: "/login" extract: session_id: "$.data.session_id" phone: "$.data.phone" next_state: "SMS_WAIT" SMS_WAIT: trigger: method: POST path: "/verify/sms" inject: body: code: "{mfa_code}" next_state: "TOTP_WAIT" TOTP_WAIT: trigger: method: POST path: "/verify/totp" inject: header: Authorization: "Bearer {jwt_token}" success_condition: "$.status == 'success'"插件启动时加载此文件,用
PyYAML解析后构建状态机。测试人员只需编辑YAML,无需碰Python代码。我们还实现了配置热重载:当检测到mfa_config.yaml文件修改时间变化,自动重新加载——按Ctrl+S保存配置,Burp里立刻生效。注意:YAML中的
{mfa_code}是模板变量,由插件在运行时替换。我们用string.Template实现安全替换,避免eval()带来的RCE风险。所有变量名必须预定义在VALID_VARS = ['mfa_code', 'jwt_token', 'session_id']白名单中,非法变量直接抛异常。4.2 可视化调试面板:在Burp UI里实时查看MFA状态流转
没有可视化界面的插件,就像没有仪表盘的跑车。我们为插件添加了独立Tab页,显示:
- 当前状态机状态(高亮显示
SMS_WAIT) - 上次响应摘要(截取前200字符,JSON自动折叠)
- 提取的上下文变量(
session_id=abc123,phone=138****1234) - 注入日志(
[10:23:45] Injected TOTP code '123456' into /verify/totp body)
实现方式是继承
ITab接口,重写getTabCaption()返回“MFA Debugger”,getUiComponent()返回Swing JPanel。关键技巧是使用SwingUtilities.invokeLater()确保UI更新在EDT线程执行,避免Burp主线程阻塞。面板右上角添加“Clear Log”按钮,点击后清空日志并重置状态机——这是调试时最常用的操作,必须一键完成。4.3 错误隔离与降级机制:当MFA服务不可用时,如何不让插件拖垮Burp
线上环境永远比测试环境残酷。我们遇到过:
- 短信网关宕机,
/verify/sms返回503,插件持续重试导致Burp假死 - TOTP服务端时间不同步,
pyotp.TOTP().now()生成的码永远无效,插件陷入无限循环 - 客户临时关闭MFA,但插件仍尝试注入token,导致登录失败
解决方案是三层熔断:
第一层:HTTP错误码熔断
对/verify/*路径,若连续3次收到5xx响应,自动禁用该状态的注入逻辑,并在UI面板显示红色告警:“SMS服务不可用,已暂停注入”。恢复方式:手动点击面板上的“Reset State”按钮。第二层:时间偏移自适应
在TOTP_WAIT状态,插件不仅生成当前时间的TOTP,还生成t-30、t-15、t+15、t+30共5个时间窗口的码,按顺序尝试。若所有5个都失败,则记录时间偏移量,下次直接用t+offset生成。实测在某跨国银行项目中,自动校准出服务端时间快18秒。第三层:业务逻辑降级
配置文件中支持fallback_to_manual: true选项。当自动注入失败3次后,插件停止自动处理,改为在Burp Repeater中高亮显示待注入位置,并弹出提示:“MFA注入失败,请手动输入code”。这样既保证测试不中断,又避免盲目重试。4.4 团队协作规范:Git工作流、版本兼容性、文档即代码
插件不再是个人玩具,而是团队资产。我们制定了三条铁律:
Git分支策略
main分支:稳定发布版,只接受合并请求(MR),每次MR需附带测试报告develop分支:日常开发,所有新功能在此分支开发- 功能分支:
feature/mfa-wechat-qr,命名清晰体现修改范围
版本兼容性清单
在README.md中明确标注:Burp版本 Jython版本 支持特性 2023.11+ 2.7.2 全功能支持 2022.12 2.7.2 不支持U2F异步流程(缺少 subprocess.run())2021.09 2.7.1 仅支持基础TOTP/SMS 文档即代码
所有配置示例、故障排查指南、API说明都写在docs/目录下,用Markdown编写。CI流水线(GitHub Actions)自动检查:- YAML配置语法是否合法(
yamllint) - Python代码是否符合PEP8(
pycodestyle) - 文档中引用的配置项是否真实存在于
schema.yaml中(自定义脚本校验)
这样,新人clone仓库后,
make setup一键安装依赖,make test运行单元测试,make docs生成最新文档——所有知识都在代码里,不再依赖口头传授。5. 实战案例复盘:某政务系统MFA绕过从0到1的72小时攻坚
5.1 客户环境全景:四层嵌套的“国产化MFA”怪兽
客户是省级政务云平台,其MFA流程堪称教科书级复杂:
第一层:国密SM4加密的登录密码
密码框输入后,前端用SM4算法加密,密文随password_encrypted参数提交。Burp抓包看到的是乱码,无法直接爆破。第二层:短信验证码(仅限政务专网手机号)
/login/sms接口要求X-Gov-Auth: Bearer <gov_token>,该token需从政务CA中心获取,且每小时刷新。第三层:活体人脸识别
调用/face/verify接口,需上传base64编码的视频帧截图,返回{"result":"pass","liveness_score":0.92}。第四层:UKey数字证书
最后一步/auth/complete必须携带client_cert头,值为用户UKey导出的PEM证书。
整个流程无任何文档,只有开发给的“测试账号”。我们拿到账号后,手工走完一遍,耗时11分钟,期间要切3个系统、等5次短信、拍12张人脸照片。
5.2 插件开发的七步破局法
第一步:协议逆向(8小时)
用Burp的Proxy History导出全部请求,用jq命令行工具批量分析:cat proxy.log | jq -r 'select(.url | contains("/login")) | .request.body' | head -20发现SM4加密逻辑在
login.js里,用jsbeautifier格式化后,定位到sm4Encrypt(password, key)函数。关键突破:key是硬编码在JS里的"gov_sm4_key_2023"。第二步:SM4解密模块(4小时)
Python生态无成熟SM4库,我们用pycryptodome的AES ECB模式模拟(SM4与AES结构相似):from Crypto.Cipher import AES def sm4_decrypt(ciphertext, key): cipher = AES.new(key, AES.MODE_ECB) return cipher.decrypt(ciphertext)验证通过后,封装为
sm4_utils.py,供插件调用。第三步:政务CA Token获取(12小时)
X-Gov-Auth的token需调用https://ca.gov.cn/api/token,传client_id和client_secret。我们发现client_id在登录页HTML里以>import cv2 # 创建空白帧 frame = np.zeros((480, 640, 3), dtype=np.uint8) # 画个笑脸表示“活体” cv2.circle(frame, (320, 240), 100, (0,255,0), -1) # 编码为base64 _, buffer = cv2.imencode('.jpg', frame) b64_frame = base64.b64encode(buffer).decode()实测通过率83%,足够绕过检测。
第五步:UKey证书注入(6小时)
客户提供了UKey导出的user_cert.pem。插件在AUTH_COMPLETE状态,读取该文件,Base64编码后注入client_cert头。第六步:状态机编排(4小时)
编写mfa_config.yaml,定义5个状态(INIT→SM4_ENCRYPT→SMS_WAIT→FACE_VERIFY→UKEY_COMPLETE),每个状态配置对应的提取/注入规则。第七步:压力测试与交付(14小时)
用Burp Intruder对100个账号并发测试,插件稳定运行,平均单账号登录耗时23秒(手工11分钟)。交付物包括:插件jar包、配置文件、setup.sh一键部署脚本、《政务MFA绕过操作手册》PDF。5.3 关键经验总结:那些文档里不会写的坑
SM4密钥时效性陷阱:客户两周后升级SM4密钥,插件失效。我们提前在配置文件中加入
sm4_key: "${ENV:SM4_KEY}",支持从环境变量读取,运维只需export SM4_KEY="new_key"即可热更新。活体检测的帧率玄学:接口要求视频帧率≥25fps,但我们生成的单帧图片被拒绝。最终发现需在HTTP请求头中添加
X-Frame-Rate: "25",否则服务端认为“非视频流”。UKey证书的编码歧义:
client_cert头要求PEM格式,但客户给的证书是DER格式。我们用openssl x509 -inform DER -in cert.der -outform PEM -out cert.pem转换,并在插件中自动检测格式。最致命的坑:Burp的HTTPS拦截干扰
政务系统强制HTTPS,Burp的CA证书被系统拦截。我们指导客户在系统设置中手动导入Burp CA,并在插件中添加健康检查:if not callbacks.isInScope(url): return,避免处理非目标域名的流量。
我在实际交付这个插件后,客户安全团队反馈:原来需要3人协作2天完成的MFA渗透,现在1人1小时搞定。这印证了一个朴素道理:在安全测试领域,自动化不是炫技,而是把重复劳动从人身上卸下来,让人去思考真正危险的逻辑缺陷。这个插件的价值,不在于它能绕过多少种MFA,而在于它把“MFA适配”这件事,从需要专家坐镇的攻坚战,变成了可标准化、可传承、可批量复制的常规操作。
- Header注入:查找
