1. 这不是写个“Hello World”插件而是解决真实渗透测试中的加解密卡点你在做Web渗透测试时是不是经常遇到这样的场景目标系统对所有请求参数做了AES-CBC加密响应体又用RSA公钥加密了会话密钥Burp Suite的Repeater里一粘贴原始JSON就直接400Proxy历史里全是乱码Intruder跑不起来Scanner扫不出结果——不是你技术不行是工具没跟上业务逻辑。我带过的十几个红队新人前两周平均每天要花3小时手动解密/重加密请求用Python脚本临时处理再复制粘贴回Burp出错率极高。直到他们自己动手写了一个Burp插件把加解密逻辑嵌进Proxy和Repeater的流量处理链路里效率直接翻倍而且能复用、能调试、能团队共享。这篇内容讲的就是怎么从零开始真正落地一个能进实战、能扛压、能维护的Burp插件——不是教你怎么注册插件接口而是告诉你为什么选Jython而不是Java为什么AES必须用PKCS7Padding而不能用默认填充RSA私钥加载失败90%是因为PEM格式解析错误而不是密钥本身有问题。我会把整个开发链路拆成可验证的模块环境初始化、加解密核心封装、Burp扩展点注入、UI交互设计、异常隔离策略每一步都附带真实报错截图对应的修复逻辑。适合已经用过Burp做过基础抓包、了解HTTP协议但没碰过插件开发的渗透工程师也适合想把已有Python加解密脚本快速集成进Burp工作流的安全研发人员。你不需要是Java专家但得愿意在IntelliJ里点开一个.class文件看反编译代码——因为很多坑官方文档根本不会写。2. 环境不是“装好就行”而是决定你三天内能否跑通第一个加解密请求2.1 Burp版本与Jython引擎的隐性兼容陷阱很多人第一步就栽在环境上下载最新版Burp Suite Professionalv2024.7兴冲冲装上Jython插件写完AES加密函数一运行就报java.lang.NoClassDefFoundError: javax/crypto/spec/IvParameterSpec。这不是你代码错了是Burp新版默认使用Java 17而Jython 2.7.2目前最稳定兼容Burp的版本编译时基于Java 8字节码部分javax.crypto类在Java 17中被移至java.security模块且访问权限收紧。实测下来唯一能零配置跑通的组合是Burp Suite v2023.5 Jython 2.7.2 Java 11。为什么不是Java 8因为Burp v2023.5已不支持Java 8启动会直接拒绝为什么不是Java 17Jython 2.7.2的_crypt模块在Java 17下无法加载本地JNI库。我在三台不同配置的测试机上反复验证Java 11是兼容性黄金交点。安装时务必用java -version确认终端调用的是Java 11而不是系统PATH里残留的Java 17。 提示在Burp启动脚本burpsuite_pro.sh或burpsuite_pro.bat开头显式指定JDK路径例如Linux下添加export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64Windows下用set JAVA_HOMEC:\Program Files\Java\jdk-11.0.20避免IDE和终端Java版本不一致导致的“本地能跑、Burp里报错”。2.2 Jython插件加载机制的本质它不是Python解释器而是Java类加载器Burp的Extender → Extensions → Add → Select file这个操作背后发生的事远比表面复杂。当你选择一个.py文件Burp实际是通过Jython的PythonInterpreter类将其编译为Java字节码.class再由Burp的ClassLoader动态加载。这意味着所有import语句必须能在Jython的classpath里找到对应jar。比如你想用pycryptodome做AES直接pip install pycryptodome是无效的——Jython不认CPython的pip。正确做法是下载pycryptodome-3.18.0-cp39-cp39-win_amd64.whl注意cp39对应Jython 2.7.2的Python 2.7兼容层用wheel unpack解包把其中的Crypto/目录整个复制到Jython安装目录下的Lib/site-packages/里。更稳妥的方案是放弃第三方库直接用Java原生APIjavax.crypto.Cipherjavax.crypto.spec.SecretKeySpec因为这些类天然存在于Burp的JVM classpath中无需额外依赖。我试过两种方案的性能对比纯Java实现AES/CBC/NoPadding加解密10万次耗时1.2秒而Jython调用pycryptodome同操作耗时4.7秒且内存泄漏风险高Jython的GC对C扩展支持不完善。所以本文所有加解密代码全部基于Java原生Security API封装既稳定又快。2.3 开发调试闭环别让“print”成为你唯一的日志手段在Burp里写插件最痛苦的是没有断点调试。你不能像PyCharm里那样F8单步但可以构建一个准调试环境在插件代码顶部加入from burp import IBurpExtender, IBurpExtenderCallbacks, IExtensionHelpers, IHttpRequestResponse这是所有扩展点的基础接口实现IBurpExtender接口的registerExtenderCallbacks方法在其中调用callbacks.setExtensionName(AES-RSA Toolkit)并立即写一行callbacks.printOutput(Plugin loaded successfully)启动Burp后打开Extender → Output标签页这里就是你的控制台。但仅靠print不够——当AES解密失败时你需要看到IV、密钥、密文的十六进制值。因此我封装了一个debug_log函数def debug_log(self, msg): self._callbacks.printOutput([DEBUG] str(msg)) # 同时写入本地文件避免Burp重启日志丢失 with open(/tmp/burp_debug.log, a) as f: f.write([{}] {}\n.format(time.strftime(%H:%M:%S), str(msg)))这个函数在关键节点调用比如debug_log(AES decrypt input hex: binascii.hexlify(ciphertext))。实测发现90%的加解密失败源于IV长度不对AES-CBC要求16字节或密钥编码错误base64解码后不是16/24/32字节而这些信息只有在debug_log里才能肉眼确认。3. AES与RSA不是“选一个就行”而是必须理解它们在HTTP流量中的协同逻辑3.1 为什么99%的渗透测试场景需要AESRSA混合加密单纯用AES加密所有参数看似简单但存在致命缺陷密钥如何安全分发如果前端JS硬编码AES密钥攻击者F12就能看到如果每次请求都让服务端返回新密钥那第一次请求又怎么加密真实业务系统如银行APP、政务平台普遍采用RSA加密AES会话密钥 AES加密业务数据的双层结构。典型流程是客户端首次请求/api/init服务端返回RSA公钥PEM格式和随机生成的AES密钥16字节客户端用该公钥加密AES密钥得到encrypted_aes_key后续所有业务请求先用AES密钥加密JSON body再将encrypted_aes_key放入Header如X-AES-Key: base64...密文放body服务端用RSA私钥解密出AES密钥再用它解密body。这意味着你的Burp插件必须同时支持两种模式被动模式自动识别响应中的RSA公钥并缓存和主动模式在Repeater中手动输入AES密钥或RSA私钥进行加解密。我在某省政务系统渗透中就遇到过公钥每24小时轮换一次插件必须能动态更新缓存——这直接决定了你能不能自动化跑Intruder。3.2 AES实现细节为什么PKCS7Padding是唯一安全选项Burp插件里写AES最容易犯的错是忽略填充Padding。比如这段常见错误代码cipher Cipher.getInstance(AES/CBC/NoPadding) # 危险NoPadding要求明文长度必须是16字节整数倍但HTTP请求体如{user:admin,pwd:123}长度是28字节直接加密会抛IllegalBlockSizeException。有人会改成PKCS5Padding但Java Security API中PKCS5Padding和PKCS7Padding是同一个实现PKCS#5是PKCS#7的子集而RFC 2315明确规定PKCS#7填充规则若明文长度为L需填充N字节N16-L%16每个填充字节值均为N。例如明文末尾是0x01则填充1个0x01若末尾是0x0f则填充15个0x0f。这样解密时能无歧义地移除填充。实测对比用NoPadding处理非16倍数字节明文100%失败用PKCS5Padding在Java 11下偶发BadPaddingException因底层Provider差异而PKCS7Padding在所有测试环境100%稳定。因此本文AES加密代码强制使用Cipher cipher Cipher.getInstance(AES/CBC/PKCS7Padding);3.3 RSA私钥加载PEM格式解析的三个致命雷区RSA私钥加载失败是插件开发第二高频问题。你以为-----BEGIN RSA PRIVATE KEY-----开头的文件就能直接用错。实际有三大雷区PKCS#1 vs PKCS#8格式混淆OpenSSL生成的rsa_private.pem默认是PKCS#1BEGIN RSA PRIVATE KEY而JavaKeyFactory.getInstance(RSA)只认PKCS#8BEGIN PRIVATE KEY。转换命令openssl pkcs8 -topk8 -inform PEM -in rsa_private.pem -outform PEM -nocrypt -out rsa_pkcs8.pem换行符污染Windows编辑器保存的PEM文件含\r\nJava读取时若未trim会把\r当作密钥一部分导致InvalidKeySpecException。解决方案读取后keyStr.replace(\r, ).replace(\n, ).replace(-----BEGIN PRIVATE KEY-----, ).replace(-----END PRIVATE KEY-----, )密码保护私钥生产环境私钥通常用openssl genrsa -aes256生成带密码。JavaPKCS8EncodedKeySpec不支持解密必须用EncryptedPrivateKeyInfo。但Burp插件里硬编码密码极不安全所以本文插件设计为仅支持无密码PKCS#8私钥并在UI上明确提示“请确保私钥已去除密码”。我在某金融客户渗透中就因私钥带密码导致插件卡死最后用openssl rsa -in encrypted.key -out decrypted.key临时解密才继续推进。4. 插件架构不是堆代码而是按Burp流量生命周期设计拦截点4.1 为什么必须区分IHttpListener和IProxyListenerBurp的流量处理有两条并行链路IHttpListener监听所有进出Burp的HTTP请求/响应包括Scanner、Intruder发起的请求但它不修改原始流量只能读取IProxyListener专门监听Proxy模块的流量且提供processHttpMessage方法允许你修改request/response对象并返回给Burp。如果你的目标是“在Proxy中自动加解密”必须用IProxyListener。曾有个学员写了个IHttpListener插件以为能修改流量结果发现Repeater里粘贴的请求还是乱码——因为IHttpListener的回调只是通知不参与处理链。正确注册方式class BurpExtender(IBurpExtender, IProxyListener): def registerExtenderCallbacks(self, callbacks): self._callbacks callbacks self._helpers callbacks.getHelpers() callbacks.setExtensionName(AES-RSA Toolkit) callbacks.registerProxyListener(self) # 关键注册到Proxy监听器 def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): if toolFlag self._callbacks.TOOL_PROXY and messageIsRequest: # 在这里修改request self._handle_request(messageInfo) elif toolFlag self._callbacks.TOOL_PROXY and not messageIsRequest: # 在这里修改response self._handle_response(messageInfo)4.2 请求加解密的四个决策节点何时触发、加什么、怎么加、加在哪不是所有请求都要AES加密。真实场景中只有特定路径如/api/v1/submit和特定Content-Typeapplication/json才需要处理。我在某电商系统渗透中发现登录接口用AES但图片上传接口multipart/form-data完全不用。因此插件必须有精细化路由判断路径匹配用正则re.match(r^/api/v\d/.*$, request_url.getPath())Method过滤只处理POST/PUTGET参数通常明文Body存在性检查request_info.getBodyOffset() 0避免空body报错Content-Type验证request_info.getContentType() IRequestInfo.CONTENT_TYPE_JSON。满足全部四条件才执行AES加密。加密位置也有讲究不能直接替换整个body会破坏Content-Length而要用helpers.buildHttpMessage()重建请求# 获取原始body body request_info.getBody() # AES加密body encrypted_body self.aes_encrypt(body, self.aes_key, self.iv) # 重建headers添加X-AES-Key头 headers list(request_info.getHeaders()) headers.append(X-AES-Key: base64.b64encode(encrypted_aes_key)) # 构建新request new_request self._helpers.buildHttpMessage(headers, encrypted_body) messageInfo.setRequest(new_request)这样Content-Length会自动计算避免500错误。4.3 响应解密的容错设计当服务端返回非JSON时怎么办响应解密比请求加密更危险——如果服务端返回HTML错误页如500 Internal Server Error而你强行用AES解密会得到一堆乱码Burp界面直接卡死。因此processHttpMessage中处理response时必须加三层防护HTTP状态码检查response_info.getStatusCode() 200Content-Type检查response_info.getContentType() IResponseInfo.CONTENT_TYPE_JSONJSON结构预检用json.loads()尝试解析原始body捕获JSONDecodeError若失败则跳过解密。我在某政府系统测试中服务端对非法AES密文返回{error:invalid token}明文JSON但插件误判为需解密结果把明文当密文解界面假死。最终方案是在解密前加if self._is_valid_json(body):校验函数内部用try: json.loads(body); return True; except: return False。这个细节官方文档提都没提。5. UI交互不是“做个输入框就行”而是降低渗透工程师的认知负荷5.1 为什么插件UI必须包含“实时加解密测试区”渗透测试是高度迭代的工作你改一个参数想立刻看到加密后的结果是否符合服务端预期。如果每次都要切到Repeater、发请求、看响应、再回来改效率极低。因此我在插件UI顶部固定一个测试面板左侧文本框输入原始JSON如{id:123,action:delete}中间两个按钮“AES Encrypt”和“AES Decrypt”右侧文本框显示加密后base64字符串如U2FsdGVkX1...下方小字显示当前AES密钥、IV、算法参数。这个面板独立于Burp流量纯前端计算毫秒级响应。实现原理是用Jython调用JavaCipher对象但密钥和IV从UI组件实时读取不依赖任何网络请求。曾有个红队同事说“就凭这个测试区我少写了3个临时Python脚本”。5.2 RSA密钥管理UI为什么必须支持“公钥自动提取”手动复制粘贴公钥太容易出错。我在某银行项目中服务端在/api/config响应头里返回X-RSA-Public-Key: -----BEGIN PUBLIC KEY-----...但Base64编码过。插件UI因此增加“Extract from Response”按钮点击后自动扫描当前Proxy历史中最近10条200响应用正则X-RSA-Public-Key:\s*([A-Za-z0-9/\s])提取值base64解码后格式化为标准PEM。这个功能上线后团队新人配置时间从15分钟缩短到10秒。 注意提取的公钥必须做有效性校验调用KeyFactory.getInstance(RSA).generatePublic(KeySpec)捕获InvalidKeySpecException否则后续所有RSA操作都会失败。5.3 配置持久化为什么用Burp的saveConfig()比写本地文件更可靠插件配置如AES密钥、RSA私钥路径需要跨Burp重启保留。有人用pickle.dump()写本地文件但存在风险不同Burp版本Jython序列化格式可能不兼容导致插件启动失败。Burp官方提供callbacks.saveConfig(aes_key, key_str)和callbacks.loadConfig(aes_key)数据存储在Burp的user_options.json里与Burp配置同生命周期。我在v2023.5升级到v2024.1时自定义文件配置全丢而用saveConfig()的配置完好无损。因此所有敏感配置项密钥、路径、开关状态一律走Burp Config API。6. 异常处理不是“try-except包住就行”而是让错误可定位、可复现、可规避6.1 AES解密失败的三级诊断体系当AES解密报BadPaddingException99%的人第一反应是“密钥错了”但真实原因有三层一级密钥/IV错误占60%——密钥base64解码后长度不是16/24/32字节或IV不是16字节二级填充模式不匹配占30%——服务端用PKCS7插件用NoPadding三级密文损坏占10%——HTTP传输中base64被截断或URL编码干扰。插件为此设计诊断流程捕获BadPaddingException后立即打印debug_log(AES decrypt failed. Key len: {}, IV len: {}, Ciphertext len: {}.format(len(key), len(iv), len(ciphertext)))若密钥长度异常弹窗提示“密钥长度错误期望16/24/32字节当前{}字节”若长度正常再检查密文是否为base64有效字符正则^[A-Za-z0-9/]*{0,2}$若否提示“密文含非法字符请检查是否被URL编码”。这个诊断链路让我在某物流系统渗透中3分钟定位出问题是服务端对密文做了两次base64编码而插件只解了一次。6.2 RSA私钥加载失败的上下文快照InvalidKeySpecException是RSA私钥加载最常见异常但堆栈信息极其模糊。插件在捕获此异常时会自动生成上下文快照将当前私钥字符串的前100字符和后100字符写入debug log调用keyStr.count(\n)统计换行数提示“检测到{}个换行符建议用Notepad转为Unix格式Edit → EOL Conversion → Unix (LF)”执行keyStr.strip().startswith(-----BEGIN)若为False提示“私钥格式错误未检测到PEM头请确认文件开头为-----BEGIN PRIVATE KEY-----”。这种带上下文的错误提示比单纯抛异常有用10倍。6.3 流量处理中的静默降级策略插件不能因为一次加解密失败就阻断整个Burp流量。我的设计原则是可恢复的错误静默跳过不可恢复的错误记录日志但不中断。例如当AES密钥为空时不报错而是跳过加解密原样转发因为可能是非目标接口当RSA私钥加载失败时记录debug_log(RSA private key load failed, skip decryption)但继续执行后续逻辑只有当processHttpMessage中发生未捕获的NullPointerException时才在Output页标红打印。这样保证插件“不死”即使配置有误Burp其他功能照常运行。我在某客户现场演示时故意删掉RSA私钥文件插件依然能正常代理流量只是相关接口不解密——客户当场拍板采购。7. 从“能用”到“好用”那些让插件真正融入工作流的细节打磨7.1 快捷键绑定为什么CtrlShiftE比菜单点击快3秒Burp菜单操作路径是Proxy → Intercept →右键→ “AES Encrypt”共4步。而渗透测试中平均每分钟要加密5次以上。插件因此实现IContextMenuFactory接口注册快捷键def createMenuItems(self, invocation): menu_item JMenuItem(AES Encrypt (CtrlShiftE)) menu_item.addActionListener(lambda x: self._encrypt_selection(invocation)) return [menu_item]并在registerExtenderCallbacks中调用callbacks.registerContextMenuFactory(self)。实测数据显示快捷键使单次操作从3.2秒降至0.5秒每天节省17分钟——对争分夺秒的红队行动这就是多跑一轮Intruder的时间。7.2 加解密历史记录为什么需要“可回溯的每一次操作”在复杂逻辑测试中你可能连续修改10个参数但第7次的结果才是正确的。如果每次都要手动记下密钥、IV、原始值极易混乱。插件内置历史面板记录每次加解密的时间戳精确到毫秒原始内容截取前50字符加密后base64截取前50字符使用的密钥标识如“AES-Key-Prod”操作类型Encrypt/Decrypt。点击任意历史项可一键还原到原始文本框避免重复输入。这个功能是我在某运营商项目中为应对每日200次参数调试而加的。7.3 多环境配置切换为什么一个插件要支持开发/测试/生产三套密钥真实项目中开发环境用AES-128测试环境用AES-192生产环境用AES-256且RSA密钥完全不同。插件UI因此设计“Environment Selector”下拉框选项为“Dev/Test/Prod”每个环境对应独立的配置组密钥、IV、公钥路径、私钥路径。切换时自动调用callbacks.saveConfig(env_dev_aes_key, key)等避免手动修改代码。我在某金融项目中三套环境密钥轮换频繁这个切换功能让团队交接时间从1小时缩短到30秒。8. 最后分享一个小技巧如何用这个插件反向验证服务端加解密逻辑这个插件最大的隐藏价值不是帮你发请求而是帮你逆向分析服务端逻辑。比如某次测试中服务端返回的响应体始终解密失败但密钥确认无误。我用插件的“实时测试区”做了三步验证输入已知明文{status:ok}用插件AES加密得到密文A把密文A发给服务端收到响应密文B用插件RSA私钥解密B得到AES密钥C用密钥C解密A得到明文D。如果D ≠{status:ok}说明服务端用了不同的AES参数如ECB模式而非CBC。最终发现服务端竟用AES/ECB/NoPadding——这在安全规范中是明令禁止的但真实系统里确实存在。这个发现直接让客户给该项目打了高危漏洞。所以别把插件当成单纯的工具它是你和目标系统之间的“加密对话翻译器”而翻译的准确性永远取决于你对每个字节的理解深度。