4.6 全局编程与生命周期钩子:Web Fuzzer 热加载
在前面的章节中,我们已经深入探讨了 {{yak(...)}} 标签,它赋予了我们在请求模板的特定位置动态生成 Payload 的能力。然而,在许多复杂的测试场景中,我们需要的不仅仅是局部数据的编程,而是对 Fuzzer 发出的每一个 HTTP 请求的生命周期进行全局干预。本章将聚焦于 Web Fuzzer 的另一项核心能力——热加载全局钩子,揭示如何通过编程来控制请求的发送前与响应后处理,从而实现状态维持、动态签名等高级测试策略。
Web Fuzzer 的热加载功能提供了三种不同层面的编程接口,它们共同构成了 Yakit 强大的可扩展性基础:
-
动态 Payload 生成 (
{{yak(...)}}): 专注于请求模板内特定数据的生成,已在 4.4.4 节“动态可编程 Payload:热加载标签”中详细介绍,此处不再赘述。 -
请求生命周期钩子 (
beforeRequest/afterRequest): 作用于整个 Fuzz 任务的全局范围,允许在请求发出前和收到响应后进行统一的编程干预。 -
数据镜像与提取 (
mirrorHTTPFlow): 提供对完整请求-响应对的旁路观察与数据提取能力。
本章将重点解析后两种用法,尤其是生命周期钩子的工作原理与实践应用。
4.6.1 请求生命周期钩子函数:beforeRequest 与 afterRequest
生命周期钩子是 Fuzzer 核心流程中预留的编程切面,允许用户注入自定义的 Yak 代码来处理每一个流经引擎的请求和响应。这与在 MITM 中使用脚本的逻辑一脉相承,但其应用场景更加聚焦于自动化模糊测试。
下图清晰地展示了热加载钩子在 Fuzz 任务中的执行时序:
图:HTTP重放请求生命周期时序图
流程解析:
该时序图准确地描绘了从任务提交到结果返回的全过程。其中,第 4 步(调用 beforeRequest)和第 7 步(调用 afterRequest)是热加载代码介入 Fuzzer 核心流程的关键节点。beforeRequest 发生在 Fuzztag 渲染之后、HTTP 请求发送之前;而 afterRequest 发生在收到 HTTP 响应之后、进行结果分析之前。这为我们提供了精确控制数据流的编程入口。
4.6.1.1 beforeRequest:请求发送前的最终干预
当标准的 Fuzzing 标签(Fuzztag)无法满足复杂的、动态的请求构建需求时,热加载代码机制便提供了命令式的编程能力。其中,beforeRequest 是整个请求生命周期中的第一个,也是至关重要的一个钩子函数(Hook)。它为用户提供了在每个请求数据包被发送到网络之前的最后一次编程干预机会,适用于实现动态签名、加密、添加追踪标识或任何其他无法通过声明式标签完成的复杂逻辑。
beforeRequest 钩子的一个核心特性是它的执行时机:它在所有 Fuzztag(如 {{int}}, {{randstr}} 等)都已经被其对应的值渲染替换之后执行。这意味着它所操作的 req 参数是一个已经基本成型、即将发送的请求,而非原始的、包含模板标签的请求。
函数签名与参数上下文
beforeRequest 钩子函数的定义清晰地揭示了其可用的上下文信息,允许进行精确的条件化处理。
// beforeRequest 允许在每次发送数据包前对请求做最后的处理
// @param isHttps - bool - 当前请求是否为HTTPS
// @param originReq - []byte - Fuzz前的原始请求包,包含Fuzztag
// @param req - []byte - Fuzz后即将发送的请求包
// @return []byte - 将作为最终发送的请求数据
beforeRequest = func(isHttps, originReq, req) {
// ... 自定义逻辑 ...
return req
}
-
isHttps** (bool)**: 一个布尔值,明确指示当前请求是否通过 HTTPS 协议发送。这对于需要根据协议类型执行不同加密或签名逻辑的场景至关重要。 -
originReq** ([]byte)**: 原始请求模板的字节流。这是用户在 Fuzzer 中配置的、未经任何 Fuzztag 渲染的初始请求。提供此参数使用户能够洞察请求的“变异”过程,或基于原始模板的结构执行某些高级逻辑。 -
req** ([]byte)**: 即将发送的请求的字节流。这个请求是originReq经过 Fuzztag 渲染替换后的结果。绝大多数的运行时修改操作都将针对此参数进行,并且其修改后的版本将作为函数的返回值。
代码实践与效果演示
为了具体展示 beforeRequest 的强大功能,我们设定一个常见的实战目标:无论原始请求如何,我们都需要强制为请求的 URL 添加一个名为 a 的查询参数,并为其赋予固定的值;同时,为了便于后端日志追踪,我们还需要为每个请求注入一个唯一的 traceId 请求头。
图:Web Fuzzer热加载代码配置界面
如上图所示,我们在 Web Fuzzer 的“热加载代码”面板中编写 beforeRequest 函数。这个界面允许用户实时编写并应用脚本,极大地提升了调试效率。
// beforeRequest 示例:修改查询参数并添加全局请求头
beforeRequest = func(isHttps, originReq, req) {
// 替换查询参数 a 的值
newReq := poc.ReplaceHTTPPacketQueryParam(req, "a", "fixed-suffix-value")
// 添加或更新请求头 traceId
finalReq = poc.AppendHTTPPacketHeader(newReq, "traceId", "yak-fuzzer-trace-" + string(randn(1000, 9999)))
// 将最终修改后的请求返回
return finalReq
}
代码逻辑拆解:
-
poc.ReplaceHTTPPacketQueryParam(req, "a", "fixed-suffix-value"): 该辅助函数接收经过 Fuzztag 渲染的请求req,并在其 URL 中添加或替换查询参数a。这种方式确保了无论原始请求中是否存在参数a,最终发出的请求中该参数的值一定是fixed-suffix-value。 -
poc.AppendHTTPPacketHeader(...): 此函数负责处理 HTTP 头部。它向请求中添加traceId头。得益于randn(1000, 9999)函数,每个请求的traceId都将是独一无二的,这在分布式系统的链路追踪中非常有用。 -
return finalReq: 函数的返回值将作为最终发送的数据包。如果返回原始的req,则所有修改都将无效。
执行效果验证:
图:Yakit Web Fuzzer模糊测试功能界面展示
上图清晰地展示了 beforeRequest 脚本执行前后的差异。
-
配置阶段 (左侧): 我们配置的基础请求中仅包含一个路径参数的 Fuzztag
{{int(1-2)}},并没有a参数和traceId头。 -
执行结果 (右侧): 在结果列表中,我们可以看到实际发出的请求已经被我们的脚本成功篡改。Fuzztag
{{int(1-2)}}被正常渲染为1或2,而紧随其后,beforeRequest脚本的效果显现了出来:-
URL 被修改为
/1?a=fixed-suffix-value,成功注入了查询参数。 -
请求头部分新增了
traceId: yak-fuzzer-trace-6581,成功添加了唯一的追踪标识。
-
通过这个例子,我们可以看到 beforeRequest 钩子是如何作为一个强大的中间件,以编程方式对批量请求进行统一、动态的调整,从而将 Web Fuzzer 的能力从简单的载荷生成扩展到了复杂的请求逻辑构建层面。
4.6.1.2 afterRequest: 响应接收后的即时处理与预分析
在深入探讨了 beforeRequest 提供的请求侧编程控制能力之后,我们现在转向其对称的另一半——afterRequest 钩子。本节将详细阐述此钩子如何在Fuzzer接收到响应后、执行内置分析规则前,对响应数据进行即时处理。我们将从其核心价值与原理入手,继而通过一个完整的实战案例,展示如何利用它实现对API响应数据的透明化解码,从而大幅提升模糊测试的精度与效率。
afterRequest 的核心价值在于其独特的执行时机。它在网络传输结束与Fuzzer自动化分析开始之间,创造了一个关键的“预处理窗口”。这使得我们能够对原始响应(originRsp)进行任意复杂的编程处理,例如实现有状态测试、动态解密或数据标准化,最终将一个更易于分析的新响应(rsp)传递给后续的处理流水线。
函数签名与参数上下文
afterRequest 钩子函数的参数设计提供了丰富的上下文信息,确保用户在处理响应时拥有对整个交互过程的完整洞察。
// afterRequest 允许在 Fuzzer 分析响应前对其进行最后处理
// @param isHttps - bool - 当前请求是否为HTTPS
// @param originReq - []byte - Fuzz前的原始请求包,包含Fuzztag
// @param req - []byte - Fuzzer实际发送的请求包
// @param originRsp - []byte - 从服务器接收到的未经修改的原始响应
// @param rsp - []byte - 供修改并用于后续分析的响应
// @return []byte - 将作为 Fuzzer 用于分析的最终响应
afterRequest = func(isHttps, originReq, req, originRsp, rsp) {
// ... 自定义逻辑 ...
return rsp
}
-
isHttps**,originReq, **req: 这三个参数与beforeRequest钩子中的含义完全相同,共同构成了此次交互的完整请求上下文。 -
originRsp** ([]byte)**: 从服务器接收到的、未经任何修改的原始响应字节流。它是一个只读的参照物,可用于记录最真实的服务器行为。 -
rsp** ([]byte)**: 一个可供修改的响应字节流副本。所有的处理逻辑都应针对此参数展开,并将其作为函数的返回值,以供 Fuzzer 的匹配器和提取器进行后续分析。
代码实践:透明化解码响应数据
为了具体阐释 afterRequest 的应用,我们将以一个真实场景为例。假设我们正在测试一个API,该API为了数据传输的紧凑性或简易混淆,将其核心业务数据封装在一个JSON结构中,并对该部分内容进行了Base64编码。我们的目标是在Fuzzer中直接对解码后的明文内容进行关键字匹配。
- 准备靶场环境
我们利用 Vulinbox 中内置的靶场进行演示。在 Vulinbox 主界面搜索 "base64",并选择“返回 Base64 编码数据的 JSON 响应”案例。如果您的 Vulinbox 中没有此案例,请先在管理界面执行更新。
图:Vulnbox Agent Base64编码JSON响应测试界面
将靶场提供的 URL (http://127\.0\.0\.1:8787/misc/response/json\-base64\-response\) 复制到 Yakit Web Fuzzer 的地址栏,点击“构造请求”按钮,即可快速生成测试请求。
图:Yakit界面显示构造请求弹窗
- 分析原始响应
发送请求后,我们观察到服务器返回的JSON响应。其中 data.result 字段的值为一长串Base64编码字符串,这对于直接进行内容匹配或漏洞判断造成了障碍。
图:Yakit界面展示HTTP请求与Base64编码响应
- 实施
afterRequest解码脚本
为了解决这个问题,我们可以在“热加载”功能区编写一个 afterRequest 钩子函数。该脚本将在每次收到响应后自动执行解码操作。
图:Yakit热加载代码示例解析响应数据
afterRequest = (isHttps, originReq, req, originRsp, rsp) => {
// 从可修改的响应包`rsp`中提取HTTP Body部分。
// 将响应体(纯文本)解析为结构化的JSON对象,以便进行字段操作。
body = poc.GetHTTPPacketBody(rsp)
jsonBody = json.loads(body)
// 使用点分路径访问JSON对象中嵌套的数据,并对提取的字符串进行Base64解码
base64encoded = jsonBody.data.result
decodeBody = codec.DecodeBase64(base64encoded)~
// 将解码后的字节切片转换为字符串,并用它更新原JSON对象中的`result`字段。
// 这样,原来的Base64密文就被替换成了明文。
jsonBody.data.result = string(decodeBody)
// 将修改后的JSON对象序列化回字符串,并用它替换原始响应包中的Body。
return poc.ReplaceBody(rsp, json.dumps(jsonBody), false)
}
该脚本的执行逻辑清晰明了:
-
提取与解析: 使用
poc.GetHTTPPacketBody提取响应体,并通过json.loads将其从文本解析为可操作的jsonBody对象。 -
定位与解码: 通过点分路径
jsonBody.data.result访问目标数据,并使用codec.DecodeBase64对其进行解码。Yaklang的错误抑制操作符~确保了即使解码失败,脚本也不会中断。 -
更新与回写: 将解码后的明文字符串重新赋值给
jsonBody.data.result字段。随后,使用json.dumps将修改后的对象序列化回JSON字符串,并通过poc.ReplaceBody将其写回响应包。
- 验证处理结果
启用该“热加载”脚本后,再次发送请求。可以看到,响应包中的 result 字段已被成功替换为解码后的明文内容。
图:Yakit工具界面展示HTTP请求与JSON响应数据
通过 afterRequest 钩子,我们成功地将一个对机器不友好的编码响应“透明化”处理,使其变为可直接分析的明文数据。Fuzzer的匹配和提取引擎现在可以基于"message"、"超大响应体"等关键词进行精确的自动化分析,极大地扩展了测试的深度和广度。这个案例充分证明了 afterRequest 在处理复杂或混淆响应时的强大能力。
4.6.2 数据镜像与提取:mirrorHTTPFlow
在探讨了能够直接干预请求-响应流的 beforeRequest 与 afterRequest 钩子后,我们引入一个设计理念截然不同的旁路处理函数:mirrorHTTPFlow。前文我们通过 afterRequest 实现了对响应体的直接修改,本节将揭示一种更为精巧的、非侵入式的数据处理方式。我们将深入剖析 mirrorHTTPFlow 的工作原理,并沿用之前的实战案例,展示如何利用它实现对原始数据的无损观察与衍生数据的自定义提取,从而实现更灵活、更清晰的测试结果呈现。
mirrorHTTPFlow 的核心设计哲学是观察而非干预。它作为一个旁观者,对每一次完整的HTTP交互(请求与响应)进行镜像分析,但不会修改原始数据流。这使得它成为实现自定义结果提取、复杂数据关联分析以及为后续请求提供动态参数的理想工具,正如我们在 4.5.4 节所提及的,它是实现自定义结果展示的关键。
函数签名与核心机制
mirrorHTTPFlow 的函数签名揭示了其作为数据管道和状态传递者的角色。
// mirrorHTTPFlow 允许对每一个请求的响应做处理
// @param req - []byte - Fuzzer实际发送的请求包
// @param rsp - []byte - 从服务器接收到的原始响应包
// @param params - map[string]any - 当前任务上下文中已提取的参数集合
// @return map[string]any - 返回更新后的参数集合,用于后续请求或最终结果展示
mirrorHTTPFlow = func(req, rsp, params) {
// ... 自定义分析与提取逻辑 ...
return params
}
-
req, rsp ([]byte): 分别代表Fuzzer发送的最终请求和从服务器接收的原始响应。它们是分析的数据源。
-
params (map[string]any): 这是一个至关重要的参数,它扮演着“状态容器”的角色。它聚合了Fuzzer内置提取器以及先前
mirrorHTTPFlow调用所产生的所有数据。 -
返回值 (map[string]any): 函数必须返回一个
map[string]any。此返回值会与现有params合并,更新状态容器。容器内的数据不仅可以在后续的模糊测试请求中通过{{params(key)}}语法引用,还会在Fuzzer的结果面板中作为“提取内容”独立展示。
代码实践:非侵入式解码与自定义结果展示
让我们回到之前处理Base64编码响应的场景。afterRequest 通过直接替换响应体来解决问题,虽然直观,但却丢失了服务器最原始的返回数据。mirrorHTTPFlow 提供了另一种更优雅的思路:保留原始响应,同时将解码后的数据作为新的提取结果进行展示。
实施mirrorHTTPFlow提取脚本
我们继续使用Vulinbox的靶场环境。这次,我们在“热加载”区域编写 mirrorHTTPFlow 脚本,其目标是从响应中提取、解码数据,并将其存入 params 映射中。
图:Yakit界面展示mirrorHTTPFlow热加载代码
mirrorHTTPFlow = func(req, rsp, params) {
// 从 Response 中获取 Body,并处理好 JSON 数据
body = poc.GetHTTPPacketBody(rsp)
jsonBody = json.loads(body)
// 从 JSON 数据中提取需要解码的数据,通过 codec.DecodeBase64 解码
encoded := jsonBody.data.result
result = codec.DecodeBase64(encoded)~
// 解码后的内容,放在 params 的 decoded 字段,返回出去即可
params["decoded"] = string(result)
return params
}
此脚本的逻辑与 afterRequest 案例相似,但关键区别在于其最终操作:
-
提取与解码: 步骤与之前完全一致,获取响应体,解析JSON,并解码
result字段。 -
数据存入参数: 核心步骤是
params["decoded"] = string(result)。这里,我们将解码后的明文字符串存入了params映射,并为其指定了键名decoded。 -
返回更新状态: 返回更新后的
params对象,将这个新提取的数据注入到Fuzzer的结果处理流程中。
验证结果:原始数据与提取内容并存
执行脚本后再次发送请求,结果清晰地展示了 mirrorHTTPFlow 方法的优势:
图:Yakit界面展示原始响应与解码提取内容
如图所示,右侧的响应面板依然显示着服务器返回的、包含Base64编码的原始JSON。这对于问题溯源和保留现场证据至关重要。与此同时,下方的“提取内容”面板中出现了一个新的 decoded 字段,其值正是我们解码后的明文。
这种将原始数据与派生分析结果分离展示的方式,实现了测试过程的非侵入性,使得结果视图既保留了真实性,又提供了极佳的可读性。mirrorHTTPFlow 因此成为在不污染原始数据的前提下,进行深度数据挖掘和自定义结果呈现的首选工具。
4.6.3 综合实战:破解动态加密认证API
经过对 beforeRequest, afterRequest, 和 mirrorHTTPFlow 三个核心钩子的独立剖析,我们已经掌握了它们各自在请求构造、响应预处理和旁路数据提取方面的能力。本章将是理论与实践的巅峰结合,我们将设计一个高度仿真的安全挑战场景,要求同时运用这三个钩子,构建一个完整的自动化测试工作流。这个案例将彻底展示,当这些工具协同工作时,如何将一个看似无法自动化的、涉及动态加密和签名认证的复杂API,转变为可被Fuzzer高效测试的透明目标。
我们的核心命题是:通过钩子函数的协同编排,将一个黑盒化的、需要多步交互才能完成的动态认证流程,完全自动化并集成到Fuzzer的单次请求模型中。 这不仅是技术的展示,更是高级自动化安全测试思想的体现。
4.6.3.1 实战场景设定:动态挑战与加密API
为了具体展示这一协同工作流,我们在Vulinbox靶场中构建了一个模拟真实世界高安全性API的场景。该API采用了动态挑战-响应(Challenge-Response)机制来防止重放攻击,并对核心业务数据进行加密传输。其完整的交互流程,如下图所示,涉及多个步骤的精密协作。
图:客户端与服务端动态挑战与加密交互时序图
根据靶场的设计,要成功获取受保护的业务数据,必须严格遵循以下任务步骤:
-
获取挑战:向
/api/get-challenge发起请求,获取加密的challenge和初始化向量iv。 -
解密Nonce:使用预共享的AES密钥 (
YakitVulinboxAES) 和返回的iv解密challenge,得到原始nonce。 -
计算签名:使用预共享的HMAC密钥 (
YakitVulinboxHMACKey-SIGNATURE) 对nonce进行HMAC-SHA256运算,生成签名。 -
访问资源:将Hex格式的签名放入
X-Auth-Signature请求头,请求核心接口/api/user/info。 -
解密响应:如果签名正确,服务器返回的业务数据仍是加密的。需要再次使用AES密钥和响应中的新
iv解密,才能看到最终的敏感信息。
这一流程的复杂性意味着,任何试图直接访问 /api/user/info 的简单请求都将失败。如下图所示,直接请求该接口会收到服务器返回的 401 Unauthorized 错误,明确指出需要先通过 /api/get-challenge 获取有效的挑战凭据。
图:Yakit HTTP重放工具界面展示动态挑战响应
面对这样一个无法通过单次请求完成Fuzzing的目标,Yakit的热加载钩子组合就成为了破解此困局的关键。接下来,我们将构建一个一体化的热加载解决方案,将这个复杂的多步流程自动化,从而实现对受保护接口的深度测试。
4.6.3.2 使用 beforeRequest 实现动态签名注入
我们的第一步是进入Vulinbox靶场的相应模块。在靶场主界面中,我们选择“动态”分类下的“动态挑战响应API靶场”,作为本次实践的目标。
图:Vulinbox Agent动态标签页选择界面
在正式编写自动化脚本前,遵循标准的测试流程,我们首先使用Yakit的Web Fuzzer对挑战接口 /api/get-challenge 进行初步探测。这一步骤的目的是为了验证我们对API交互流程第一步的理解,并观察实际的响应数据结构。
图:Yakit构造请求界面展示URL与HTTP报文
将请求发送后,我们成功获得了200 OK响应,其Body为一个JSON对象,包含了challenge和iv两个字段。这证实了我们的初步分析:challenge和iv均为Base64编码的字符串,后续的解密与签名操作将围绕这两个动态值展开。
图:Yakit Web Fuzzer 请求与响应数据包详情
初步分析确认了API的行为模式。现在,我们的核心任务是:在Fuzzer每次尝试攻击/api/user/info之前,自动完成获取挑战、解密、计算签名这三个前置步骤。beforeRequest钩子为此提供了完美的实现平台,它允许我们在原始请求发送前执行任意代码,并对请求进行最终修改。
以下是我们为此场景编写的beforeRequest热加载脚本:
// beforeRequest 允许在每次发送数据包前对请求做最后的处理
// 定义:func(https bool, originReq []byte, req []byte) []byte
beforeRequest = (https, originReq, req) => {
// 1. 构造并发送获取挑战的前置请求
packet = `GET /api/get-challenge HTTP/1.1
Host: 127.0.0.1:8787
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Cache-Control: max-age=0
`
// 使用poc.HTTP在钩子函数内部发起一个全新的HTTP请求
rsp, _ = poc.HTTP(packet, poc.https(false))~
body = poc.GetHTTPPacketBody(rsp)
// 2. 解析响应,解密获取Nonce
i := json.loads(body)
clg = codec.DecodeBase64(i.challenge)~
iv = codec.DecodeBase64(i.iv)~
// 使用预设密钥进行AES解密,得到Nonce
decNonce := codec.AESCBCDecryptWithPKCS7Padding(`YakitVulinboxAES`, clg, iv)~
// 3. 使用Nonce计算签名
result = codec.HmacSha256(`YakitVulinboxHMACKey-SIGNATURE`, string(decNonce))
sign = codec.EncodeToHex(result)
// 4. 将签名注入到Fuzzer的原始请求中
req = poc.AppendHTTPPacketHeader(req, `X-Auth-Signature`, sign)
// 返回被修改后的请求
return req
}
这段代码的精髓在于,它将一个需要多步交互的认证流程,压缩到了一个自动化执行的函数内。其执行逻辑可以分解为以下几个关键步骤:
-
发起前置请求 (Pre-flight Request):脚本的核心是利用
poc.HTTP函数。它在beforeRequest钩子内部,独立于Fuzzer的主请求,向/api/get-challenge发起了一个硬编码的GET请求。这确保了在处理主请求(例如对/api/user/info的Fuzz)之前,我们总能获取一个全新的、有效的challenge和iv。 -
解析与解密:脚本接收到前置请求的响应后,立即使用
json.loads解析JSON数据,并通过codec.DecodeBase64进行解码。随后,调用codec.AESCBCDecryptWithPKCS7Padding,使用预共享密钥YakitVulinboxAES解密出核心的nonce值。这一步完美复现了任务流程中的第二步。 -
签名计算:获取
nonce后,脚本紧接着使用codec.HmacSha256和预共享的HMAC密钥YakitVulinboxHMACKey-SIGNATURE计算出HMAC-SHA256签名,并将其编码为Hex格式。 -
动态签名注入:最后,也是最关键的一步,脚本使用
poc.AppendHTTPPacketHeader函数,将刚刚计算出的sign值,作为一个名为X-Auth-Signature的请求头,添加回req变量中。这里的req正是Fuzzer即将发送到/api/user/info的原始请求。最终,函数返回这个被动态增强过的请求。
通过这个脚本,我们成功地将一个有状态的、多步的认证过程,无缝地集成到了一个无状态的Fuzzing流程中。无论Fuzzer如何变异/api/user/info的参数,每一次请求都会自动携带一个合法有效的签名,从而绕过了API的认证和防重放机制。
图:Yakit热加载代码注入实现动态签名
如上图所示,该界面生动地展示了 beforeRequest 自动化脚本在实际攻击中的应用效果。这清晰地验证了我们将有状态认证流程无缝集成到无状态Fuzzing中的核心思想。
-
攻击目标与配置:在下方的请求面板中,我们的攻击目标已直接设置为受保护的核心业务接口
/api/user/info。同时,我们通过点击“热加载”按钮,启用了在上半部分编辑器中已编写好的beforeRequest脚本。 -
执行与结果验证:当请求被发送后,右侧的响应面板清晰地显示了服务器返回
HTTP/1.1 200 OK状态码。正如图片中的标注“生效后,响应为 200,不再 401”所强调的,这与直接访问该接口时收到的401 Unauthorized错误形成了鲜明对比。
这张图有力地证明了热加载脚本已成功生效:它在Fuzzer发送请求的瞬间,于后台自动完成了获取挑战、解密、计算签名并注入 X-Auth-Signature 请求头的全过程。正是这个自动化的“预处理”步骤,使得原本会被拒绝的请求能够成功通过API的认证与防重放校验,为后续的深度Fuzzing铺平了道路。
遗憾的是,上图的响应数据仍然是被加密的,不过别担心,我们将会在下一节重点讲解如何自动化解密响应,帮助用户理解应用内容。
4.6.3.3 响应的自动化解密:afterRequest 钩子实践
在前一节中,我们已经通过 beforeRequest 钩子成功地解决了客户端请求的动态认证问题,使得Fuzzer能够向受保护的 /api/user/info 接口发送有效的、携带签名的请求。然而,整个交互流程并未就此结束。根据靶场设计,即使认证成功,服务器返回的业务数据本身也是经过加密的,这对测试结果的有效性判断构成了新的挑战。
如果我们仅使用 beforeRequest,Fuzzer虽然能够收到 200 OK 响应,但其响应体将是无法直接解读的密文。如下图所示,响应体是一个包含加密数据 data 和新 iv 的JSON结构。对于Fuzzer的自动化分析引擎而言,无论内部参数如何 fuzz,只要签名正确,它看到的响应体在结构和内容上都几乎没有变化,从而无法根据响应内容(如关键字匹配、正则表达式搜索)来判断Fuzzing是否成功触发了预期的业务逻辑或漏洞。
要解决这个问题,就必须引入请求-响应生命周期中的另一半——afterRequest 钩子。它的核心使命是在Yakit接收到服务器响应之后、将其最终呈现给用户或分析引擎之前,对响应包进行预处理。
响应解密脚本的核心逻辑
要实现响应的自动化解密,我们需要编写一个 afterRequest 脚本,其执行逻辑与认证过程相反,是一个标准的解密流程。该脚本的核心步骤如下:
-
响应提取与解析:首先,通过
poc.GetHTTPPacketBody(rsp)函数从当前响应包中提取Body内容。由于我们已知响应是JSON格式,随即使用json.loads(body)将其解析为结构化对象。 -
密文与IV提取:从解析后的JSON对象中,分别提取出Base64编码的加密数据(
jsonBody.data)和初始化向量(jsonBody.iv)。使用codec.DecodeBase64将它们解码为原始的字节序列。 -
AES-CBC解密:调用
codec.AESCBCDecryptWithPKCS7Padding函数,并传入预共享的AES密钥(YakitVulinboxAES)、上一步解码出的密文字节和IV字节,执行标准的AES-CBC解密操作。 -
响应体替换:最后,也是至关重要的一步,使用
poc.ReplaceBody(rsp, string(decryptResult), false)函数,将解密后得到的明文字符串替换回原始的响应包中。这确保了后续的Fuzzer分析模块看到的是解密后的内容。
以下是该逻辑的具体脚本实现,它将被配置在Yakit的热加载代码编辑器中:
// afterRequest: 在接收到响应后、Fuzzer分析前执行
// 参数: https, originReq, req, originRsp, rsp
afterRequest = (https, originReq, req, originRsp, rsp) => {
// 1. 提取并解析响应体
body = poc.GetHTTPPacketBody(rsp)
jsonBody = json.loads(body)
// 2. Base64解码加密数据和IV
dataDecode = codec.DecodeBase64(jsonBody.data)~
ivDecode = codec.DecodeBase64(jsonBody.iv)~
// 3. 执行AES-CBC解密
decryptResult = codec.AESCBCDecryptWithPKCS7Padding(`YakitVulinboxAES`, dataDecode, ivDecode)~
// 4. 将解密后的明文替换回响应体
return poc.ReplaceBody(rsp, string(decryptResult), false)
}
构建闭环:双向加解密的自动化Fuzzing
至此,我们已经拥有了处理请求签名的 beforeRequest 脚本和处理响应解密的 afterRequest 脚本。当这两个钩子同时启用时,它们便协同工作,在Fuzzer和目标服务器之间构建起一个功能强大的、针对特定目标的透明加解密代理层。
图:Yakit热加载代码配置界面展示解密逻辑
如上图所示,我们在热加载代码区域同时配置了请求签名与响应解密的脚本。这个组合拳彻底解决了对复杂加密API进行Fuzzing的难题,实现了一个完美的自动化闭环:
-
出向流量(请求):Fuzzer只需关注构造业务数据。
beforeRequest钩子会自动拦截请求,完成获取挑战、计算签名、添加认证头等一系列复杂操作,将一个简单的Fuzz请求“包装”成服务器可接受的合规请求。 -
入向流量(响应):服务器返回的加密响应在被Fuzzer分析引擎处理前,会被
afterRequest钩子拦截。脚本自动完成解密流程,将响应体“解包”成Fuzzer和安全研究员都能直接理解的明文结果。
图:Yakit Web Fuzzer请求与响应数据包展示
最终效果如上图所示。从Fuzzer的视角来看,整个交互过程变得极其简单:它发送一个不含 X-Auth-Signature 的普通GET请求,却能收到一个包含明文JSON(如{"user": "admin", "permission": "all", ...})的 200 OK 响应。目标API仿佛退化成了一个无需认证、明文通信的普通接口。
这种双向的、透明的自动化处理,极大地降低了安全测试的复杂度,使得所有标准的Fuzzing技术,如基于响应内容的漏洞判断、基于数据差异的异常分析等,都能在此类高安全场景下恢复其全部功效,从而显著提升测试的深度与效率。