工程实践:Mock 热加载实现无污染客户端测试
在传统的渗透测试中,我们常常在客户端与服务器之间架设窃听设备,我们拦截、观察、篡改真实的通信。这种模式有效,但存在固有缺陷:
• 状态污染 (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。 -
检查请求头中是否包含特定的
Authorization或Cookie。 -
检查请求体(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 公众号,阅读原文。
