
1. 这不是“又一个加解密接口”而是BurpSuite工作流的断点重铸你有没有在做API安全测试时反复遇到这种场景目标接口对请求体做了AES加密且每次请求还带一个动态生成的签名字段你用Burp Suite抓到包想手动改参重放结果一发就401——不是密钥错了是签名校验失败你翻文档、查JS源码、扣CryptoJS逻辑花40分钟还原出前端的AES-CBCPKCS7Base64流程刚写完Python脚本后端又悄悄把IV改成时间戳哈希签名算法从HMAC-SHA256切到了AES-ECB加密后再取前8字节……最后你放弃重放退回到“只看不改”的被动审计模式。这就是为什么我决定把AES加解密和签名逻辑直接塞进Burp Suite的工作流里——不是靠外部脚本临时调用而是用Flask搭一个轻量、可嵌入、低侵入的本地签名服务让Burp的Repeater、Intruder甚至Scanner都能通过简单HTTP请求实时获取合法签名与密文。关键词很明确Python、Flask、BurpSuite、AES签名接口。它不替代Burp原生功能而是补上它最缺的一环对强绑定型加密协议的实时协同能力。适合三类人做移动App渗透的测试工程师尤其面对自研加密SDK的金融/政务类App、需要高频构造加密请求的红队队员、以及正在搭建标准化API安全测试流水线的SDL工程师。它解决的不是“能不能加解密”而是“能不能像改Cookie一样自然地改密文参数”。这个接口不是玩具。我在某省级医保平台的渗透中用它把单次重放耗时从22分钟压到3.7秒在一次银行App的越权测试中靠它在Intruder里跑出237个有效加密Payload而手工构造连10个都没凑齐。它的核心价值不在代码多炫酷而在把原本需要跨工具、跨上下文、跨人脑缓存的加密操作压缩成Burp里一个HTTP请求的距离。下面我就从设计动机、协议逆向要点、Flask服务实现、Burp集成实操到真实环境踩坑这五个维度把整套方案掰开揉碎讲清楚——包括那些文档里绝不会写的细节比如为什么必须用AES-CBC而非GCM为什么签名字段要单独传输而非拼在密文里以及Burp发送请求时如何避免因HTTP头顺序引发的签名不一致。2. 协议逆向不是抠JS而是重建密钥生命周期与上下文依赖很多同学一上来就去扣前端JS里的CryptoJS.AES.encrypt以为还原出encrypt方法参数就万事大吉。我试过——在三个不同项目里都栽了跟头。第一次JS里明文写着key: 1234567890123456但实际请求永远失败第二次发现key是localStorage里某个token的MD5前16位而token每小时刷新一次第三次最狠key本身是RSA公钥加密后的密文解密私钥藏在Android SO里……所以真正的协议逆向第一步不是读代码而是定位密钥的生成时机、存储位置、更新条件和作用域边界。2.1 密钥来源的四层定位法我把密钥来源拆成四个物理层级按排查优先级排序层级典型位置检测方式Burp中对应操作L1硬编码常量JS文件中的字符串字面量、HTML>var key CryptoJS.enc.Utf8.parse(1234567890123456); var iv CryptoJS.enc.Utf8.parse(1234567890123456); var encrypted CryptoJS.AES.encrypt(hello, key, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7});注意这里的iv是固定字符串不是随机生成。这意味着服务端校验时IV也是确定的、可预测的。而GCM模式要求IVnonce绝对唯一否则直接导致密钥泄露。当Burp重放请求时如果用GCM每次请求的IV必须不同但服务端怎么知道你这次用了哪个IV它得在请求里显式传而多数协议根本没预留IV字段。更致命的是兼容性。CryptoJS的GCM实现与OpenSSL的GCM在AADAdditional Authenticated Data处理上存在差异我实测过12个不同版本的CryptoJS有7个在GCM模式下与Python的cryptography库结果不一致。而CBCPKCS7是工业级标准Python的pycryptodome、Java的BouncyCastle、Node.js的crypto模块全都能100%对齐。注意不要被“GCM更安全”误导。在API签名场景中安全性不取决于模式本身而取决于密钥管理强度。一个被硬编码在JS里的128位AES-GCM密钥比一个动态派生的256位AES-CBC密钥更脆弱。我们选CBC是因为它在现实协议中可预测、可复现、可调试。2.3 签名字段的设计哲学分离密文与签名而非拼接常见错误做法把AES密文Base64后再用HMAC-SHA256算签名最后拼成{ data: xxx, sign: yyy }。问题在于——当你在Burp里修改data字段时sign字段已失效但你没法实时重算。而我们的方案强制要求签名计算必须覆盖原始明文且签名字段独立于加密字段传输。正确结构应为{ encrypted_data: U2FsdGVkX1..., timestamp: 1717023456, nonce: a1b2c3d4e5f6, signature: sha256(明文_json timestamp nonce secret_key) }看到区别了吗signature的输入是原始明文未加密前的JSON字符串不是密文。这样当你在Burp中修改任意明文参数时只需把新明文、当前时间戳、随机nonce一起发给Flask接口它就能返回新的encrypted_data和signature——两个字段同步更新零延迟。为什么这么做因为服务端校验逻辑必然是先解密encrypted_data得到明文再用相同规则计算signature最后比对。如果签名基于密文计算服务端就得先解密再签名再比对多一层解密失败风险且无法做前置签名校验比如WAF直接拦截非法签名。3. Flask服务实现轻量、无状态、抗并发的签名引擎这个Flask服务不是Web应用而是一个协议转换中间件。它不存session不连数据库不读配置文件——所有密钥、IV、算法参数都通过HTTP请求传入。这样设计是为了适配Burp的无状态重放场景你不可能要求测试人员每次重放前先登录Flask后台设置密钥。3.1 核心路由设计与参数契约服务只暴露两个端点全部走POSTBody为JSONPOST /aes/encrypt输入明文输出密文签名POST /aes/decrypt输入密文输出明文仅用于调试生产环境可关闭每个请求必须携带以下字段缺失则400plaintext: string待加密的原始JSON字符串如{user_id:123,amount:100}key: string16/24/32字节的AES密钥Base64编码避免特殊字符iv: string16字节IVBase64编码mode: stringcbc或ecbECB仅用于极简场景不推荐padding: stringpkcs7唯一支持signature_key: string用于HMAC签名的密钥Base64timestamp: int当前Unix时间戳服务端会校验±300秒nonce: string客户端生成的随机字符串防重放注意所有二进制参数key/iv/signature_key必须Base64编码。这是为了规避HTTP传输中URL编码、空格截断、不可见字符等问题。我吃过亏——某次用明文key1234567890123456在Burp中复制粘贴时末尾多了个换行符导致解密失败排查3小时才发现是编辑器自动加的LF。3.2 加密核心逻辑CBC模式下的IV安全传递这是最容易出错的部分。很多人以为IV可以固定但严格来说IV必须随机且不可预测。然而在Burp重放场景中“随机”意味着每次请求IV都不同服务端怎么解密答案是IV不参与密钥派生而是随密文一起传输并在加密时显式指定。我们的实现强制要求客户端传入iv服务端不做任何修改直接用于AES.new()。这样保证了加解密一致性。关键代码如下使用pycryptodomefrom Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 import hmac import hashlib def aes_encrypt(plaintext: str, key_b64: str, iv_b64: str) - str: key base64.b64decode(key_b64) iv base64.b64decode(iv_b64) # 验证长度AES-128要求key16, iv16 if len(key) not in (16, 24, 32): raise ValueError(fInvalid key length: {len(key)}) if len(iv) ! 16: raise ValueError(fInvalid IV length: {len(iv)}) cipher AES.new(key, AES.MODE_CBC, iv) padded pad(plaintext.encode(utf-8), AES.block_size, stylepkcs7) ciphertext cipher.encrypt(padded) return base64.b64encode(ciphertext).decode(utf-8) def generate_signature(plaintext: str, timestamp: int, nonce: str, signature_key_b64: str) - str: key base64.b64decode(signature_key_b64) msg f{plaintext}{timestamp}{nonce}.encode(utf-8) sig hmac.new(key, msg, hashlib.sha256).digest() return base64.b64encode(sig).decode(utf-8)看到没iv是直接base64.b64decode()后传给AES.new()的没有二次哈希没有截断。这就是为什么客户端Burp必须能生成并传递IV——我们在Burp Extender里用Java写了个小工具每次请求前调用SecureRandom.getInstance(SHA1PRNG)生成16字节随机数再Base64编码填入iv字段。3.3 抗并发与性能为什么不用Redis存IV而用客户端传递有同学建议“把每次生成的IV存Redis设置5分钟过期服务端解密时查Redis”。这看似合理但在Burp场景中是灾难。原因有三网络延迟放大Burp发请求到FlaskFlask再连Redis单次请求增加50~200ms延迟。而Burp Intruder跑1000个payload就是1000次Redis往返总耗时从3秒飙到30秒状态耦合Flask服务重启Redis里IV全丢所有正在重放的请求立即失败扩展性差红队多人共用一台Flask服务Redis key冲突概率陡增。我们的方案是无状态设计IV由Burp侧生成并透传服务端只做纯计算。实测单核CPU下该Flask服务QPS稳定在1200加密签名全流程完全满足Burp重放需求。你甚至可以把Flask进程跑在树莓派上用USB网卡直连测试机延迟低于0.3ms。实操心得在Burp Intruder中如果Payload数量超500建议把iv字段设为Null Payloads即不变化而用plaintext字段做变量。因为IV只需保证单次请求内一致没必要每个payload都换IV。这样既安全单次请求IV唯一又高效减少随机数生成开销。4. Burp集成实战从Extender插件到Intruder自动化Flask服务搭好只是第一步真正价值在于无缝嵌入Burp工作流。这里不讲基础配置只聚焦三个高阶场景Repeater一键加签、Intruder批量加密、Scanner动态签名。4.1 Repeater增强用Extender写Java插件实现自动加签Burp原生Repeater不支持动态计算必须用Extender写插件。我用Java写了不到200行代码核心逻辑是监听Repeater的processHttpMessage事件在请求发送前提取plaintext参数我们约定放在HTTP Body的data字段调用本地Flask接口把返回的encrypted_data和signature写回请求。关键代码片段public class BurpExtender implements IBurpExtender, IHttpListener { private static final String FLASK_URL http://127.0.0.1:5000/aes/encrypt; Override public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { if (toolFlag IBurpExtenderCallbacks.TOOL_REPEATER messageIsRequest) { IRequestInfo reqInfo callbacks.getHelpers().analyzeRequest(messageInfo.getRequest()); String bodyStr getRequestBody(messageInfo); // 解析原始明文假设Body是JSON且含data字段 JSONObject jsonBody new JSONObject(bodyStr); String plaintext jsonBody.optString(data, ); if (!plaintext.isEmpty()) { try { // 调用Flask接口 String response callFlaskEncrypt(plaintext); JSONObject flaskResp new JSONObject(response); // 注入加密后字段 jsonBody.put(encrypted_data, flaskResp.getString(encrypted_data)); jsonBody.put(signature, flaskResp.getString(signature)); jsonBody.remove(data); // 移除原始明文 byte[] newBody jsonBody.toString().getBytes(StandardCharsets.UTF_8); IExtensionHelpers helpers callbacks.getHelpers(); byte[] newRequest helpers.buildHttpMessage( reqInfo.getHeaders(), newBody); messageInfo.setRequest(newRequest); } catch (Exception e) { stdout.println(Encrypt failed: e.getMessage()); } } } } }编译成JAR后Burp → Extender → Add → Java → 选择JAR。从此你在Repeater里改完data字段点“Send”瞬间完成加签无需切窗口、无需复制粘贴。4.2 Intruder批量加密用Snippets注入动态IV与时间戳Intruder的难点在于每个Payload都要生成唯一的iv和timestamp。Burp原生Payload类型不支持动态计算必须用Snippets代码片段。步骤在Intruder的Payload Options中选择Custom iterator添加两列plaintext你的原始参数列表和iv留空点击Add→Payload Processing→Add item→Invoke a Snippet输入Java代码import java.security.SecureRandom; import java.util.Base64; SecureRandom sr new SecureRandom(); byte[] ivBytes new byte[16]; sr.nextBytes(ivBytes); return Base64.getEncoder().encodeToString(ivBytes);同样为timestamp添加Snippetreturn String.valueOf(System.currentTimeMillis() / 1000);这样每个Payload都会动态生成IV和时间戳再通过前面的Extender插件统一调用Flask加密。实测1000个Payload全程自动耗时12.3秒。4.3 Scanner动态签名绕过“未授权访问”拦截的终极方案Scanner扫API时常因缺少合法签名被WAF拦截返回401或跳转登录页导致路径发现失败。解决方案是让Scanner的每个请求都携带动态签名。但这有陷阱Scanner会并发发送请求而签名依赖时间戳。如果10个请求在同一秒发出timestamp相同nonce若没随机化会导致签名重复被服务端拒绝。我们的解法是在Extender插件中对Scanner请求做特殊处理——nonce字段用System.nanoTime()生成纳秒级唯一timestamp用System.currentTimeMillis()/1000并确保signature_key从Burp的Session Handling Rules中读取比如从登录响应中提取的token。关键经验Scanner的并发请求数不要设太高。实测发现当并发20时Flask服务CPU飙升部分请求超时。建议设为10配合Delay选项每个请求间隔100ms稳定性最佳。5. 真实环境踩坑实录从医保平台到银行App的5个血泪教训理论再完美不如一线踩坑来得深刻。以下是我在3个真实项目中总结的硬核避坑指南全是文档里找不到的细节。5.1 坑一CryptoJS的UTF-8编码陷阱——中文参数永远解密失败某省级医保平台请求体含中文{name:张三,id:123}。我在Flask里用plaintext.encode(utf-8)加密服务端却报“Padding is incorrect”。抓包对比发现CryptoJS的enc.Utf8.parse()对中文处理是先UTF-8编码再按字节切分而Python的str.encode(utf-8)没问题。问题出在——CryptoJS的parse()会把字符串末尾的\x00空字符也当作有效字节而Python的pad()填充是标准PKCS7。解决方案在Flask加密前对plaintext做预处理# CryptoJS兼容模式 def cryptojs_utf8_encode(s: str) - bytes: # CryptoJS的Utf8.parse等价于先UTF-8编码再移除BOM如果有 b s.encode(utf-8) if b.startswith(b\xef\xbb\xbf): b b[3:] return b然后用这个bytes对象做pad和encrypt。实测后中文参数100%通过。5.2 坑二IV的Base64编码在Windows与Linux下的换行符差异开发时在Mac上一切正常部署到测试服务器CentOS后所有加密请求失败。tcpdump抓包发现Mac生成的Base64 IV是MTIzNDU2Nzg5MDEyMzQ1Ng无换行而CentOS的base64命令默认每76字符加\n变成MTIzNDU2Nzg5MDEyMzQ1 Ng服务端base64.b64decode()遇到换行直接抛异常。解决方法在Flask中强制用Python的base64.b64encode()并加.replace(b\n, b)同时在Burp Extender的Snippet里用Java的Base64.getEncoder().encodeToString(bytes)它不加换行。5.3 坑三Burp的HTTP/2请求头大小写导致签名不一致某银行App升级HTTP/2后签名突然失效。对比发现HTTP/2规范要求Header名称小写而我们的签名计算包含Content-Type字段。Flask服务收到的Header是content-type: application/json但签名逻辑里写的是Content-Type导致拼接字符串不一致。修复方案在签名计算前统一将所有Header名转为小写headers_lower {k.lower(): v for k, v in request.headers.items()} # 然后用 headers_lower[content-type] 参与签名并在Burp Extender中对HTTP/2请求做同样处理。5.4 坑四Flask的JSON解析自动转义引号破坏原始JSON结构当plaintext是{msg:he\llo}含转义引号时Flask的request.get_json()会把它变成{msg:he\\llo}双反斜杠导致签名原文与客户端不一致。解决方案禁用Flask的JSON自动解析改用原始Bodyapp.route(/aes/encrypt, methods[POST]) def encrypt(): raw_body request.get_data(as_textTrue) # 手动解析JSON保留原始转义 import json data json.loads(raw_body, strictFalse) plaintext data[plaintext]strictFalse允许解析非标准JSON如单引号、尾逗号get_data(as_textTrue)避免字节解码错误。5.5 坑五Burp的Auto Cookie Handler与动态签名冲突开启Burp的Project options → Sessions → Session Handling Rules → Auto Cookie Handler后它会自动在每个请求加Cookie。但我们的签名计算必须包含完整请求体如果Cookie是动态的比如JSESSIONIDabc123而签名时没包含它服务端校验就失败。终极解法关闭Auto Cookie Handler改用Macro Session Handling Rule。先建一个Macro抓取登录请求的Set-Cookie提取JSESSIONID再建Rule对所有请求Inject Cookie值为Macro提取的变量。这样Cookie值固定签名计算可复现。我在实际使用中发现这套方案最大的价值不是技术多先进而是把加密测试从“玄学调试”变成了“确定性工程”。以前改一个参数要5分钟现在3秒以前不敢用Intruder怕触发风控现在敢跑5000个Payload以前看到AES就绕着走现在看到就笑——因为知道只要拿到密钥和协议规则剩下的全是体力活。最后分享一个小技巧把Flask服务打包成Docker镜像用docker run -p 5000:5000 -d aes-signer一键启动测试机上curl -X POST http://localhost:5000/aes/encrypt -d {plaintext:{\\\id\\\:1},key:...}3秒内拿到结果。这才是生产力。