1. 项目概述:当加密遇上抓包
在Web应用安全测试和日常开发调试中,我们经常需要拦截和修改客户端与服务器之间的数据包。当数据以明文形式传输时,使用Burp Suite、Charles或Fiddler这类代理工具可以轻松实现。然而,随着安全意识的提升,越来越多的前端应用开始采用AES(高级加密标准)等对称加密算法对请求体进行加密,再通过HTTPS通道传输。这就形成了一个“双重保险”:传输层有TLS/SSL保护,应用层还有AES加密。对于测试人员或开发者而言,这堵加密墙使得我们无法直接看到或修改业务逻辑层面的数据,传统的“中间人”抓包工具瞬间失效,看到的只是一串串无法理解的密文。
这正是“Yakit实战:如何在前端AES加密场景下抓取并修改明文数据包”要解决的核心痛点。Yakit作为一款新兴的、面向安全从业者的集成化工具平台,其强大的MITM(中间人攻击)和插件化能力,为我们提供了一条破解此困境的路径。这个项目的目标不是破解AES算法本身(那是不切实际且不合规的),而是模拟一个“合法”的解密与再加密流程:在数据包经过我们的代理时,利用已知的密钥和加密模式,将其解密为明文供我们查看和修改,然后再用同样的密钥加密回去,发送给服务器。整个过程对客户端和服务器都是透明的,它们依然认为自己在进行安全的加密通信。
这听起来像是“魔法”,但本质上是一种基于代理的加解密转发。它特别适用于以下几种场景:第一,安全测试人员需要对加密接口进行漏洞挖掘,比如测试越权、注入等逻辑漏洞,必须能看到具体的参数;第二,前端开发者在联调时,后端返回的也是加密数据,需要解密查看响应内容以定位问题;第三,学习研究前端加密实现,动态分析其加密逻辑和密钥管理方式。如果你正被前端AES加密的数据包搞得束手无策,那么接下来的完整配置和实战过程,将为你提供一套可直接复现的解决方案。
2. 核心思路与工具选型解析
2.1 为什么传统抓包工具会失效?
要理解解决方案,先得明白问题根源。在一个典型的前端AES加密场景中,数据流是这样的:
- 前端加密:用户在页面表单输入数据(如
username=admin&password=123456),前端JavaScript代码使用预置或动态获取的AES密钥(Key)、初始化向量(IV)和指定的模式(如CBC),调用类似CryptoJS的库,将明文数据加密成Base64格式的密文。 - 发送请求:前端将密文作为请求体(例如放在
data字段),通过HTTPS POST请求发送给服务器。 - 服务器解密:服务器端用相同的Key和IV解密接收到的密文,还原出明文数据进行业务处理。
当你使用Burp Suite等工具设置代理后,你的设备成为了客户端和服务器的中间人。你确实能截获HTTPS流量(在安装并信任了Burp的CA证书后),但你看到的应用层数据(HTTP Body)已经是经过前端加密的密文。没有密钥,你无法解密;即使你强行修改了密文中的一个字符,由于AES CBC等模式的雪崩效应,服务器解密时几乎必然失败,导致请求被拒绝。
因此,核心思路必须转向:让代理工具具备“知晓”密钥并执行加解密的能力。我们需要一个“智能代理”,它能在流量经过时,自动完成“解密 -> 展示/修改 -> 再加密”的流水线作业。
2.2 为什么选择Yakit?
市面上具备一定脚本能力的代理工具不止Yakit,比如Burp Suite的Extender API、Mitmproxy的Python脚本。选择Yakit进行此次实战,主要基于以下几点考量:
- 原生集成与便捷性:Yakit内置了强大的MITM服务器和流量劫持功能,无需像Mitmproxy那样需要单独编写脚本并启动服务。其“热加载”能力使得编写JavaScript插件来处理流量变得非常直观和快速,修改代码后几乎实时生效,极大提升了调试效率。
- 对前端加密场景的友好支持:Yakit的插件系统设计,使得它能够非常方便地操作HTTP请求/响应的原始Body。我们可以直接编写JavaScript代码,调用Node.js环境的Crypto模块(Yakit插件引擎基于Node.js)来执行AES加解密,这与前端使用的CryptoJS库在算法上能很好对齐,减少了环境差异带来的麻烦。
- 一体化工作流:Yakit不仅是一个抓包工具,还集成了漏洞扫描、端口爆破、爬虫等多种安全测试功能。对于安全测试人员来说,在一个工具内完成从流量拦截、解密、修改到漏洞探测的整个流程,体验更加连贯。
- 活跃的社区与文档:虽然相对较新,但Yakit拥有活跃的中文社区和不断完善的文档,遇到问题时更容易找到解决方案或获得帮助。
当然,这个方案的前提是你必须能够获取到前端使用的AES加密密钥(Key)和初始化向量(IV)。这通常通过逆向分析前端JavaScript代码、调试获取或是在开发/测试环境中由开发团队提供。没有这个前提,任何针对应用层加密的抓包修改都是空中楼阁。
3. 环境准备与关键信息获取
3.1 Yakit的安装与基础配置
首先,你需要从Yakit官网下载对应操作系统(Windows/macOS/Linux)的安装包并完成安装。安装过程比较简单,这里不赘述。安装完成后,启动Yakit,你会看到主界面。
进行MITM抓包前,有几个关键配置步骤:
- 启动MITM服务器:在Yakit左侧功能栏找到“MITM”模块,点击进入。通常你需要配置监听端口(默认8080)和上游代理(如果需要)。点击“启动”按钮,Yakit会在本地启动一个HTTP/HTTPS代理服务器。
- 安装CA证书:这是拦截HTTPS流量的必要条件。在“MITM”页面有“下载CA证书”的选项。将证书下载到本地,然后手动导入到你的系统或浏览器的受信任根证书颁发机构中。以Chrome浏览器为例,你也可以在启动代理后,访问
http://yakit.local来下载和安装证书。务必确保证书安装成功且被信任,否则你只能看到HTTPS握手失败或密文。 - 配置设备代理:将你需要抓包的设备(可以是PC,也可以是手机模拟器或真机)的网络代理设置为Yakit所在机器的IP地址和监听端口(如
192.168.1.100:8080)。
完成以上步骤后,在Yakit的“MITM”页面,你应该能看到“劫持”到的HTTP/HTTPS请求列表。如果看不到HTTPS请求,请检查证书安装步骤。
3.2 定位前端AES加密参数
这是整个实战中最关键、也最具技术挑战性的一步。你需要分析目标Web应用的前端代码,找到加密函数和密钥。以下是几种常见的方法:
静态代码分析:
- 在浏览器中打开目标网站,按F12打开开发者工具。
- 切换到“源代码”(Sources)标签页,使用全局搜索(Ctrl+Shift+F)功能,搜索关键词如
CryptoJS、AES、encrypt、mode、padding、iv、key、secret等。 - 仔细阅读搜索到的JavaScript文件,找到加密函数的定义和调用处。密钥(Key)和IV可能以硬编码字符串、从某个接口获取、或通过某种算法生成的形式存在。
动态调试追踪:
- 在开发者工具的“网络”(Network)标签页,找到一个发送加密数据的请求(请求体看起来像Base64字符串)。
- 在“源代码”标签页,使用“XHR/ Fetch断点”功能,在该请求的URL上设置断点。
- 重新触发请求,代码会在发送前暂停。此时调用堆栈(Call Stack)会显示出发送请求前的函数调用链。
- 沿着调用栈向下查找,通常能找到执行加密操作的函数(例如
CryptoJS.AES.encrypt)。在此函数内部,你可以查看传入的参数,从而直接看到明文、密钥、IV等值。
Hook关键函数:
- 这是一种更高级的方法。在开发者工具的“控制台”(Console)中,可以注入JavaScript代码来“钩住”(Hook)加密函数。例如:
var originalEncrypt = CryptoJS.AES.encrypt; CryptoJS.AES.encrypt = function (message, key, cfg) { console.log("[HOOK] Plaintext:", message); console.log("[HOOK] Key:", key); console.log("[HOOK] Config:", cfg); // 继续执行原函数 return originalEncrypt(message, key, cfg); }; - 执行上述代码后,再触发请求,加密函数的参数就会被打印到控制台。这种方法非常有效,但需要网站没有做很强的反调试措施。
- 这是一种更高级的方法。在开发者工具的“控制台”(Console)中,可以注入JavaScript代码来“钩住”(Hook)加密函数。例如:
实操心得:在实际操作中,密钥和IV可能是固定的,也可能是每次会话动态生成的。对于动态生成的情况,你需要进一步分析生成逻辑。一个常见的模式是:前端先通过一个公开的接口获取一个“加密种子”或“会话密钥”,然后用这个种子结合固定盐值(Salt)或时间戳,通过某种哈希算法生成最终的AES密钥。你需要通过调试,理清这个完整的链条。
假设通过分析,我们得到以下信息:
- 加密算法:AES-CBC
- 密钥(Key):
0123456789abcdef0123456789abcdef(32字节十六进制字符串,对应AES-256) - 初始化向量(IV):
abcdefghijklmnop(16字节字符串) - 填充方式:PKCS7
- 输出格式:Base64
请务必记录下这些信息,它们将是编写Yakit插件的核心输入。
4. Yakit插件编写:解密与再加密流水线
Yakit通过“流量劫持”功能配合“插件”来实现对数据包的动态修改。我们将编写一个JavaScript插件,在请求发送到服务器前,解密其Body;在服务器响应返回后,解密其Body(如果需要查看明文响应的话)。这里我们主要关注请求的修改。
4.1 创建与配置插件
- 在Yakit主界面,进入“插件”模块。
- 点击“新建插件”,选择“MITM插件”。
- 给插件起一个名字,例如
AES-CBC Decryptor。 - 在代码编辑区,我们将编写完整的处理逻辑。
4.2 完整插件代码解析
以下是一个功能完整的Yakit MITM插件代码,用于处理AES-CBC加密的请求和响应。请将之前获取到的密钥、IV等参数替换到代码中相应位置。
// 引入必要的模块 const crypto = require('crypto'); // 配置你的AES参数 (请根据实际情况修改!!!) const AES_KEY = Buffer.from('0123456789abcdef0123456789abcdef', 'hex'); // 32字节 for AES-256 const AES_IV = Buffer.from('abcdefghijklmnop', 'utf8'); // 16字节 const ALGORITHM = 'aes-256-cbc'; const INPUT_ENCODING = 'base64'; const OUTPUT_ENCODING = 'utf8'; // 辅助函数:AES解密 function aesDecrypt(encryptedBase64) { try { const encryptedBuffer = Buffer.from(encryptedBase64, INPUT_ENCODING); const decipher = crypto.createDecipheriv(ALGORITHM, AES_KEY, AES_IV); // 使用 autoPadding,兼容PKCS7 let decrypted = decipher.update(encryptedBuffer); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(OUTPUT_ENCODING); } catch (e) { return `[Decrypt Error] ${e.message}: ${encryptedBase64}`; } } // 辅助函数:AES加密 function aesEncrypt(plainText) { try { const cipher = crypto.createCipheriv(ALGORITHM, AES_KEY, AES_IV); let encrypted = cipher.update(plainText, OUTPUT_ENCODING, INPUT_ENCODING); encrypted += cipher.final(INPUT_ENCODING); return encrypted; } catch (e) { return `[Encrypt Error] ${e.message}: ${plainText}`; } } // 判断请求体是否可能是我们的目标加密数据(简单启发式判断) function isTargetEncryptedData(body) { if (!body || typeof body !== 'string') return false; // 规则1: 非空且长度较长 // 规则2: 是标准的Base64字符串(可选,可通过正则简单判断) const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; return body.length > 16 && base64Regex.test(body.replace(/\s/g, '')); } // MITM插件主处理函数 function mirrorHttpFlow(flow) { // 1. 处理请求(在请求发往服务器前) if (flow.IsRequest) { let req = flow.Request; // 检查请求方法是否为POST/PUT等可能有Body的,并检查Content-Type if (req.Method === 'POST' || req.Method === 'PUT') { let body = req.Body; if (isTargetEncryptedData(body)) { try { // 解密请求体 let decryptedBody = aesDecrypt(body); console.log(`[Req Decrypted] ${req.Url}: ${decryptedBody.substring(0, 200)}...`); // 这里是关键:将解密后的明文存入一个自定义字段,供后续修改 // 原始密文仍然保留在Body中,直到我们决定替换它 flow.Set(`decrypted_request_body`, decryptedBody); // 可以在这里将解密后的内容显示到Yakit的UI上,方便查看 // flow.ToJson 然后修改? // 更常见的做法是,我们先不解密替换,而是通过“热加载”在另一个阶段修改。 // 但Yakit的MITM插件通常是在这个函数里直接修改flow.Request.Body。 // 所以,如果我们想修改,应该在这里直接替换Body。 // 假设我们想将明文中的某个字段值从“test”改为“admin” if (decryptedBody.includes('"username":"test"')) { let modifiedBody = decryptedBody.replace('"username":"test"', '"username":"admin"'); // 将修改后的明文重新加密 let reEncryptedBody = aesEncrypt(modifiedBody); // 替换原始请求体 req.Body = reEncryptedBody; console.log(`[Req Modified & Re-encrypted] Username changed to admin.`); } // 如果只是想查看,不修改,可以什么都不做,或者将解密内容记录到日志。 } catch (e) { console.error(`Failed to process request for ${req.Url}:`, e); } } } } // 2. 处理响应(在响应返回客户端前) else { let rsp = flow.Response; // 检查响应状态码和Content-Type,判断是否可能是加密响应 if (rsp.StatusCode === 200 && rsp.Body) { let body = rsp.Body; // 这里需要更精确的判断,比如根据URL路径或响应头特征 // 假设我们只处理特定API的响应 if (flow.Request.Url.includes('/api/secure-data') && isTargetEncryptedData(body)) { try { let decryptedBody = aesDecrypt(body); console.log(`[Rsp Decrypted] ${flow.Request.Url}: ${decryptedBody.substring(0, 500)}...`); // 同样,可以修改响应后再加密回去,但需谨慎,可能破坏客户端逻辑 // 这里我们仅解密并存储,用于查看 flow.Set(`decrypted_response_body`, decryptedBody); } catch (e) { console.error(`Failed to process response for ${flow.Request.Url}:`, e); } } } } // 必须返回true,表示此流量已被处理(或至少检查过) return true; } // 导出主函数 module.exports = { mirrorHttpFlow };4.3 代码关键点与配置说明
- 模块引入:
const crypto = require('crypto');这是Node.js内置的加密模块,功能强大且标准,确保与前端CryptoJS的算法实现一致。 - 参数配置区:这是你需要重点修改的地方。
AES_KEY: 密钥。注意格式,如果前端用的是十六进制字符串,就用Buffer.from(keyString, 'hex');如果是Base64,就用'base64';如果是普通字符串,就用'utf8'。长度必须是16(AES-128)、24(AES-192)或32(AES-256)字节。AES_IV: 初始化向量。必须为16字节。确保获取的IV与前端完全一致,包括编码。ALGORITHM: 指定算法和模式。'aes-256-cbc'表示AES-256算法,CBC模式。如果前端是ECB模式(不推荐使用),则应为'aes-256-ecb',且ECB模式不需要IV。INPUT_ENCODING/OUTPUT_ENCODING: 指定密文和明文的编码。前端通常输出Base64密文,所以解密时输入编码为'base64'。明文一般是UTF-8字符串。
- 加解密函数:
aesDecrypt和aesEncrypt函数封装了Node.jscrypto模块的调用,并添加了错误处理。注意createDecipheriv和createCipheriv的使用,这是使用指定Key和IV的正确方法。 - 目标判断函数:
isTargetEncryptedData是一个简单的启发式函数,用于避免对所有请求都尝试解密,提高效率并减少错误。你可以根据实际情况强化判断逻辑,例如检查特定的URL路径、请求头(如Content-Type: application/json)等。 - 主处理函数:
mirrorHttpFlow是Yakit规定的入口函数。参数flow包含了单个HTTP请求或响应的所有信息。flow.IsRequest: 布尔值,判断当前是请求还是响应。flow.Request/flow.Response: 获取请求或响应对象,包含Method、Url、Headers、Body等属性。flow.Set()和flow.Get(): 用于在请求和响应处理之间传递自定义数据。例如,本例中在请求阶段将解密后的明文存储起来(虽然本例后续未使用,但展示了用法)。- 修改数据包:直接在函数中修改
req.Body或rsp.Body即可。修改请求体后,Yakit会自动将修改后的请求转发给服务器。修改响应体后,会自动返回给客户端。
- 修改逻辑:示例中展示了一个简单的修改场景:如果解密后的JSON字符串中包含
"username":"test",就将其替换为"username":"admin",然后重新加密并赋值给req.Body。你可以根据测试需求,编写更复杂的修改逻辑,如递增ID、遍历参数、注入测试Payload等。
注意:在响应处理部分,对响应体进行解密和修改需要格外小心。因为客户端JavaScript期望收到的是加密数据,如果你修改了响应内容却没有正确加密回去,会导致客户端解密失败,页面功能异常。通常,查看响应解密内容用于调试即可,除非有特殊测试目的,否则不建议主动修改响应。
5. 插件部署与实战抓包流程
5.1 加载并激活插件
- 将上述代码粘贴到Yakit插件的编辑器中,修改好密钥等配置。
- 点击编辑器上方的“保存”按钮。
- 进入“MITM”模块,在“劫持”页面,找到“加载插件”或“热加载”区域(不同Yakit版本位置可能略有不同)。你应该能看到你刚创建的插件名称(如
AES-CBC Decryptor)。 - 勾选该插件,使其处于激活状态。Yakit会自动将插件代码加载到MITM服务器中。
5.2 启动抓包与触发请求
- 确保Yakit的MITM服务器正在运行(端口监听中)。
- 确保你的浏览器或测试设备已正确配置代理指向Yakit。
- 访问目标Web应用,进行登录、提交表单等会触发加密请求的操作。
- 回到Yakit的“MITM” -> “劫持”页面,你应该能看到捕获到的HTTP/HTTPS请求流。
5.3 观察与验证
- 查看原始流量:在流量列表里,找到你触发的那个POST请求。点击查看详情,在“请求”标签页的“原始”视图下,你会看到Body是一串Base64密文。这是未经插件处理的样子。
- 验证插件工作:如果插件配置正确且密钥无误,你应该能在Yakit的“插件日志”或控制台输出中(通常有一个“日志”标签页),看到插件打印的
[Req Decrypted] ...信息,后面跟着解密后的JSON或表单数据明文。 - 查看修改效果:如果你在插件中编写了修改逻辑(如替换username),那么在发送到服务器的请求中,Body的密文已经发生了变化。你可以通过对比修改前后向服务器发送的请求包来验证。更直接的方法是,查看服务器的响应。如果修改成功,服务器会以修改后的参数(如username=admin)进行处理,并返回相应的结果。
一个完整的实战循环:
- 目标:测试一个加密登录接口的用户名枚举漏洞。
- 步骤:
- 正常登录一次,通过插件日志获取解密后的请求体格式,例如:
{"username":"test_user", "password":"encrypted_password_hash", "timestamp":1234567890}。 - 修改插件代码,将解密后的
username字段值,从一个字典文件中逐行读取并替换。 - 在插件中,每替换一个用户名,就重新加密并发送请求。
- 观察服务器的响应(如状态码、响应时间、返回信息),判断用户名是否存在。
- 正常登录一次,通过插件日志获取解密后的请求体格式,例如:
这就需要你在插件中实现一个循环或读取外部文件的功能,这超出了单个请求修改的范畴,可能需要结合Yakit的“Web Fuzzer”功能,或者编写更复杂的、能维持会话状态的插件。但核心原理依然是本插件所展示的解密->修改->再加密。
6. 常见问题排查与进阶技巧
6.1 插件不生效?按此清单排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 看不到任何插件日志输出 | 1. 插件未激活。 2. 插件代码有语法错误,加载失败。 3. 流量未匹配插件判断条件。 | 1. 在MITM设置中确认插件已勾选。 2. 查看Yakit日志或插件管理界面是否有错误提示。 3. 在插件开头加 console.log("Plugin loaded!")测试。简化isTargetEncryptedData函数,直接返回true进行测试。 |
解密失败,报错Invalid key length或Invalid IV length | 密钥或IV的格式、长度或编码转换错误。 | 确认前端的Key/IV究竟是十六进制、Base64还是普通字符串。用Buffer.from(key, 'hex').length检查转换后的字节长度是否正确(16/24/32)。IV必须是16字节。 |
解密失败,报错error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt | 1. 密钥或IV错误。 2. 加密模式不匹配(如前端是GCM模式,插件用CBC)。 3. 密文在传输中被修改或损坏。 4. 填充方式不匹配。 | 1. 反复核对Key/IV,确保与前端完全一致。 2. 确认前端使用的AES模式(CBC, GCM, ECB等),修改 ALGORITHM变量。3. 确保抓取的是完整的密文,没有被截断。 4. Node.js的 crypto默认使用PKCS7填充,与CryptoJS的PKCS5(在AES中等同于PKCS7)通常兼容。如果前端是NoPadding,则需要特殊处理。 |
| 解密后是乱码 | 输出编码错误。解密结果是Buffer,需要用正确的编码转为字符串。 | 确认明文原本的编码。如果是JSON,就用'utf8'。尝试decrypted.toString('utf8')。 |
| 修改后请求发送失败或服务器返回错误 | 1. 重新加密后的密文格式不对。 2. 修改明文时破坏了结构(如JSON格式错误)。 3. 请求签名或Token被破坏(如果请求还有签名机制)。 | 1. 将重新加密后的密文与原始密文对比,长度、字符集应相似。 2. 将修改后的明文用 JSON.parse()测试一下是否合法。3. 这是更复杂的情况,前端可能对完整请求体做了签名。你需要先解密,修改数据,然后按照前端的签名算法重新计算签名,并更新请求头中的签名字段。这需要深入分析前端代码。 |
6.2 进阶技巧与注意事项
- 动态密钥的处理:如果密钥是每次会话动态生成的,你的插件需要能够获取它。一种方法是在插件中Hook前端生成密钥的JavaScript代码(这非常复杂)。更实际的方法是,如果动态密钥是通过某个API接口下发的,你可以先捕获那个接口的响应,从中提取密钥,然后缓存起来供后续加解密使用。这需要插件有状态存储的能力。
- 处理多种加密模式或端点:一个应用可能对不同接口使用不同的加密参数。你可以在插件中维护一个“URL模式 -> 加密配置”的映射表,根据
flow.Request.Url来动态选择解密策略。 - 性能考量:加解密是CPU密集型操作。如果处理大量并发请求,可能会影响Yakit性能。在插件中做好条件判断,只对必要的请求进行加解密操作。
- 与Yakit Fuzzer结合:对于参数爆破等测试,更高效的方法是使用Yakit的“Web Fuzzer”。你可以先手动抓一个加密包,然后利用Fuzzer的“数据包变形”功能,配合一个“外部处理器”插件。这个处理器插件接收Fuzzer生成的明文Payload,将其加密后返回给Fuzzer进行发送。这样就能实现自动化测试。
- 安全与合规警告:此技术仅限用于您拥有合法测试权限的系统,如公司内部测试、授权渗透测试、或个人学习研究。切勿用于任何未授权的系统,否则将触犯法律。
实操心得:在实际测试中,最耗时的部分往往是前端加密逻辑的分析。一旦成功提取出稳定的Key和IV,编写Yakit插件本身是很快的。建议在分析阶段,多利用浏览器的调试工具,特别是“Sources”面板中的代码搜索和“Network”面板的XHR断点功能。对于复杂的混淆代码,可以尝试使用JS反混淆工具(如 de4js)进行初步处理,但核心还是需要耐心地跟踪代码执行流程。成功解密出第一个数据包的那一刻,所有的努力都是值得的,因为它为你打开了一扇通往应用核心逻辑的大门。