Yakit热加载自动化破解前端AES/RSA加密表单的渗透测试实战

Yakit热加载自动化破解前端AES/RSA加密表单的渗透测试实战 1. 项目概述当加密表单遇上自动化渗透在Web应用安全测试中前端加密表单一直是个让人又爱又恨的“硬骨头”。爱的是它代表了开发者在安全意识上的进步恨的是它给传统的手动渗透测试流程带来了巨大的阻碍。想象一下这个场景你打开Burp Suite准备对一个登录接口进行爆破或重放攻击却发现提交的用户名和密码是一长串毫无规律的密文。你修改任何一个字符整个密文就失效了服务器直接返回“数据解密失败”。这种基于AES、RSA等算法的前端加密让很多依赖拦截修改请求的测试手段瞬间哑火。过去面对这种情况测试人员要么得去逆向分析前端的JavaScript加密逻辑自己写脚本模拟加密过程要么就得跟开发沟通临时关闭加密或者获取密钥。前者技术门槛高、耗时耗力后者流程繁琐且可能影响测试的真实性。整个过程就像是在一扇锁着的门前反复试探效率极低。而Yakit的出现特别是其“热加载”功能为这个问题提供了一个极其优雅的自动化解决方案。它本质上是一个“代码注入”与“流量操控”的瑞士军刀。我们不再需要完全逆向整个加密流程也不需要脱离测试工具链。我们可以直接在HTTP请求的生命周期中插入我们自己的JavaScript代码在请求发出前实时计算正确的加密参数或者在收到响应后实时解密查看明文。整个过程是动态的、自动化的并且完全集成在Yakit的MITM中间人攻击或Web Fuzzer工作流中。这个项目就是带你一步步实操如何利用Yakit的热加载功能自动化地穿透前端AES/RSA加密表单的防御高效完成登录爆破、参数重放、越权测试等核心渗透测试任务。无论你是刚开始接触Web安全测试的新手还是被加密表单困扰已久的老手这套方法都能显著提升你的测试效率和深度。2. 核心原理与Yakit热加载机制拆解要搞定加密表单首先得理解“热加载”到底热在哪里以及它如何与我们遇到的加密难题相匹配。2.1 前端加密表单的典型模式与测试痛点目前前端加密主要有两种主流模式它们的混合使用最为常见非对称加密RSA传输对称密钥这是最常见、安全性较高的组合方式。流程通常是客户端首次访问时服务器返回一个RSA公钥。客户端随机生成一个AES密钥即sessionKey。客户端用RSA公钥加密这个AES密钥得到encryptedKey将其发送给服务器。服务器用RSA私钥解密encryptedKey得到相同的AES密钥。后续所有敏感数据如登录密码客户端都用这个AES密钥进行加密服务器用同样的AES密钥解密。测试痛点每次会话的AES密钥可能不同且密钥交换过程可能还涉及时间戳、随机数等干扰项手动构造请求异常困难。固定密钥的对称加密AES一些应用可能会使用固定的AES密钥硬编码在前端或由后端固定返回。客户端直接用这个固定密钥加密数据。测试痛点虽然密钥固定但加密模式如CBC、GCM、填充方式PKCS7、初始向量IV等参数若不对同样无法生成有效密文。手动计算并替换请求体非常繁琐。无论是哪种模式传统测试工具在拦截到加密请求时看到的都是“黑盒”密文。我们无法直接修改“密码123”为“密码456”因为我们不知道“密码123”对应的密文是什么修改明文后对应的新密文又是什么。2.2 Yakit热加载的工作原理与优势Yakit的热加载功能其核心思想是“在正确的时机执行你的代码操控原始的流量”。它主要作用于两个环节beforeRequest请求前钩子在Yakit将HTTP请求真正发送到目标服务器之前触发执行用户编写的代码。你可以在这里修改请求的任何部分——URL、Header、Body。对于我们就是在这里调用前端相同的加密函数将我们想测试的明文如爆破字典中的密码加密成合法的密文替换掉原请求体。afterResponse响应后钩子在Yakit收到服务器响应后触发执行用户编写的代码。你可以在这里修改响应内容。我们可以在这里解密响应直接查看明文结果方便判断爆破成功与否。它的核心优势在于无侵入性不需要修改目标应用不需要关闭其加密功能。语言友好直接使用JavaScript编写逻辑前端加密代码往往就是JS移植和调试非常方便。动态集成代码保存后立即生效与Fuzzer、MITM等功能无缝结合一键开始自动化测试。信息可见通过console.log输出调试信息到Yakit控制台整个加密、解密过程完全透明。简单来说热加载给了我们一个“魔法层”让原本对我们不透明的加密解密过程变得可控、可观察、可编程。我们从一个被动的请求观察者变成了一个主动的流量导演。3. 实战环境准备与目标分析在开始写热加载代码之前充分的侦察和信息收集至关重要。你不能对着一个未知的黑盒写代码。3.1 工具准备与目标定位Yakit安装与启动从官方渠道下载并安装Yakit。启动后确保核心引擎正常运行。首次使用可能需要配置监听端口和安装证书用于HTTPS流量解密按照向导操作即可。目标网站确定选择一个有前端加密表单的测试目标务必在合法授权范围内进行。常见的如登录页面、注册页面、修改密码、支付确认等。浏览器与开发者工具使用Chrome或Edge浏览器。打开开发者工具F12切换到“网络”Network标签页并勾选“保留日志”Preserve log。3.2 手动抓包与加密逻辑分析这是最关键的一步目的是成为“第一个明白人”。捕获加密请求在浏览器中打开目标登录页。在开发者工具的Network面板中清空现有记录。在登录框输入测试账号如test/123456点击登录。在网络请求列表中找到提交登录信息的请求通常是/login、/api/user/login等POST请求。点击该请求查看其“标头”Headers和“负载”Payload。分析请求负载你通常会看到Request Payload是类似{encryptedData: U2FsdGVkX1/..., key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...}的JSON结构。encryptedData是AES加密后的密文里面包含了用户名、密码等。key很可能就是经过RSA加密后的AES会话密钥。留意其他可能存在的参数如timestamp时间戳、nonce随机数、sign签名等。追踪加密函数在Network面板点击该登录请求查看“发起程序”Initiator或“响应”Response标签尝试找到调用栈定位到发起这个请求的JavaScript文件。更直接的方法是在“源代码”Sources面板全局搜索CtrlShiftF加密可能用到的关键词如encrypt、AES、RSA、CryptoJS、JSEncrypt、publicKey等。找到关键的加密函数比如encryptPassword(password)、generateEncryptedData(data)。我们的目标就是把这段函数的逻辑“搬”到Yakit的热加载脚本里。注意现代前端项目可能经过Webpack等工具打包代码被压缩混淆。函数名可能变成单个字母。此时需要更多耐心通过关键参数如encryptedData的生成位置设置断点进行调试逐步理清逻辑。记录关键信息加密库使用的是CryptoJS、node-forge、还是浏览器原生的Web Crypto API这决定了我们热加载脚本的环境依赖。加密模式与参数AES的模式是CBC还是GCM填充是PKCS7吗IV初始向量是固定的、随机的还是从服务器获取的密钥管理RSA公钥是如何获取的一个固定值还是单独接口请求AES会话密钥是如何生成和交换的额外参数是否有时间戳、随机数参与加密或签名签名算法是什么4. 热加载脚本编写实战以混合加密为例假设我们分析出的目标采用典型的“RSA加密AES密钥AES加密业务数据”模式。下面我们手把手编写热加载脚本。4.1 创建与配置热加载模块在Yakit主界面找到“Web Fuzzer”或“MITM”模块。这里以更通用的“Web Fuzzer”为例。在Web Fuzzer界面你会看到“数据包编辑器”和“Payload”设置区域。在其附近找到“热加载”或“Codec”相关的标签页不同版本位置可能略有不同通常在标签栏或设置中。点击“热加载”创建一个新的脚本。Yakit会提供一个基础模板包含beforeRequest和afterResponse函数的基本结构。4.2beforeRequest脚本自动化加密我们的目标是在发送请求前将我们想要测试的明文密码动态地加密成服务器能接受的格式。// 1. 引入或定义加密库。如果目标前端使用CryptoJS且Yakit环境内置了可以直接使用。 // 以下假设使用CryptoJS并且环境已支持。 const CryptoJS require(crypto-js); // 2. 定义beforeRequest函数 function beforeRequest(request) { // request 是原始的请求对象我们可以修改它 let req request; // 3. 获取当前请求的Payload假设是JSON格式 let rawBody req.Body; let jsonBody {}; try { jsonBody JSON.parse(rawBody); } catch (e) { console.log(请求体不是JSON无需处理或需要其他处理方式); return req; // 如果不是JSON原样返回 } // 4. 假设我们从Payload中识别出需要加密的明文字段是 passwordPlain // 在Fuzzer中我们可以通过{{params}}等方式将字典值赋给这个字段 let plainPassword jsonBody.passwordPlain; if (!plainPassword) { console.log(未找到明文密码字段跳过加密); return req; } // 5. 模拟前端加密逻辑这里是核心需根据实际分析结果编写 // 案例目标使用固定RSA公钥加密随机AES密钥再用该AES密钥加密数据。 // 5.1 生成一个随机的AES密钥和IV (模拟前端每次请求的生成) const aesKey CryptoJS.lib.WordArray.random(32); // 256位密钥 const iv CryptoJS.lib.WordArray.random(16); // 128位IV // 5.2 使用AES-CBC模式加密密码 const encryptedPassword CryptoJS.AES.encrypt(plainPassword, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(); // 得到密文 // 5.3 使用固定的RSA公钥加密AES密钥 (这里需要替换成目标真实的公钥) // 注意Yakit环境可能没有JSEncrypt这里用CryptoJS的RSA模拟实际可能需用其他库或更复杂处理 // 这里简化演示实际应使用分析得到的公钥和正确的RSA加密函数。 const publicKey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...你的公钥 -----END PUBLIC KEY-----; // 假设有一个rsaEncrypt函数需要根据实际情况实现或引入库 // const encryptedAesKey rsaEncrypt(aesKey.toString(), publicKey); // 由于RSA加密在JS环境较复杂此处用伪代码表示关键步骤 console.log([INFO] 明文密码:, plainPassword); console.log([INFO] 生成AES Key:, CryptoJS.enc.Hex.stringify(aesKey)); console.log([INFO] 生成IV:, CryptoJS.enc.Hex.stringify(iv)); console.log([INFO] AES加密后密文:, encryptedPassword); // 5.4 构造最终服务端期待的Payload格式 // 假设格式为 {“key”: “RSA加密后的AES密钥”, “data”: “AES加密后的密码”, “iv”: “IV的Base64”} jsonBody.key encryptedAesKeyPlaceholder; // 替换为真实的RSA加密结果 jsonBody.data encryptedPassword; jsonBody.iv CryptoJS.enc.Base64.stringify(iv); // IV通常需要传输 // 删除临时使用的明文字段 delete jsonBody.passwordPlain; // 6. 将修改后的JSON对象重新设置为请求体 req.Body JSON.stringify(jsonBody); // 7. 重要如果修改了BodyContent-Length头部可能需要更新Yakit通常会自动处理 // 但最好显式删除让底层库重新计算 delete req.Headers[Content-Length]; console.log([SUCCESS] 请求体已加密替换); return req; } // 将函数暴露给Yakit引擎 module.exports { beforeRequest };关键点解析模块引入require(“crypto-js”)尝试引入加密库。Yakit的Node环境可能预装了常用库如果没有你需要了解环境支持情况或将加密逻辑用纯JavaScript实现。逻辑移植CryptoJS.AES.encrypt这部分代码必须与你从目标网站分析出的加密逻辑完全一致包括密钥长度、模式、填充、IV来源。动态替换我们假设原始请求中有一个passwordPlain字段存放了Fuzzer生成的测试密码。脚本读取它加密后用标准的加密字段data,key等替换它并删除明文字段。调试输出console.log对于调试脚本至关重要你可以在Yakit的控制台看到每一步的输出确认加密是否按预期进行。4.3afterResponse脚本自动化解密响应为了在爆破时能直观看到登录成功与否我们常常需要解密响应查看里面的明文提示。function afterResponse(response) { let resp response; // 1. 只处理感兴趣的响应比如登录接口 if (!resp.Url.includes(/login)) { return resp; } // 2. 获取响应体 let rawBody resp.Body; let jsonBody {}; try { jsonBody JSON.parse(rawBody); } catch (e) { // 如果不是JSON可能是HTML直接返回 return resp; } // 3. 假设服务器返回的加密数据在 encryptedResult 字段 let encryptedData jsonBody.encryptedResult; if (!encryptedData) { console.log([响应] 响应体无加密字段可能登录失败或格式不同); return resp; } // 4. 解密逻辑这里需要知道服务端返回数据用的密钥可能是会话AES密钥 // 注意这个密钥需要你在 beforeRequest 中保存下来并在整个会话上下文共享。 // Yakit热加载脚本是单次执行默认不保存状态。需要通过全局变量或外部文件来传递密钥。 // 这里仅为示例假设我们神奇地知道了密钥 sessionAesKey const sessionAesKey globalThis.sessionAesKey; // 从全局获取 if (sessionAesKey) { try { // 解密逻辑需与实际算法匹配 const decryptedBytes CryptoJS.AES.decrypt(encryptedData, sessionAesKey, { // 参数需与加密时一致 // iv: ..., // mode: ..., // padding: ... }); const decryptedText decryptedBytes.toString(CryptoJS.enc.Utf8); console.log([响应解密] 明文结果, decryptedText); // 可以选择将解密后的明文替换回响应体方便查看 jsonBody.decryptedMessage decryptedText; resp.Body JSON.stringify(jsonBody); } catch (decryptError) { console.log([响应解密] 解密失败, decryptError.message); } } else { console.log([响应解密] 未获取到会话密钥跳过解密); } return resp; } module.exports { beforeRequest, afterResponse // 同时导出 };实操心得响应解密的挑战响应解密比请求加密更难因为解密密钥AES会话密钥的获取和传递是个状态管理问题。一个实用的技巧是如果登录成功后的响应里包含用于后续请求的令牌Token而这个令牌在明文或简单编码的响应头里那么我们可以暂时不纠结于解密整个响应体转而关注如何获取这个Token。afterResponse脚本可以用于提取并打印这个Token作为爆破成功的标志。4.4 密钥管理与会话状态保持这是热加载处理加密表单的高级难点。在“RSA加密AES密钥”的模式中AES密钥是动态的。我们的beforeRequest脚本需要在每次请求前知道当前会话用的AES密钥是什么。解决方案有两种模拟完整密钥交换流程在beforeRequest脚本最开始判断是否是本次测试的“第一个请求”。如果是先模拟浏览器向获取RSA公钥的接口如果有发送请求拿到公钥。然后生成AES密钥用公钥加密并保存这个AES密钥到一个全局变量如globalThis.currentAesKey或外部文件。在构造正式登录请求时使用这个保存的AES密钥加密数据并将加密后的密钥放入请求。后续的请求如爆破尝试都复用这个AES密钥和IV。更巧妙的“会话固定”法如果漏洞存在先手动在浏览器正常登录一次用Burp或Yakit的MITM抓取整个流程。分析发现可能服务器在登录成功后返回的某个Set-Cookie或响应体里包含了加密后的AES密钥或者密钥的标识。在热加载脚本中硬编码这个抓取到的、有效的“加密AES密钥”和对应的IV。之后的所有测试请求都使用这组固定的密钥和IV。这相当于“固定”了本次测试的会话加密上下文。这种方法成功率很高因为很多开发者在实现时会允许同一个加密密钥在短时间内多次使用。// 示例在beforeRequest顶部使用固定密钥如果分析可行 function beforeRequest(request) { // 固定从某次成功会话中捕获的密钥和IV (Hex或Base64格式) const fixedAesKeyHex a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890; const fixedIvHex 1234567890abcdef1234567890abcdef; const fixedAesKey CryptoJS.enc.Hex.parse(fixedAesKeyHex); const fixedIv CryptoJS.enc.Hex.parse(fixedIvHex); // ... 后续使用 fixedAesKey 和 fixedIv 进行加密 ... }5. 集成Web Fuzzer进行自动化渗透测试脚本写好并调试通过后就可以与Yakit强大的Web Fuzzer结合实现自动化测试了。5.1 配置Fuzzer参数与Payload加载原始请求将你捕获到的那个加密登录请求包Raw格式复制粘贴到Yakit Web Fuzzer的请求编辑器里。定位爆破点在请求体中找到原本存放加密密码的字段比如data将其值替换为一个特殊的标记例如{{password}}。但根据我们的热加载脚本设计我们更推荐替换一个我们自己定义的明文字段。比如将请求体改为{ “username”: “testuser”, “passwordPlain”: “{{password}}”, // Fuzzer将在这里注入字典 “otherParams”: “...” }这样原始加密字段data,key等可以暂时留空或保留旧值因为它们会被beforeRequest脚本动态覆盖。设置Payload在“Payload”标签页点击“添加”。类型选择“文件”或“手动输入”载入你的密码字典文件如rockyou.txt的片段。在“参数名”中填写你在请求体中设置的标记名即password。关联热加载脚本在Fuzzer设置区域找到“热加载”或“编码解码”选项。选择你刚刚编写并测试好的那个脚本文件。5.2 执行测试与结果分析开始爆破点击“执行”按钮。Yakit会使用字典中的每一个密码依次执行以下流程用当前密码替换请求体中的{{password}}。调用beforeRequest脚本将明文密码passwordPlain加密并重新构造出完整的、带有正确data和key的请求体。发送这个“合法”的加密请求到服务器。接收响应并可通过afterResponse脚本尝试解密或提取关键信息。结果判断状态码登录成功和失败通常返回不同的HTTP状态码如200 vs 401。响应长度成功和失败的响应体大小通常不同。响应内容这是最准确的。通过afterResponse脚本你可以直接让脚本在解密后的明文里搜索“登录成功”、“token”等关键词并给这个请求打上标记。Yakit的Fuzzer结果表会显示每一轮请求的状态码、长度、时间。你可以根据上述特征进行排序和过滤快速定位成功的请求。5.3 扩展测试场景一旦登录爆破的管道打通这个“热加载加密”的模式可以复用到几乎所有需要处理加密参数的场景越权测试修改加密请求中的用户ID参数测试是否能访问他人数据。重放攻击研究加密请求中是否包含时间戳或序列号测试重放的有效性。业务逻辑漏洞测试加密订单金额、优惠券码等。你只需要修改beforeRequest脚本中需要加密的参数和逻辑即可适配新的测试用例。6. 常见问题排查与调试技巧实录在实际操作中你一定会遇到各种问题。下面是我踩过坑后总结的排查清单。6.1 脚本不执行或报错检查脚本是否被正确加载在Yakit的热加载管理界面确认脚本状态是“启用”。在Web Fuzzer中确认已从下拉菜单选中了该脚本。查看控制台输出Yakit有专门的热加载控制台输出。确保你的console.log语句被执行了。如果没有输出可能是脚本有语法错误导致根本没有运行。检查JavaScript语法Yakit的热加载环境基于某种JavaScript引擎如Otto。确保你的代码语法兼容避免使用太新的ES6特性如let在某些旧环境可能不支持建议用var或者使用Babel等工具预编译。检查模块依赖require(‘crypto-js’)失败可能是环境里没有这个模块。尝试使用Yakit可能内置的其他加密库或者将CryptoJS的源码直接复制到脚本里作为最小化嵌入。6.2 加密结果不正确服务器返回解密失败这是最常遇到的问题说明你的加密逻辑与前端不完全一致。逐位对比这是黄金法则。在浏览器中用固定的测试数据如密码123456进行一次登录用开发者工具仔细记录下最终发送出的所有请求参数key,data,iv,timestamp等的原始值。在你的热加载脚本中在beforeRequest函数里用同样的测试数据123456运行通过console.log输出你计算出的所有参数值。将两者进行逐字段、逐字符的对比。一个Base64编码的尾符、一个Hex大小写的差异都会导致失败。重点检查以下参数密钥AES密钥的生成方式、长度、格式WordArray, Hex, Base64。是随机生成还是固定值是否经过了RSA加密RSA加密后的输出格式Base64是否正确IV初始向量。是全零随机生成还是从服务器获取是否需要随请求发送发送的格式是什么模式与填充AES-256-CBC-PKCS7Padding和AES-256-CBC-PKCS5Padding在大部分情况下是等价的但最好确认前端使用的库的默认行为。数据格式要加密的明文是什么是单纯的密码字符串还是一个JSON字符串如JSON.stringify({“pwd”: “123456”})这个细节至关重要。编码加密后的输出是直接CryptoJS的CipherParams对象还是调用.toString()是Base64格式还是Hex格式必须和前端的格式完全一致。6.3 如何处理时间戳、随机数和签名很多应用为了防重放会在加密数据外再加一层签名或校验。时间戳在beforeRequest脚本中用Date.now()生成当前时间戳。注意单位是毫秒还是秒是否需要与服务器时间同步有时需要从服务器响应获取。随机数Nonce可以用Math.random().toString(36).substr(2)或CryptoJS.lib.WordArray.random(8).toString()生成。签名找到前端的签名算法如HMAC-SHA256。在脚本中实现相同的算法。通常是对“密钥时间戳随机数请求体”等元素的特定拼接字符串进行签名。将计算出的签名放入请求头如X-Sign或请求体。// 示例生成并添加时间戳、随机数和签名 function generateSign(params, secretKey) { // 1. 参数排序并拼接成键值对字符串 const sortedKeys Object.keys(params).sort(); const signStr sortedKeys.map(k ${k}${params[k]}).join(); // 2. 拼接密钥 const toSign secretKey signStr; // 3. 计算HMAC-SHA256 (假设使用CryptoJS) const hash CryptoJS.HmacSHA256(toSign, secretKey); return CryptoJS.enc.Hex.stringify(hash); } // 在beforeRequest中 let timestamp Date.now(); let nonce CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex); jsonBody.timestamp timestamp; jsonBody.nonce nonce; // 假设签名密钥是固定的或从某处获取 const appSecret your_app_secret_here; jsonBody.sign generateSign(jsonBody, appSecret);6.4 性能优化与脚本管理避免重复计算如果RSA公钥是固定的不要在每次请求前都去获取。可以在脚本开头定义为常量。脚本模块化将加密函数、签名函数等提取为独立的工具函数方便在不同测试脚本中复用。使用Yakit的“全局热加载”对于基础性的加密函数可以将其设置为全局热加载脚本这样在所有MITM和Fuzzer任务中都能自动调用无需重复配置。最后破解前端加密表单的自动化测试是一个对耐心和细心要求极高的过程。它不像SQL注入那样有现成的工具一把梭。你需要像侦探一样分析前端代码像工程师一样移植加密逻辑再像测试专家一样设计攻击流程。但一旦这套流程跑通它就会成为你渗透测试武器库中一件非常强大且高效的专属工具能帮你突破许多其他测试者望而却步的防线。每一次成功的逆向和自动化都是对目标应用安全机制最深刻的理解。