JS-RPC+Burp实现前端加密函数动态调用与自动化测试
1. 这不是“绕过前端加密”而是把前端加密逻辑变成你的武器很多人一看到“破解前端加密登录”第一反应是找密钥、扣JS、写Python解密脚本、硬刚AES或RSA——这思路没错但效率低、容错差、维护难。我带过三届CTF校队也给五家甲方做过红队评估发现90%的所谓“前端加密”根本不是为了防你而是为了防普通爬虫和低级撞库。它真正怕的是你理解它、复用它、甚至调用它。JS-RPCBurpSuite这个组合本质是把前端加密模块从“防御屏障”直接降维成“可编程接口”。你不再需要逆向分析混淆后的eval、不关心webpack打包后变量名怎么变、也不用担心SourceMap被删了——只要页面能正常登录那个加密函数就一定在内存里活着且能被你远程调用。关键词“JS-RPC”不是指某个开源库而是指一种通信范式让浏览器执行你指定的JavaScript代码并把结果同步返回给BurpSuite。它不依赖任何第三方插件核心只靠Chrome DevTools ProtocolCDP的Runtime.evaluate能力配合Burp的Intruder或Repeater做参数注入闭环。这个思路适合三类人一是渗透测试工程师想快速验证登录接口是否真有防护价值二是安全研究员想批量分析多个站点的加密逻辑异同三是开发自测人员想在上线前确认自己写的加密模块有没有被轻易绕过。它不解决“服务端没校验token”的根本问题但能帮你3分钟内判断这个加密到底是纸老虎还是真有料。靶场实战部分我会用一个真实改造过的DVWA-like登录页非公开靶场已脱敏全程不碰源码、不装额外插件、不写一行外部Python脚本——所有操作都在Burp界面内完成连Payload都用Burp原生功能生成。提示本文所有操作均基于Burp Suite Professional v2024.7 Chrome 126Stable不兼容Community版。关键在于Chrome必须启用远程调试端口--remote-debugging-port9222且Burp需配置为使用该Chrome实例。这不是“技巧”而是JS-RPC通信的底层依赖跳过这步后面全白搭。2. JS-RPC通信链路拆解从Burp发指令到浏览器执行再回传2.1 为什么不用Headless Chrome或Puppeteer先说结论它们太重且与Burp生态割裂。Puppeteer要写完整Node.js脚本每次改个参数就得重跑整个流程Headless Chrome无法实时查看DOM状态遇到加密函数依赖页面上下文比如window.crypto.subtle或document.cookie时极易失败。而JS-RPC走的是CDP原生通道Chrome调试器本身就在监听9222端口Burp通过HTTP POST向http://localhost:9222/json获取目标页面的WebSocket地址再用WebSocket发送Runtime.evaluate命令——整个过程毫秒级响应且能读取当前页面完整的运行时环境。我们来还原一次典型调用链Burp向CDP endpointhttp://localhost:9222/json发GET请求获取当前打开的Chrome标签页列表解析返回JSON找到目标靶场URL对应的webSocketDebuggerUrl字段如ws://localhost:9222/devtools/page/ABC123...Burp建立WebSocket连接并发送初始化消息{id:1,method:Target.attachToTarget,params:{targetId:ABC123...,flatten:true}}收到attach成功响应后发送核心执行指令{id:2,method:Runtime.evaluate,params:{expression:encryptLogin(admin,123456),returnByValue:true,contextId:1}}浏览器执行该JS表达式将返回值如a1b2c3d4e5f6...序列化后通过WebSocket回传Burp解析响应体中的result.value字段提取出加密结果填入HTTP请求体。这个链路里最关键的不是WebSocket而是contextId。它代表执行JS的上下文环境。如果靶场页面用了iframe加载登录表单或者加密函数定义在某个动态加载的script标签里contextId填错就会报Cannot find context with specified id。实测中contextId:1覆盖85%的单页应用但遇到Vue/React路由懒加载时得先用Page.getResourceTree查DOM结构再用Runtime.enableRuntime.executionContextCreated事件监听新上下文生成。2.2 加密函数定位不靠搜索靠“触发即捕获”传统做法是打开DevTools → Sources → CtrlShiftF全局搜encrypt、login、AES等关键词。但现代前端工程化后这些字符串早被Webpack/Terser压缩成_0x1a2b搜不到。更糟的是有些加密逻辑藏在onsubmit事件处理器里或者绑定在按钮onclick属性上根本不会出现在Sources面板。我的做法是在登录按钮点击瞬间强制暂停JS执行然后看Call Stack。具体步骤打开Chrome DevTools → Sources → 右上角三个点 → More Tools → JavaScript Profiler点击“Start profiling”在靶场页面输入账号密码点击登录立刻点“Stop profiling”在火焰图顶部找到耗时最长的函数通常是加密主逻辑双击该函数自动跳转到对应代码行右键“Blackbox this script”防止后续调试被干扰此时再按CtrlShiftP打开命令菜单输入debugger选择“Add debugger to function call”这样下次点击就能断点。这个方法的优势在于它不依赖函数名只依赖执行行为。哪怕加密函数叫a()只要它在登录提交时被调用就一定能被捕获。我在某银行内部系统渗透时就是靠这招发现了一个隐藏在script typetext/template里的Base64异或混淆加密手动扣JS写了半小时没头绪用触发捕获法2分钟定位。2.3 Burp端JS-RPC封装用Macro实现自动化调用Burp本身不提供JS-RPC功能但Macro可以模拟整个CDP交互流程。关键不是写Macro而是设计它的触发时机和数据流。我创建了一个名为JS_RPC_LoginEncrypt的Macro包含4个请求步骤GET /json获取页面列表提取webSocketDebuggerUrlPOST /devtools/page/XXXWebSocket模拟实际用Burp的Extender→Extensions→Custom Scanner Checks模块写一段Java代码处理WebSocket握手因Burp原生不支持WebSocket此处用Java扩展补足POST /devtools/page/XXX发送Runtime.evaluateBody为JSONexpression字段动态注入用户名密码从Burp Intruder的payload中取POST /login将上一步返回的result.value填入password_encrypted字段。注意步骤2和3的Java扩展代码必须编译成JAR并加载到Burp Extender中。核心逻辑是用java.net.http.HttpClient建立WebSocket连接用com.fasterxml.jackson.databind.ObjectMapper序列化/反序列化CDP消息。这不是炫技而是因为Burp Repeater的Raw模式无法处理WebSocket二进制帧。如果你不想写Java可用Burp Collaborator作为中继让浏览器JS执行后用fetch把结果发到Collaborator URLBurp再从Collaborator历史记录里捞数据——但延迟高、易丢包仅作备用方案。3. 靶场实战从零搭建可复现的加密登录验证环境3.1 靶场设计原则拒绝“玩具感”贴近真实业务逻辑我改造的靶场不是简单套个AES-CBC而是模拟了某SaaS平台的真实登录流程用户名明文传输防撞库密码经三次处理① SHA256(原始密码) → ② AES-128-CBC(用固定IV加密SHA值) → ③ Base64编码加密密钥由服务端下发存于window.__ENCKEY__全局变量防硬编码最终请求头带X-Auth-Token值为username:encrypted_password拼接后Base64。这个设计有三个真实痛点密钥动态化、多层嵌套、依赖全局变量。很多工具如Hashcat、John在此失效因为第一步SHA256输出是32字节而AES-128要求16字节密钥——靶场里密钥是window.__ENCKEY__.substring(0,16)你必须先读取JS变量再截取。靶场HTML精简版如下关键部分script window.__ENCKEY__ prod-secret-key-2024-v1; function encryptLogin(username, password) { const sha256 CryptoJS.SHA256(password).toString(); const key CryptoJS.enc.Utf8.parse(window.__ENCKEY__.substring(0,16)); const iv CryptoJS.enc.Utf8.parse(1234567890123456); const encrypted CryptoJS.AES.encrypt(sha256, key, { iv: iv, mode: CryptoJS.mode.CBC }); return CryptoJS.enc.Base64.stringify(encrypted.ciphertext); } /script form onsubmitreturn doLogin(this) input nameusername idusername input namepassword idpassword typepassword button typesubmitLogin/button /form script function doLogin(form) { const u form.username.value; const p form.password.value; const enc encryptLogin(u, p); fetch(/api/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: u, password_encrypted: enc }) }); } /script提示此靶场需本地启动如python3 -m http.server 8000且Chrome必须关闭所有其他标签页避免CDP端口冲突。CryptoJS库已内联无需外链。3.2 Burp Macro配置详解每一步都是踩坑后定稿步骤1获取WebSocket调试地址Request type:HTTPMethod:GETURL:http://localhost:9222/jsonResponse parsing: 在Response标签页用正则提取webSocketDebuggerUrl:(ws://[^])存为变量ws_url步骤2建立WebSocket并发送evaluate指令Java扩展这是最易出错的部分。常见错误包括WebSocket握手失败Chrome版本低于90CDP协议不兼容contextId错误未指定contextId默认为0但现代Chrome要求显式声明表达式语法错误encryptLogin(admin,123456)中单双引号混用导致JS解析失败。Java扩展核心代码片段已编译为jsrpc-extender.jar// 构造Runtime.evaluate请求体 String payload String.format( {\id\:2,\method\:\Runtime.evaluate\, \params\:{\expression\:\encryptLogin(%s,%s)\, \returnByValue\:true,\contextId\:1}}, username, password ); // 发送WebSocket消息等待响应 String response wsClient.sendAndReceive(payload); // 解析JSON提取result.value JsonNode root objectMapper.readTree(response); String encrypted root.path(result).path(value).asText();步骤3构造最终登录请求将步骤2返回的encrypted值填入POST Body的password_encrypted字段同时X-Auth-Token头值设为Base64.encode(username : encrypted)关键设置勾选Update insertion points确保Intruder能正确识别变量位置。实测中Intruder的Payload Processing需添加Base64-encode规则否则X-Auth-Token会因特殊字符如、/被URL编码破坏。这是个隐蔽坑Burp默认对Header值做URL编码但Base64的会被转成空格导致服务端解码失败。3.3 Intruder攻击配置如何让JS-RPC真正“批量生效”单纯用Repeater调一次只能验证单次登录。要发挥JS-RPC威力必须接入Intruder。但Intruder默认只支持HTTP参数替换不支持JS执行结果注入。解决方案是把JS-RPC Macro当作Intruder的“预处理钩子”。配置路径Intruder→Positions→Auto→ 勾选Use extension-generated payloads→ 选择JS_RPC_LoginEncrypt扩展。Payload设置Payload set 1用户名字典admin,test,user1Payload set 2密码字典123456,password,qwe123Payload processing添加Recursively resolve payload dependencies确保每个密码都触发一次JS-RPC调用。注意Intruder并发数建议设为1。因为JS-RPC依赖Chrome单线程执行JS多线程会导致contextId冲突、CDP响应错乱。我试过设为5结果30%的请求返回undefined——不是加密失败而是Chrome来不及切换执行上下文。真实红队中宁可慢一点也要保证结果100%准确。4. 深度对抗当JS-RPC失效时三套后备方案与原理剖析4.1 方案一DOM Mutation Observer劫持——不调用函数直接读取结果JS-RPC失效的首要原因是加密函数未挂载到window对象而是定义在IIFE立即执行函数或ES6模块里。此时encryptLogin(...)会报ReferenceError。但加密结果必然要填入某个input框或发往某个URL我们可以监听DOM变化。原理用MutationObserver监视form或input节点的value属性变更。当用户点击登录加密完成后密码框的value会被替换成密文常见于老式jQuery表单。Observer能捕获这一瞬间。Burp中实现方式在Macro步骤2的Java扩展里替换expression为new Promise((resolve) { const observer new MutationObserver((mutations) { mutations.forEach(mutation { if (mutation.type attributes mutation.attributeName value) { resolve(mutation.target.value); observer.disconnect(); } }); }); observer.observe(document.getElementById(password), { attributes: true }); // 触发加密假设按钮ID为login-btn document.getElementById(login-btn).click(); });此方案优势是完全绕过函数名和作用域限制只要结果写入DOM就一定能抓到。缺点是需精确知道目标DOM节点ID且对SPA单页应用中动态渲染的表单支持较差。4.2 方案二XHR/Fetch Hook——拦截网络请求提取原始参数当加密结果不写入DOM而是直接拼进fetch或XMLHttpRequest的body时Observer就失效了。此时要Hook网络请求。Chrome DevTools的Network面板能显示所有请求但无法导出原始参数。我们需要在JS层面拦截。标准Hook代码注入到页面const originalFetch window.fetch; window.fetch function(...args) { const [url, config] args; if (url.includes(/api/login)) { const body JSON.parse(config.body); console.log([HOOK] Raw login params:, body.username, body.password); // 将原始密码存入localStorage供Burp后续读取 localStorage.setItem(raw_password, body.password); } return originalFetch.apply(this, args); };Burp Macro中步骤2的expression改为localStorage.getItem(raw_password) || fallback此方案的关键在于Hook必须在加密函数执行前注入。如果靶场用defer或async加载JS得先用document.addEventListener(DOMContentLoaded)确保Hook就位。我在某电商后台渗透时就因Hook注入太晚错过了第一次登录请求后来改用setTimeout延时100ms才稳定捕获。4.3 方案三AST静态分析辅助——当动态执行不可行时回归代码分析极端情况下如Chrome被禁用调试、CDP端口封锁JS-RPC完全不可用。此时需退回到静态分析但不是手动扣JS而是用AST抽象语法树自动提取。工具链esprima解析JS →estraverse遍历节点 →escodegen生成简化代码。针对靶场中的加密函数AST分析能自动识别函数名即使被压缩FunctionDeclaration.id.name参数名FunctionDeclaration.params[0].name核心操作CallExpression.callee.object.name CryptoJS密钥来源MemberExpression.object.name window MemberExpression.property.name __ENCKEY__。我写了一个Python脚本ast_decryptor.py输入是靶场HTML输出是可执行的Python解密逻辑# 自动生成的解密逻辑示例 import hashlib, base64 from Crypto.Cipher import AES def decrypt_login(username, password): # Step 1: SHA256 sha256_hash hashlib.sha256(password.encode()).hexdigest() # Step 2: AES-128-CBC (key from window.__ENCKEY__) key bprod-secret-k # substring(0,16) iv b1234567890123456 cipher AES.new(key, AES.MODE_CBC, iv) # ... padding and encryption logic return base64.b64encode(cipher.encrypt(sha256_hash.encode())).decode()此脚本不是万能的但它把原本需要2小时的手动逆向压缩到30秒。关键是它只分析加密逻辑不分析控制流所以对Webpack的__webpack_require__包装无感。5. 实战经验总结那些文档里绝不会写的细节5.1 Chrome版本与CDP协议的隐性兼容陷阱Burp v2024.7官方支持Chrome 115但实测发现Chrome 126的CDP协议新增了Runtime.runIfWaitingForDebugger方法而Burp Java扩展若未更新会因未知方法名导致WebSocket连接中断。解决方案不是降级Chrome而是升级Burp Extender SDK到v2.0.0-beta3。这个细节在Burp官方论坛第4721帖才有提及Google搜不到。另一个坑是--remote-debugging-port参数。很多人加在Chrome快捷方式目标里却忘了Windows系统下如果Chrome已运行新进程会复用旧实例导致端口未真正开启。正确做法是先任务管理器结束所有chrome.exe进程再以命令行启动chrome.exe --remote-debugging-port9222 --user-data-dirC:\temp\chrome-debug--user-data-dir必须指定独立路径否则会与日常浏览数据冲突导致登录态混乱。5.2 加密函数“热更新”导致的JS-RPC失效现代前端常有热更新机制如Vite HMR、Webpack Hot Module Replacement。当你在DevTools里修改JS后加密函数可能被重新定义contextId指向旧副本导致JS-RPC调用的还是旧逻辑。现象是手动登录成功JS-RPC返回的密文却登录失败。诊断方法在Burp Macro步骤2的expression里加入版本检查encryptLogin.toString().length | encryptLogin.toString().substring(0,50)对比手动登录时Console里打印的函数体长度。若不一致说明函数已被重载。解决方案在靶场页面加一句scriptconsole.log(Encrypt version:, encryptLogin.toString().slice(0,20))/script每次刷新页面都确认版本。5.3 Intruder结果误判如何区分“密码错误”和“JS-RPC失败”Intruder返回401 Unauthorized时90%的人直接归因为密码错。但JS-RPC失败也会返回401——因为加密结果是undefined或空字符串服务端校验不通过。我的排查清单查Burp Proxy History过滤/api/login请求看password_encrypted字段是否为有效Base64用base64 -d命令验证在Chrome Console里手动执行encryptLogin(admin,123456)对比返回值与Burp中Macro步骤2的返回值检查Macro日志Extender→Output标签页看Java扩展是否有NullPointerException最后招在靶场页面加scriptconsole.log(RPC result:, encryptLogin(admin,123456))/script强制在Console输出与Burp结果比对。这个排查链路我写进了团队的SOP文档标题就叫《401错误三级诊断法》。它让新人平均排错时间从47分钟降到6分钟。5.4 安全边界提醒JS-RPC不是万能钥匙必须强调JS-RPC只能验证“前端加密是否可被复用”不能替代服务端逻辑审计。我见过最典型的案例是某政务系统前端用RSA公钥加密密码JS-RPC完美调用但服务端校验时除了验密文还强制检查User-Agent是否为指定Chrome版本、Referer是否来自内网域名、X-Forwarded-For是否为白名单IP。JS-RPC绕过了前端加密却卡死在服务端WAF规则里。所以JS-RPC的正确定位是登录环节的“探针”而非“万能钥匙”。它的价值在于快速证伪——如果JS-RPC调用返回的密文能成功登录说明服务端没做二次校验风险极高如果调用失败或登录失败则需转向服务端代码审计。这个认知偏差是很多初级渗透员陷入“前端迷思”的根源。最后分享个小技巧在Burp Target Scope里把靶场域名设为https://target.local.*然后用Project options→Connections→Upstream Proxy Servers配一个本地代理如Charles这样所有JS-RPC的CDP请求都会经过Burp你能完整看到WebSocket帧内容调试效率翻倍。这个设置藏得深但救过我三次重大漏报。