1. 项目概述当JS逆向遇上热加载登录爆破这个在渗透测试和日常安全评估中再常见不过的场景如今正变得越来越棘手。几年前你可能只需要一个Burp Suite的Intruder模块配上精心准备的字典就能对登录接口发起冲锋。但现在你大概率会撞上一堵由前端JavaScript精心构筑的加密墙。用户名和密码在离开浏览器前就被一套复杂的算法处理得面目全非服务器收到的是一串毫无规律可言的密文。传统的“发包-爆破”模式在这里彻底失效了。这就是JS逆向要解决的问题我们需要深入前端代码的腹地找到那个负责加密的“黑盒子”理解它的运作逻辑并最终在我们的攻击脚本中复现它。这个过程传统上充满了挑战你需要设置浏览器调试环境、下断点、跟踪调用栈、在控制台里一点点抠代码最后再用Python的execjs或Node.js环境去模拟执行。任何一个环节的变动比如代码混淆、环境检测都可能让你前功尽弃。而今天要聊的“Yakit热加载”正是为了简化这个流程而生。Yakit本身是一个强大的安全工具平台它集成了Burp Suite的流量拦截、修改能力并在此基础上深度整合了基于Go语言的JavaScript执行引擎。所谓“热加载”就是指你可以将逆向分析出的关键JavaScript函数直接“注入”到Yakit的引擎中。之后所有流经Yakit的请求其参数都可以在发送前动态地调用这些注入的函数进行实时加密计算。这意味着你无需再写一个独立的外部脚本去处理加密整个“分析-提取-调用”的闭环都在Yakit这一个工具里完成了。特别是当目标网站使用了Webpack这类现代前端构建工具时其模块化的代码结构对我们既是挑战也是机遇。挑战在于核心函数可能被分散在成百上千个模块中机遇则在于Webpack的模块加载机制有迹可循一旦掌握提取方法我们就能精准地“捞出”所需的功能模块而不必在数万行混淆代码中大海捞针。这篇文章我将以一个模拟的实战场景为例带你走通整个流程从如何定位登录接口的加密参数到分析Webpack打包后的代码结构并提取核心加密模块最后在Yakit中配置热加载插件实现一键化的登录爆破。你会发现这套组合拳打下来从前令人头疼的JS加密登录将变得有章可循。2. 核心思路与工具选型解析2.1 为什么选择Yakit热加载方案在应对前端加密的登录爆破时我们通常有几种技术路径。第一种是“完全外部模拟”即用Python的PyExecJS、js2py或者直接起一个Node.js服务将逆向出的JS代码完整移植过去。这种方法隔离性好但流程割裂需要维护独立的脚本且环境兼容性问题如浏览器特有的window、document对象常常是拦路虎。第二种是“浏览器自动化”如使用Puppeteer或Selenium直接控制一个真实浏览器去填表、点击、抓取加密后的结果。这种方法绕过加密逻辑最彻底但代价是速度极慢资源消耗大完全不适合爆破这类需要高并发的场景。Yakit的热加载方案巧妙地走了中间路线。它内置了一个功能相对完整的JavaScript运行时基于Goja引擎可以执行大多数ES5/ES6语法。更重要的是它允许你将JS代码以“插件”的形式加载到引擎中并暴露成可供调用的函数。在数据包经过时你可以通过类似fuzz标签的语法动态调用这些函数来处理载荷。这就把“逆向分析”和“攻击利用”两个环节无缝衔接了起来。它的核心优势有三点闭环操作分析、调试、利用都在同一个平台完成无需在浏览器开发者工具、代码编辑器和爆破工具之间反复切换极大提升了效率。性能与便利的平衡相比浏览器自动化纯JS引擎的执行速度要快几个数量级能满足爆破的并发需求相比外部脚本它又避免了环境搭建和进程间通信的麻烦。与流量深度集成Yakit本身是流量代理可以轻松查看、修改任何请求响应。热加载函数能直接作用于流经的每一个数据包实现实时加密这对测试加密参数的位置、格式等非常方便。2.2 Webpack模块提取从混沌中寻找秩序现代前端项目尤其是Vue、React应用几乎都使用Webpack进行构建。开发阶段清晰明了的模块化代码经过Webpack打包后会变成一个或多个bundle.js文件里面的代码往往被压缩、混淆并且包裹在Webpack自定义的模块加载器函数中。对于逆向者来说这看起来像一团乱麻。但Webpack的模块化是有固定模式的。一个典型的Webpack打包代码结构如下// 这是一个极度简化的Webpack加载器框架 (function(modules) { // webpackBootstrap // 模块缓存 var installedModules {}; // webpack自实现的require函数 function __webpack_require__(moduleId) { // 检查缓存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 创建新模块并放入缓存 var module installedModules[moduleId] { i: moduleId, l: false, exports: {} }; // 执行模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l true; // 返回模块的exports对象 return module.exports; } // 入口起点 return __webpack_require__(__webpack_require__.s 入口模块ID); })({ // 这是一个模块字典key是模块IDvalue是模块函数 ./src/utils/encrypt.js: function(module, exports, __webpack_require__) { // 这里就是原始的加密函数代码 function encryptData(data) { // ... 加密逻辑 } module.exports { encryptData: encryptData }; }, // ... 成百上千个其他模块 });我们的目标就是从这样一个庞大的modules字典里找到包含加密逻辑的那个模块比如上面的./src/utils/encrypt.js并把它“挖”出来。提取的关键在于定位。通常我们会在浏览器开发者工具的Sources面板中搜索加密参数的特征值如参数名sign、encryptedPassword或加密函数可能的关键字如CryptoJS、encrypt、MD5、RSA从而找到可疑的代码段再通过断点调试最终确定目标函数在Webpack模块中的位置和导出方式。3. 实战环境搭建与初步侦察3.1 目标定位与代理配置假设我们测试的目标是一个名为testvul.com的网站其登录接口为/api/v1/login使用POST方法请求体为JSON格式形如{username:admin,password:a1b2c3d4e5f6...}其中password字段明显是经过加密的密文。第一步配置Yakit作为系统代理。启动Yakit进入“MITM”模块开启“劫持”功能并设置好监听端口默认8080。随后在操作系统或浏览器的网络设置中将HTTP/HTTPS代理指向Yakit127.0.0.1:8080。确保Yakit的证书已正确安装并被系统信任以便解密HTTPS流量。配置完成后在浏览器中访问testvul.com并尝试登录。此时Yakit的“HTTP History”标签页应该能捕获到登录请求。找到那条POST /api/v1/login的请求仔细观察其请求体。你可能会发现password的值是一长串固定的、类似Base64或十六进制的字符串并且每次登录即使密码相同这个值也可能变化说明加入了随机盐或时间戳。同时请求中可能还存在其他可疑参数如sign、nonce、timestamp等这些都是需要逆向的重点。3.2 关键加密逻辑的初步追踪在Yakit中右键点击这条登录请求选择“浏览器中打开”。这会在一个由Yakit控制的、代理配置好的浏览器中重放该请求方便我们进行调试。打开浏览器的开发者工具F12切换到“Network”面板清空记录然后在页面中再次执行登录操作。捕获到登录请求后在其上右键选择“Open in Sources panel”。这会直接跳转到“Sources”面板并定位到可能包含请求生成逻辑的JavaScript文件。通常这个文件会是一个巨大的、名字可能被哈希化的.js文件即Webpack的bundle。此时我们需要在代码中搜索线索。在Sources面板中按CtrlShiftFWindows或CmdOptFMac进行全局搜索。搜索关键词可以尝试加密参数名如password、encrypt、sign。可能调用的加密库如CryptoJS、JSEncryptRSA、md5、sha256。请求相关的关键字如JSON.stringify、axios.post、fetch、payload。当你搜索password时可能会发现类似这样的代码片段var e { username: t.username, password: o.encrypt(t.password) };这告诉我们密码在发送前被一个o.encrypt函数处理了。我们的下一个目标就是找到这个o对象究竟是什么以及encrypt函数的具体实现。4. Webpack模块提取实战详解4.1 定位目标函数与模块ID在上一步找到o.encrypt的调用处后最好的方法是给它打上断点。在代码行号处点击设置一个断点然后再次触发登录操作。浏览器执行到断点时会暂停。此时将鼠标悬停在变量o上开发者工具会显示其值。你可能会看到它指向一个模块的exports对象。同时在Call Stack调用堆栈面板中你可以看到当前的执行路径。仔细观察调用栈中的函数它们很可能都位于同一个Webpack模块函数内部。Webpack模块函数通常长这样function(e, t, n) {...}其中e是module对象t是exports对象n是__webpack_require__函数。我们需要找到这个模块函数的“门牌号”即它的模块ID。一个实用的技巧是在Console面板中当执行暂停在断点时输入arguments.callee.toString()并回车。这会打印出当前正在执行的函数的源代码。仔细看打印出的代码开头你很可能发现它被包含在一个大的function(module, exports, __webpack_require__)中而这个函数本身正是Webpack模块字典里某个键对应的值。虽然我们看不到键名模块ID但我们已经拿到了这个模块的完整函数体。另一种更直接的方法是使用“搜索所有文件”功能搜索o.encrypt函数体内的某句独特代码。比如如果encrypt函数里有一行return CryptoJS.AES.encrypt(...).toString();那么我们就搜索CryptoJS.AES.encrypt。搜索结果会定位到包含这行代码的模块文件。在这个文件的源代码视图里通常你能在文件最底部看到这个模块被添加到Webpack模块数组的映射格式可能是/* harmony export */ ...或者通过查看整个bundle文件的格式化代码找到包裹它的那个模块函数定义。4.2 提取与重构独立模块假设我们最终定位到的加密模块函数如下经过格式化// 模块ID: 1234 function(module, exports, __webpack_require__) { use strict; var t __webpack_require__(5678); // 可能引入了CryptoJS模块 Object.defineProperty(exports, __esModule, { value: true }); exports.encryptPassword void 0; var encryptPassword function(e) { var r Date.now().toString(); var a t.MD5(r SALT_STRING e).toString(); return t.AES.encrypt(a, SECRET_KEY, { mode: t.mode.ECB }).toString(); }; exports.encryptPassword encryptPassword; }我们的任务是把这段代码变成一个能独立运行的JS脚本。这需要解决两个问题依赖和上下文。处理依赖这个模块通过__webpack_require__(5678)引入了一个依赖假设是CryptoJS。我们需要找到模块5678的内容并把它也提取出来或者更简单的方法——在独立环境中直接引入完整的CryptoJS库。模拟上下文原代码使用t来调用加密方法。在独立脚本中我们需要让t指向真正的CryptoJS对象。因此提取后的独立脚本可能如下// 独立加密函数 - extracted_encrypt.js // 1. 引入依赖在Node.js或Yakit环境中可能需要不同的引入方式 // 假设我们使用CryptoJS的CDN链接或本地文件 // 在Yakit热加载中我们可以直接使用其内置的CryptoJS如果已内置或通过外部资源引入。 // 2. 复制核心函数 function encryptPassword(plainPassword) { // 引入CryptoJS这里假设CryptoJS已作为全局变量存在或在Yakit环境中可用 var CryptoJS require(crypto-js); // 在Node环境或Yakit支持require时 // 或者直接使用全局的 CryptoJS如果Yakit已注入 var timestamp Date.now().toString(); // 注意原代码中的 SALT_STRING 和 SECRET_KEY 是逆向分析得到的固定字符串 var md5Hash CryptoJS.MD5(timestamp SALT_STRING plainPassword).toString(); // AES-ECB加密 var encrypted CryptoJS.AES.encrypt(md5Hash, SECRET_KEY, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 // 通常默认是Pkcs7需确认 }).toString(); return encrypted; } // 3. 导出函数使其在Yakit中可被调用 // 在Yakit热加载中最后一行赋值给全局变量是常见的导出方式 global.encryptPassword encryptPassword; // 或者使用 module.exports取决于Yakit引擎的支持情况 // module.exports { encryptPassword: encryptPassword };注意原Webpack模块中的exports.encryptPassword encryptPassword;是将函数作为模块的命名导出。我们在提取时需要根据目标执行环境这里是Yakit的JS引擎调整导出方式。通常Yakit的热加载插件会将整个脚本执行的结果置于一个上下文中我们可以通过将函数赋值给一个特定全局变量如global对象下的属性来暴露它。4.3 验证提取代码的正确性在将代码放入Yakit之前最好先进行验证。我们可以使用浏览器的Console面板来测试。首先在提取出的独立脚本中将最后一行改为console.log(encryptPassword(test123))然后在浏览器Console中确保CryptoJS库已加载如果目标网站使用了通常全局CryptoJS变量已存在。将整个脚本粘贴到Console中执行它会输出加密后的结果。接着我们需要一个“标准答案”进行比对。回到之前打了断点的登录流程在断点处让代码执行完o.encrypt(t.password)这一行然后在Console中手动执行o.encrypt(test123)记录下返回的密文。比较两者输出的结果。如果完全一致恭喜你提取成功如果不一致问题可能出在依赖库版本或配置目标网站可能使用了自定义编译的CryptoJS或者加密模式、填充方式有细微差别如CBC模式需要IV向量。全局变量或环境原代码可能依赖了其他未提取的模块或全局变量。盐值或密钥分析得到的盐值(SALT_STRING)或密钥(SECRET_KEY)可能有误。这就需要你回到断点调试状态仔细检查o.encrypt函数执行过程中的每一个变量和每一步计算与你的提取代码进行逐行比对。5. Yakit热加载插件配置与爆破5.1 创建与调试热加载插件在Yakit中进入“插件”模块点击“新建插件”。选择“Yak”类型Yakit的脚本语言但实际上我们要写的是JavaScript。更常用的方式是使用“热加载”功能它本质上是创建一个能执行JS并暴露函数的插件。我们可以直接使用“Yak Runner”来模拟和调试。新建一个Yak脚本但内容我们主要用fuzz模块来调用JS。不过更直观的方式是通过“Web Fuzzer”模块的热加载功能。编写热加载脚本在“Web Fuzzer”中先输入一个基础的登录请求数据包。然后在请求体如password参数的位置你会看到一个小火箭图标点击它选择“热加载”。编辑热加载代码在弹出的编辑器中将我们之前验证通过的独立加密脚本粘贴进去。但需要做一些适配// Yakit 热加载脚本示例 // 注意Yakit的热加载环境可能内置了部分常用库如果不确定可以用require语法尝试。 // 通常更稳妥的方式是将所有依赖代码都内联。 // 内联CryptoJS (简化版核心)或者使用Yakit可能提供的内置对象 // 这里假设Yakit环境提供了crypto-js模块 try { var CryptoJS require(crypto-js); } catch(e) { // 如果require不可用尝试全局变量或者手动实现关键函数对于简单MD5/AES // 在实际复杂场景下建议将完整的crypto-js库代码打包进热加载脚本。 console.log(CryptoJS not found via require, trying global...); // 假设全局有如果还没有就需要手动引入了。 } function encryptPassword(plainPassword) { var timestamp Date.now().toString(); var md5Hash CryptoJS.MD5(timestamp SALT_STRING plainPassword).toString(); var encrypted CryptoJS.AES.encrypt(md5Hash, SECRET_KEY, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }).toString(); return encrypted; } // 关键将函数暴露给Yakit的fuzz引擎 // 使用 global 变量或直接定义函数Yakit通常能捕获到顶层函数 // 另一种标准方式是使用 module.exports (如果Yakit环境支持CommonJS) if (typeof module ! undefined module.exports) { module.exports { encryptPassword: encryptPassword }; } else { // 回退方案直接挂载到全局 globalThis.encryptPassword encryptPassword; }调试与测试保存热加载代码后Yakit通常会有一个“测试”或“执行”按钮。你可以提供一个测试输入如test123查看输出结果是否与浏览器中生成的一致。如果报错CryptoJS is not defined说明环境缺少依赖。这时你需要将CryptoJS的完整源代码可以从开源项目获取复制出来内联到你的热加载脚本最前面。虽然这样会让脚本体积变大但保证了环境的纯净和一致性。5.2 配置Web Fuzzer进行登录爆破热加载函数调试通过后就可以配置爆破了。构建请求模板在Web Fuzzer中填入目标URL、方法、Headers尤其是Content-Type: application/json。在请求体Raw中写入JSON模板{username:{{user}},password:{{pass}}}这里{{user}}和{{pass}}是待填充的占位符。关联热加载函数选中{{pass}}这个占位符在右侧的“Payload处理”区域点击“添加处理器”选择“代码c”。在代码c的编辑框中你可以直接调用刚才热加载中暴露的函数。假设函数通过module.exports导出调用方式可能类似于{{code(encryptPassword({{pass}}))}}但Yakit的具体语法可能略有不同可能是{{encryptPassword({{pass}})}}或需要通过fuzz模块的函数调用。你需要查阅Yakit的官方文档或示例确认在Payload中调用热加载函数的正确语法。一个常见的模式是热加载脚本中导出的函数会自动注册为可调用的“标签函数”。配置Payloads为{{user}}设置一个用户名字典可能包含admin、administrator、test等。为{{pass}}设置一个密码字典。关键点密码字典中的原始密码会先经过热加载函数encryptPassword的加密处理再替换到请求体中。这样服务器收到的永远是加密后的密文。设置并发与过滤根据目标服务器的承受能力调整并发线程数。在“结果过滤”中可以设置识别登录成功的标志例如响应状态码为200且响应体中含有success: true或跳转的Location头等。反之可以标记error或密码错误等关键字为失败。执行与结果分析点击“开始”按钮Yakit便会使用你提供的用户名字典和密码字典进行组合爆破并自动对每一个密码实时加密。在结果列表中你可以清晰地看到哪些组合触发了成功的响应。5.3 高级技巧处理动态盐值与签名有些网站的加密逻辑更复杂比如密码加密盐值动态获取可能在登录前需要先访问一个/api/get_salt?usernameadmin的接口服务器返回一个一次性的盐值用于本次登录的密码加密。请求签名整个请求体或包含特定头信息需要用一个密钥计算签名如HMAC-SHA256以sign参数的形式附在请求中。对于这类场景Yakit热加载依然可以应对但需要更精巧的设计。方案链式热加载与中间变量第一个请求获取盐值在Web Fuzzer中可以配置“前置请求”。先配置一个请求去获取盐值并从中提取如使用正则或JSON提取器出salt值将其设置为一个临时变量比如{{salt}}。加密函数改造修改热加载中的encryptPassword函数使其接受两个参数明文密码和盐值。function encryptPasswordWithSalt(plainPassword, salt) { var timestamp Date.now().toString(); var md5Hash CryptoJS.MD5(timestamp salt plainPassword).toString(); var encrypted CryptoJS.AES.encrypt(md5Hash, SECRET_KEY, {mode: CryptoJS.mode.ECB}).toString(); return encrypted; } module.exports { encryptPasswordWithSalt: encryptPasswordWithSalt };主请求登录在登录请求的密码参数处调用改造后的函数并传入前置请求获取的盐值变量{{encryptPasswordWithSalt({{pass}}, {{salt}})}}。处理签名如果还有签名可以再写一个热加载函数generateSign(requestBody)计算请求体的签名。然后在Web Fuzzer的“数据包处理”阶段可以添加一个“代码c”处理器在请求最终发送前调用这个函数计算签名并自动添加到请求头或参数中。通过这种方式Yakit热加载能够处理非常复杂的、有状态的前端加密逻辑将多步交互和动态参数的计算全部自动化。6. 常见问题排查与实战心得6.1 逆向与提取阶段的典型问题问题1断点打不上或者代码是压缩混淆的完全无法阅读。排查在Sources面板中点击左下角的{}Pretty-print按钮可以将压缩的代码格式化使其具备可读性。对于高度混淆的代码格式化是第一步。然后不要直接搜索函数名而是搜索加密后参数的特征字符串如请求体中password对应的那一长串密文的前几个字符这可能会直接定位到生成该密文的代码附近。问题2找到了加密函数但它依赖了太多其他模块层层嵌套难以完整提取。策略不要试图提取整个调用链。我们的目标是“复现”而不是“移植”。仔细分析加密函数的核心逻辑它最终调用了哪个加密算法AES, RSA, MD5, SHA256?输入的参数有哪些密码本身、时间戳、随机数、其他固定字符串输出的格式是什么Base64, Hex?方案只要弄清了算法、密钥或密钥生成方式、模式和填充完全可以用一个已知正确的加密库如Python的pycryptodome、Node.js的crypto模块在Yakit外重写一个功能相同的函数。Yakit热加载也支持Python插件这给了我们更多选择。有时用你熟悉的语言重写比硬抠JS代码更高效。问题3加密函数中有浏览器环境特有的对象如window、document、navigator。解决这是JS逆向中最常见的问题。你需要判断这些对象是否真正参与了加密计算。如果只是环境检测比如if (typeof window ! undefined)那么在无头环境中我们可以直接模拟在热加载脚本开头定义global.window {}; global.document {};来绕过检测。如果使用了其属性例如使用了window.btoaBase64编码这可以在Node.js或Yakit环境中用Buffer.from(str).toString(base64)来替代。需要找到这些浏览器API的等效实现并在热加载脚本中提前定义好。6.2 Yakit热加载使用中的坑问题1热加载函数执行报错提示某些变量或模块未定义。解决这是依赖缺失。Yakit的JS引擎并非完整的浏览器环境。你需要将外部依赖内联。以CryptoJS为例去官网下载完整的crypto-js.js文件将其内容复制出来粘贴到你的热加载脚本的最前面。确保它在你的函数定义之前执行。这样你的函数就能使用全局的CryptoJS对象了。问题2热加载函数在测试时运行正常但在实际Fuzzer调用时输出不对或报错。排查作用域问题确保函数被正确导出。在热加载脚本的末尾使用module.exports { yourFunc: yourFunc };是最稳妥的方式。然后在Web Fuzzer的Payload处理器中通过{{yourFunc({{pass}})}}这样的语法调用具体语法请以Yakit最新文档为准。参数类型确认传递给函数的参数类型是字符串。有时从Payload读取的值可能需要显式转换。并发问题加密函数如果是无状态的、纯函数的通常不会有并发问题。但如果函数依赖了外部可变状态比如一个自增的计数器在高并发下可能会出错。尽量将函数设计为无状态。问题3爆破速度很慢。分析热加载JS函数本身执行很快瓶颈通常在于网络延迟目标服务器响应慢。字典过大合理裁剪字典优先使用高质量、有针对性的字典。Yakit Fuzzer配置适当增加并发线程数但不要过高避免被封IP或压垮目标。可以启用“自动重试”和“超时设置”。加密函数本身复杂如果加密函数涉及非常耗时的计算如故意慢哈希可以考虑是否有可能优化或者是否必须采用这种方式。6.3 个人实战心得与技巧保持代码整洁与模块化将加密函数、签名函数、工具函数分别写在不同的热加载脚本中通过module.exports导出。在复杂的测试场景中可以像搭积木一样组合使用它们使配置更清晰。善用“先测试后爆破”在Web Fuzzer中不要一上来就导入大字典开跑。先用一个已知正确的用户名密码对配置好热加载后发送单次请求。对比这个请求与浏览器正常登录发出的请求确保每一个参数包括加密后的密码、签名、时间戳等都完全一致。这是保证后续爆破有效性的最关键一步。留意时间戳与非对称加密如果加密用了服务器时间戳确保你的热加载函数中生成时间戳的逻辑与前端一致通常是Date.now()。对于RSA加密公钥可能是硬编码在前端也可能是动态从服务器获取。如果是动态获取就需要像处理动态盐值一样配置前置请求来获取公钥。Yakit不是万能的对于极其复杂、混淆严重、且有强反调试机制的前端代码纯静态分析和热加载可能不够。此时可能需要结合动态执行跟踪工具或者考虑使用浏览器自动化工具先获取到加密后的结果再研究其规律。Yakit热加载是利器但也要知道其边界。道德与法律边界所有技术都应在合法授权的范围内使用。对自家产品进行安全测试或是在取得明确书面授权的渗透测试项目中这些技术才能发挥其正面价值。切勿用于未授权的系统测试那是违法行为。