跳到主要内容

工程实践:Mock 热加载实现无污染客户端测试

· 阅读需 10 分钟
Yak Project
网络安全垂直语言团队

在传统的渗透测试中,我们常常在客户端与服务器之间架设窃听设备,我们拦截、观察、篡改真实的通信。这种模式有效,但存在固有缺陷:

• 状态污染 (State Contamination): 每次测试都会在服务器上留下痕迹。测试支付功能会产生订单,测试删除功能会销毁数据,测试登录会更新 last_login 时间。测试环境很快会变得“脏”,难以重置。

• 不可控的依赖 (Uncontrollable Dependencies): 后端可能不稳定,网络可能有延迟,依赖的第三方服务可能宕机。这些因素都会干扰我们对客户端行为的精确分析。

• 危险操作的顾虑 (Fear of Destructive Actions): 在测试“注销账户”、“清空数据”等功能时,我们总是束手束脚,因为一次误操作就可能导致测试账号失效,打断测试节奏。

所以我们引入了一个新的热加载函数:我们不再去完成真实的通信,而是为客户端创建一个完全受控的响应模拟器。在这个模拟器中,客户端以为它在与真实的服务器对话,但实际上它的一举一动都在我们的掌控之下,且永远不会触及真实后端。整个过程不会与后端服务器发生任何实际的网络通信,因此它非常干净、快速且安全。

热加载函数简介

在传统的渗透测试中,我们常常在客户端与服务器之间架设窃听设备,我们拦截、观察、篡改真实的通信。这种模式有效,但存在固有缺陷:

  • 状态污染 (State Contamination): 每次测试都会在服务器上留下痕迹。测试支付功能会产生订单,测试删除功能会销毁数据,测试登录会更新last_login时间。测试环境很快会变得“脏”,难以重置。
  • 不可控的依赖 (Uncontrollable Dependencies):后端可能不稳定,网络可能有延迟,依赖的第三方服务可能宕机。这些因素都会干扰我们对客户端行为的精确分析。
  • 危险操作的顾虑 (Fear of Destructive Actions): 在测试“注销账户”、“清空数据”等功能时,我们总是束手束脚,因为一次误操作就可能导致测试账号失效,打断测试节奏。

所以,我们引入了一个新的热加载函数:我们不再去完成真实的通信,而是为客户端创建一个完全受控的响应模拟器。 在这个模拟器中,客户端以为它在与真实的服务器对话,但实际上它的一举一动都在我们的掌控之下,且永远不会触及真实后端。整个过程不会与后端服务器发生任何实际的网络通信,因此它非常干净、快速且安全。

热加载函数简介

mockHTTPRequest = func(isHttps, url , req , mockResponse)

1、isHttps

  • 意义: 判断当前被劫持的请求是否使用了 HTTPS 协议。
  • 用途: 在某些场景下,你可能只想对特定协议的请求进行 Mock。例如,只 Mock 加密的 API 请求,而忽略普通的 HTTP 资源请求。

2、url

  • 意义: 请求的完整 URL,例如:https://example.com/api/v1/user/info?id=123
  • 用途: 这是筛选和定位目标请求最常用、最直接的依据。如您的示例代码所示,通过 str.Contains(url, "...") 就可以精准捕获你关心的特定 API 接口。

3、req

  • 意义: 原始的、完整的 HTTP 请求报文(字节切片形式),包含了请求行、所有请求头(Headers)和请求体(Body)。

  • 用途: 当仅通过 URL 无法满足复杂的判断逻辑时,就需要检查 req 的内容。例如:

  • 判断请求方法是否为 POST

  • 检查请求头中是否包含特定的 AuthorizationCookie

  • 检查请求体(Body)中是否包含某些关键词或数据结构。

  • 你需要将 []byte 转换为 string 来进行字符串匹配:string(req)

4、mockResponse (func(fakeResponse string)类型)

  • 意义: 这是一个核心回调函数**,用于向客户端(浏览器)注入一个你完全自定义的虚假响应
  • 用途: 一旦你调用 mockResponse(fakeResponse), 这个请求的生命周期就结束了。Yakit 会将 fakeResponse 的内容直接发回给客户端,原始请求将不会被发送到后端服务器fakeResponse 参数必须是一个符合 HTTP 响应格式的完整字符串。

模版案例

// mockHTTPRequest 会在请求即将发往真实服务器之前被调用。
// 你可以通过调用 mockResponse(fakeResponse) 来伪造一个响应直接返回给客户端,从而阻止原始请求被发送。
// isHttps (bool): 请求是否为HTTPS协议。
// url (string): 请求的完整URL。
// req ([]byte): 完整的原始HTTP请求报文。
// mockResponse func(fakeResponse string): 回调函数,用于注入虚假的响应。
mockHTTPRequest = func(isHttps, url, req, mockResponse) {
// 场景1:Mock一个成功的JSON响应 (例如:获取用户信息)
// 适用于测试前端在拿到正确数据后,UI是否能正常渲染。
if str.Contains(url, "/api/user/profile") {
yakit.Info("[MOCK] 拦截到用户信息请求,返回成功的模拟数据: " + url)
// 构造一个合法的 HTTP 响应报文
// 关键点:
// 1. 状态行: "HTTP/1.1 200 OK"
// 2. 响应头: 至少要有 Content-Type,跨域的请求需要 Access-Control-Allow-Origin
// 3. 空行: 响应头和响应体之间必须有一个空行 `\r\n\r\n`
// 4. 响应体: JSON 字符串
successResponse := "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n" +
`{"ok":true, "code": 200, "data": {"username": "mock-user", "email": "mock@example.com", "level": 99}}`

// 调用 mockResponse 将伪造的响应返回给客户端
mockResponse(successResponse)
}
// 场景2:Mock一个失败的响应 (例如:服务端错误)
// 适用于测试前端在遇到服务器5xx错误时,是否能优雅地处理并给出提示。
if str.Contains(url, "/api/submit/order") {
yakit.Info("[MOCK] 拦截到订单提交请求,返回服务器错误: " + url)

errorResponse := "HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n" +
`{"ok":false, "message": "服务暂时不可用,请稍后再试"}`

mockResponse(errorResponse)
}
// 场景3:根据请求体内容进行复杂判断和Mock (例如:阻止危险操作)
// 适用于测试前端对特定输入的处理,或防止测试时产生垃圾数据。
if str.Contains(url, "/api/data/delete") && str.Contains(string(req), `"is_production":true`) {
yakit.Info("[MOCK] 检测到危险的删除操作,已拦截并返回'禁止操作'响应: " + url)

// 返回一个 403 Forbidden 响应
forbiddenResponse := "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n" +
`{"ok":false, "message": "模拟环境禁止删除线上数据!"}`
mockResponse(forbiddenResponse)
}
// 如果以上 if 条件都没有命中,函数会默认结束,Yakit将正常处理该请求(即将其发往后端服务器)。
}

我们将以一个“销毁云主机”的功能为例,进行一个小型对照实现,来展示引入的此函数的意义。本次实验的核心目标是:在不向后端发送任何真实请求、不修改后端任何数据的前提下,完整地测试一个“危险操作”的全部前端逻辑链路。**

实验

实验靶场设计

**后端:**极其简单,它只是模拟一个功能,用户单例级别的创建和销毁容器。

下面是一个后端代表逻辑图:

前端(HTMLwithJavaScript):**

实验步骤与分析

正常使用销毁测试

1、启动vulinbox,点击逻辑场景用户容器靶场,登陆用户;

2、: 点击 "Activate" 按钮。建立一个容器;

3、前端**:**获取到建立的Active 容器,显示了Deactivate 按钮;

**4、操作:**点击 "Deactivate" 按钮,销毁这个容器;

5、观察前端**:**重新回到了没有容器建立的样式;

6、分析: 我们成功测试了一次流程。但问题也随之而来:

  • 数据已“销毁”: 在真实系统中,这个主机已经没了。
  • 测试不可重复: 为了再测试一次,我需要重新部署一个主机,或者重启我的后端模拟程序。
  • 无法测试异常: 我如何测试“网络超时”对UI的影响?或者服务器返回503 Service Unavailable时,UI是否能正确恢复按钮状态?手动让后端返回503很麻烦。

Mock热加载测试

热加载代码:

mockHTTPRequest = func(isHttps, url, req, mockResponse) {
if str.Contains(url, "logic/user/container/deactivate") {

// 1. 记录意图:我们清晰地知道客户端想要做什么
yakit.Info("[MOCK] Intercepted a deactivate call to: " + url)
yakit.Info("[MOCK] No actual request will be sent to the backend.")
// 2. 模拟场景A:一个完美的成功响应
successResponse = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n" +
`{"ok":true}`

// --- 请在此处解除下面的注释,来模拟场景B ---
// serverErrorResponse = "HTTP/1.1 503 Service Unavailable\r\n\r\n"
// 3. 返回虚假响应
mockResponse(successResponse)
// mockResponse(serverErrorResponse) // 用于场景B
}
}

操作与观察:

1、在 mitm 中间人劫持中加载并启用上述热加载代码;

2、正常操作申请到容器;

3、点击销毁容器;

4、观察结果:

  • 前端UI表现: 页面触发刷新后,仍然表现为建立好了容器的情况,证明后端的容器并没有被清空,也就没有触发危险的“销毁操作”。

  • **MITM 日志:**正确捕获和打印出了 mock 信息。

结论

本次实验证明:mockHTTPRequest 不仅仅是一个攻击工具,更是一个强大的客户端行为分析与测试框架。

**1、测试隔离:**我们创建了一个安全的“沙箱”,让前端应用在其中自由运行。所有操作都在这个沙箱内完成,对外部世界(后端、数据库、Cookie)的影响为零。这解决了状态污染和危险操作的问题。

2、快速安全控制: 我们不再被动地等待后端返回特定状态,而是可以主动地、精确地为客户端注入任何我们想要的场景——无论是网络成功、服务器错误、特定业务代码错误,还是网络超时。这使得对前端鲁棒性的测试(如错误处理、UI状态恢复)变得简单和高效。

综上,mockHTTPRequest 可以让安全测试人员更松弛有度地管理后端的借口调用情况,减少一些冗杂调用,更集中在客户端或者目标功能。


本文首发于 Yak Project 公众号,阅读原文