前端加密绕过实战:JsRpc+Yakit热加载自动化渗透测试
1. 项目概述当渗透测试遇上前端加密在Web应用安全测试的日常工作中我们最常遇到的“拦路虎”之一就是前端加密与签名机制。你兴致勃勃地打开Burp Suite准备对登录接口进行一波爆破或注入测试结果抓包一看username和password字段变成了一串毫无规律的密文旁边还跟着一个长长的signature签名参数。这时候传统的“改包重放”瞬间失效测试工作仿佛被按下了暂停键。很多新手朋友遇到这种情况要么选择放弃要么硬着头皮去逆向整个JavaScript加密逻辑过程繁琐且容易出错。今天我们就以Yakit自带的靶场为例完整走一遍从手动分析前端加密逻辑到利用JsRpc实现远程调用最后在Yakit的WebFuzzer中通过“热加载”脚本实现自动化加密发包的全过程。这不仅仅是一个靶场通关教程更是一套应对真实世界前端加密场景的通用“组合拳”。你会发现借助合适的工具链绕过前端加密可以变得高效且优雅让你能重新聚焦于核心的业务逻辑漏洞挖掘。2. 环境准备与目标分析2.1 Yakit靶场搭建与目标锁定首先你需要一个实践环境。Yakit内置的靶场功能为我们提供了绝佳的练习场。启动Yakit后在左侧功能栏找到“靶场”模块选择“前端加密与签名绕过”相关的实验场景并启动。靶场会提供一个本地Web应用地址通常类似http://127.0.0.1:8085。用浏览器打开这个地址你会看到一个典型的登录页面。先进行最基础的操作打开浏览器开发者工具F12切换到“网络”(Network)标签页并勾选“保留日志”(Preserve log)。然后在登录框输入任意账号密码比如admin/admin点击登录。此时你的注意力应该立刻聚焦在网络请求列表中出现的那条POST请求上。点击查看其“载荷”(Payload)问题初现端倪请求体不再是明文的usernameadminpasswordadmin而可能呈现为类似dataxxxxxxsignatureyyyyyy的格式。data字段看起来是加密后的密文signature字段则是根据某些规则生成的签名字符串。服务器端会先验证签名是否正确然后再解密data字段进行身份验证。我们的目标很明确在不逆向解密算法、不获取密钥的前提下能够自由地构造任意用户名和密码的加密请求以便进行后续的漏洞测试。2.2 手动逆向加密与签名逻辑自动化之前手动分析是理解原理的必经之路。我们需要找到生成data和signature的JavaScript代码。搜索关键字段在开发者工具的“源代码”(Sources)标签页或“控制台”(Console)标签页使用全局搜索CtrlShiftF搜索关键词如signature、encrypt、data、login、submit等。定位加密函数很快你可能会在某个.js文件中找到核心函数。例如在Yakit靶场中你可能会发现一个名为generateSign()或encryptData()的函数。关键代码可能长这样function encryptData(username, password) { var key CryptoJS.enc.Utf8.parse(一个固定的或动态的密钥); var srcs CryptoJS.enc.Utf8.parse(username | password); var encrypted CryptoJS.AES.encrypt(srcs, key, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}); return encrypted.toString(); } function generateSign(data, timestamp) { var secret your_secret_here; var str data timestamp secret; return CryptoJS.MD5(str).toString(); // 也可能是SHA1、HMAC等 }在控制台验证找到函数后直接在浏览器控制台尝试调用。输入encryptData(test, 123456)和generateSign(encryptedDataString, 1678888888888)观察输出是否与抓包到的data和signature一致。这一步至关重要它确认了我们找到的函数确实是正在使用的函数并且其运行环境如依赖的CryptoJS库、全局变量是完备的。注意有时加密函数被包裹在闭包或模块内部无法在全局控制台直接访问。这时可以尝试在函数定义处打上断点或者更粗暴有效的方法是在加密函数被调用后将其“劫持”并暴露到全局。例如在加密函数所在文件末尾或通过控制台注入window.myEncrypt encryptData;这样就能通过myEncrypt()来调用了。手动分析完成我们已经掌握了“钥匙”。但每次测试都要手动执行JS代码、复制结果、再粘贴到Burp里重放效率极低。接下来我们将引入JsRpc把这把“钥匙”变成可以远程遥控的“智能钥匙”。3. JsRpc原理与实战部署构建远程调用桥梁3.1 为什么是JsRpc传统绕过前端加密的方法要么是纯手动要么是使用Selenium等浏览器自动化工具模拟执行JS。前者太慢后者太重需要启动完整浏览器实例资源占用大。JsRpc提供了一种轻量级、精准的解决方案它通过在目标页面注入一个WebSocket客户端与本地一个WebSocket服务端通信从而允许我们远程调用页面内已有的JavaScript函数。简单来说它的工作流程是我们在本地启动一个JsRpc服务端一个Go语言编写的轻量级程序。将一段连接代码JsEnv_Dev.js注入到目标网页的浏览器控制台。注入的代码会连接到本地服务端并将我们指定的JS函数如encryptData注册为可供远程调用的“动作”(Action)。此后我们就可以通过向本地服务端发送HTTP请求来间接调用浏览器页面里的那个JS函数并获得其返回值。3.2 部署JsRpc服务端从JsRpc的GitHub仓库https://github.com/jxhczhl/JsRpc下载编译好的可执行文件如JsRpc-windows-amd64.exe或自行编译。在命令行中运行它./JsRpc-windows-amd64.exe -port 12080-port参数指定服务端监听的端口。看到输出提示服务已启动在12080端口即可。这个服务端将扮演“中转站”的角色。3.3 注入连接环境并注册函数保持浏览器打开着靶场登录页面并确保已登录以便加密函数处于可用状态。打开开发者工具的控制台(Console)标签页。将JsEnv_Dev.js文件中的全部内容复制粘贴到控制台并按下回车执行。这段代码的核心作用是创建一个WebSocket连接并提供了注册函数的方法。接着我们需要注册我们找到的加密函数。假设我们找到的加密函数叫encryptData签名函数叫generateSign。我们需要在控制台继续执行自定义的注册代码var client new Hlclient(ws://127.0.0.1:12080/ws?grouplogin); client.regAction(encrypt, function (resolve, param) { // param 是服务端传过来的参数我们约定用Base64编码避免特殊字符问题 var rawParams atob(param); // 解码假设我们传递username|password var paramsArr rawParams.split(|); var username paramsArr[0]; var password paramsArr[1]; // 调用页面中真实的加密函数 var encryptedData encryptData(username, password); // 可能需要获取当前时间戳用于签名 var timestamp new Date().getTime(); var signature generateSign(encryptedData, timestamp); // 将结果返回可以组合成一个字符串如“加密数据|签名|时间戳” var result encryptedData | signature | timestamp; resolve(result); });这段代码做了几件事连接到本地的JsRpc服务端并指定分组(group)为login。注册了一个名为encrypt的动作。当远程调用这个动作时会执行这个回调函数。回调函数接收一个param参数我们约定用Base64编码解码后拆出用户名和密码。调用页面内真实的encryptData和generateSign函数进行加密和签名。通过resolve()方法将最终结果返回给调用者。执行后如果看到控制台输出连接成功的提示并且JsRpc服务端也显示有客户端上线说明注入成功。3.4 测试远程调用现在我们可以不依赖浏览器界面直接通过HTTP请求来获取加密签名后的数据了。打开一个新的终端或浏览器标签访问http://127.0.0.1:12080/go?grouploginactionencryptparamYWRtaW58YWRtaW4这里的param值是admin|admin的Base64编码。访问这个URLJsRpc服务端会通过WebSocket将请求转发给浏览器中的注入脚本脚本执行函数后再将结果通过HTTP响应返回给你。你应该会收到一个包含加密数据、签名和时间戳的响应。至此我们已经成功架起了一座从外部工具直达浏览器内部JS执行环境的桥梁。但这还不够方便我们最终希望能在Yakit的WebFuzzer里像使用普通变量一样使用这个加密能力。4. Yakit热加载脚本编写实现自动化FuzzingYakit的“热加载”功能是其WebFuzzer模块的一大杀器。它允许你在每次请求发送前或后动态执行一段Yak脚本一种内嵌的Go风格脚本语言来修改请求或处理响应。我们将利用这个特性在发送登录请求前动态生成加密参数。4.1 编写热加载脚本在Yakit中进入“WebFuzzer”模块在请求编辑框下方找到“热加载”标签页。我们需要编写两个主要的函数一个负责与JsRpc服务端通信的辅助函数用于获取加密结果。signRequest函数这是热加载的入口函数Yakit会自动调用它来处理Payload。以下是完整的脚本示例请根据你的实际JsRpc注册的函数名和返回格式进行调整// 定义与JsRpc服务端通信的函数 func getEncryptedDataFromJsRpc(username, password) { // 1. 构造参数格式需与之前注入的JS代码约定一致 rawParam sprintf(%s|%s, username, password) encodedParam codec.EncodeBase64(rawParam) // 进行Base64编码 // 2. 构造请求JsRpc服务端的HTTP包 target 127.0.0.1:12080 packet GET /go?grouploginactionencrypt¶m{{params(encodedParam)}} HTTP/1.1 Host: {{params(target)}} Connection: close // 3. 发送请求 rsp, req, err poc.HTTP( packet, poc.params({encodedParam: encodedParam, target: target}), poc.timeout(10), // 设置超时 ) // 4. 处理响应 if err ! nil { die(调用JsRpc失败:, err) } body poc.GetHTTPPacketBody(rsp) // 假设JsRpc返回的是JSON: {data: encryptedData|signature|timestamp} jsonObj json.loads(body) if jsonObj jsonObj[data] { resultStr string(jsonObj[data]) // 按约定分割字符串例如“加密数据|签名|时间戳” parts str.Split(resultStr, |) if len(parts) 3 { return parts } } die(从JsRpc获取数据失败或格式错误:, body) } // Yakit热加载标准入口函数参数result是原始Payload signRequest func(result) { // result 可能是 username|password 格式取决于你在fuzzer里如何设置 pairs str.SplitN(result, |, 2) if len(pairs) ! 2 { return result // 如果不是预期格式原样返回 } username pairs[0] password pairs[1] // 调用JsRpc获取加密后的数据、签名和时间戳 encryptedParts getEncryptedDataFromJsRpc(username, password) encryptedData encryptedParts[0] signature encryptedParts[1] timestamp encryptedParts[2] // 构造最终的请求体格式需与目标接口一致 // 例如dataxxxxsignatureyyyy×tampzzzz finalBody sprintf(data%ssignature%s×tamp%s, encryptedData, signature, timestamp) return finalBody }4.2 配置WebFuzzer进行测试构造原始请求在WebFuzzer的请求编辑框中先填入靶场登录接口的URL和方法POST。请求体部分先随意填写一个模板比如dataPLACEHOLDERsignaturePLACEHOLDER×tampPLACEHOLDER。重点是我们需要将data字段的值交给热加载脚本动态生成。设置Payload和热加载在“Payload”标签页添加一个Payload集合。例如你可以设置两个Payload列表一个叫username值为[admin, test, guest]另一个叫password值为[admin, 123456, password]。在“攻击类型”中选择“交叉合并”等方式生成组合。关键步骤回到请求编辑框将请求体修改为data{{yak(signRequest|{{username}}|{{password}})}}signaturePLACEHOLDER×tampPLACEHOLDER注意这里signature和timestamp还是占位符。更优的做法是让signRequest函数返回一个包含所有字段的完整字符串然后在热加载脚本中直接替换整个请求体。我们可以修改signRequest函数让它返回一个JSON或特定分隔符连接的字符串然后在“热加载”设置中选择“替换整个请求体”。启用热加载在“热加载”标签页将上面编写的脚本粘贴进去并确保“启用热加载”复选框被勾选。发起攻击点击“执行”按钮。Yakit会为每一组username和password组合先调用signRequest函数。该函数内部通过JsRpc获取到对应的加密数据、签名和时间戳并构造出完整的请求体然后替换掉原始请求中的占位符最后将正确的加密请求发送给服务器。此时你看到的效果就是你在Payload里配置的是明文的用户名密码列表但实际发送出去的已经是完全符合前端加密签名规则的请求了。你可以像测试普通接口一样进行爆破、撞库、模糊测试等操作。5. 实战进阶与深度避坑指南掌握了基础流程但在真实环境中你会遇到更多挑战。下面分享一些进阶技巧和常见坑点。5.1 处理动态密钥与参数真实场景的加密绝不会像靶场这么简单。常见的变化有动态密钥加密密钥可能由前一个接口响应返回或者根据时间、用户ID等计算得出。签名参数动态化参与签名的timestamp、nonce随机数可能每次请求都必须不同且服务器会校验其有效性。加密参数包含其他字段除了用户名密码加密对象可能还包括设备ID、版本号等固定或动态字段。应对策略修改注入的JS代码你的regAction回调函数需要能获取这些动态值。例如如果密钥来自一个全局变量window.encryptKey你就在回调函数里读取它。如果nonce需要随机生成你就在JS环境里用Math.random()生成。client.regAction(encryptV2, function (resolve, param) { var rawParams atob(param); var paramsArr rawParams.split(|); var username paramsArr[0]; var password paramsArr[1]; // 从页面全局变量获取动态密钥 var dynamicKey window.currentEncryptKey; // 生成随机数 var nonce Math.floor(Math.random() * 1e9).toString(); // 获取当前时间戳 var timestamp new Date().getTime(); // 调用更复杂的加密函数传入所有必要参数 var encryptedData complexEncrypt(username, password, dynamicKey, nonce, timestamp); var signature generateSign(encryptedData, timestamp, nonce); var result encryptedData | signature | timestamp | nonce; resolve(result); });同步修改热加载脚本热加载脚本中的getEncryptedDataFromJsRpc函数需要能处理新的返回格式例如包含四个部分并正确拼装请求体。5.2 JsRpc连接稳定性与浏览器环境保持页面刷新导致连接断开注入的JS在页面刷新后会失效。解决方案是使用浏览器插件如Tampermonkey自动注入脚本或者将关键加密函数持久化。更简单的方法是在测试期间尽量避免刷新页面使用新标签页进行其他操作。浏览器休眠导致JS停止执行笔记本电脑合盖或浏览器页面长时间不活动可能导致浏览器降低页面JS的优先级甚至暂停执行导致JsRpc调用无响应。保持浏览器窗口在前台激活状态可以设置系统和浏览器不自动休眠。多标签页干扰确保JsRpc连接的标签页是目标页面避免注入到其他标签页导致调用错误。5.3 Yakit热加载脚本调试技巧使用die和println调试在脚本中关键位置使用println打印变量值或用die在出错时抛出信息有助于定位问题。这些输出会显示在Yakit的“热加载”标签页下方控制台。先单元测试再集成测试不要一下子就在复杂Payload里用热加载。先单独写一个测试脚本调用getEncryptedDataFromJsRpc函数看是否能正确返回。确认无误后再整合到signRequest中。注意字符串编码与拼接在Yak脚本中拼接HTTP请求体时确保特殊字符如,,被正确编码。使用sprintf或字符串连接时务必小心。有时候返回的加密数据本身包含或直接拼接会破坏请求结构可能需要URL编码。5.4 性能考量与优化JsRpc调用延迟每个加密请求都需要一次浏览器到JsRpc服务端的WebSocket通信和一次HTTP请求这会引入额外延迟。在批量爆破成千上万个密码时可能成为瓶颈。优化建议批量加密可以修改注入的JS代码和热加载脚本支持一次传入多组用户名密码返回多组加密结果减少通信次数。本地模拟对于固定算法、密钥静态的场景一旦通过JsRpc分析清楚算法可以直接用Yak语言或Python、Go重写加密函数彻底脱离浏览器环境速度最快。但这要求算法可完全还原。连接池与并发控制保持JsRpc连接的稳定在Yakit中适当控制并发线程数避免过高并发压垮浏览器或导致调用超时。6. 总结从手动到自动的思维跃迁回顾整个流程我们从最笨拙但必须的手动分析开始厘清了前端加密签名的逻辑。然后引入JsRpc巧妙地利用浏览器本身作为“加密黑盒”的执行环境避免了复杂的环境还原。最后通过Yakit强大的热加载功能将远程调用能力无缝集成到自动化测试流程中。这套方法的核心优势在于其普适性。无论前端使用AES、RSA、国密还是各种自定义的哈希签名算法只要你能在浏览器里执行它就能通过JsRpc远程调用它。你不需要理解算法细节不需要寻找密钥只需要找到函数入口。在实际渗透测试项目中我多次依靠这套组合拳快速突破前端加密的封锁。遇到的最复杂情况是一个登录流程包含三个连续的加密步骤且密钥由WebSocket推送。即使如此通过仔细分析JS代码找到最终的加密函数入口点并通过JsRpc一次性传入所有必要参数依然成功实现了自动化测试。记住你的目标是发现后端业务逻辑漏洞而不是成为密码学专家。这套“手动分析 JsRpc桥接 Yakit热加载自动化”的流水线正是帮你高效达成这一目标的利器。下次再遇到前端加密不妨自信地打开Yakit和浏览器开发者工具开始你的“绕过”之旅。