1. 项目概述:从一次“意外”的请求响应说起

最近在审计一个基于Next.js 15构建的现代Web应用时,我遇到了一个挺有意思的现象。在常规的接口测试中,我向一个看似普通的API端点发送了一个GET请求,但服务器返回的响应头里,赫然出现了 Content-Type: application/octet-stream ,而响应体则是一串难以直接阅读的二进制数据。这立刻引起了我的警觉——在React生态里,这种“非文本”的响应格式,往往指向了React Server Components(RSC)的序列化数据流。RSC是React团队为了提升服务端渲染性能而引入的架构,它允许在服务端预先渲染组件树,并将序列化后的“指令”流式传输到客户端。然而,这种序列化与反序列化的过程,如果实现不当,就可能成为攻击者撬开系统大门的缝隙。CVE-2025-55182正是这样一个存在于RSC反序列化过程中的高危漏洞。

这个漏洞的核心在于,攻击者可以构造恶意的RSC响应数据,当客户端(通常是Next.js的客户端运行时)尝试反序列化这些数据时,可能触发非预期的JavaScript代码执行。想象一下,攻击者不需要窃取你的密码,只需要诱使你点击一个链接或访问一个被植入恶意RSC载荷的页面,就可能在你浏览器中执行任意代码,窃取本地存储的敏感信息、会话令牌,甚至进行进一步的横向移动。这比传统的XSS更具隐蔽性和危害性,因为它发生在React框架的核心渲染流程中。

为了系统性地挖掘和验证这类漏洞,我选择了两款强大的工具进行组合:Yakit和Wavely。Yakit作为一个高度集成化的安全测试平台,提供了从流量代理、漏洞扫描到Payload管理的全链路能力,它的MCP(Message Control Protocol)插件架构尤其适合与自定义工具链集成。而Wavely则是一个专注于Web安全研究,特别是针对现代JavaScript框架(如React, Vue)和API(如GraphQL, RSC)的模糊测试与漏洞利用框架。它内置了对RSC协议格式的解析和变异能力,能够自动生成大量畸形的、可能触发边界条件的序列化数据。本篇文章,我将详细拆解如何利用Yakit和Wavely,从零开始构建针对CVE-2025-55182类RSC反序列化漏洞的挖掘环境、分析流量、编写POC,并分享在整个过程中踩过的坑和总结出的实战技巧。无论你是应用安全研究员、红队队员,还是对React底层安全感兴趣的前端开发者,相信都能从中获得直接的参考价值。

2. 工具链搭建与核心思路解析

工欲善其事,必先利其器。针对RSC这类相对较新的攻击面,传统的Burp Suite或ZAP虽然强大,但在协议解析和Payload生成上往往需要大量的手动工作或插件开发。我们的目标是实现一个半自动化的漏洞挖掘流程:由Wavely负责生成针对RSC协议格式的测试用例,由Yakit作为中间人代理和流量操控中枢,拦截、修改并重放测试流量,同时记录和分析服务器的异常响应。

2.1 为什么是Yakit + Wavely?

首先说说工具选型。Yakit的核心优势在于其“一体化”和“可编程性”。它内置了MITM代理、Web Fuzzer、端口扫描、漏洞检测等模块,并且通过Yaklang脚本和MCP插件,可以轻松与外部工具(如Wavely)进行深度集成。例如,我们可以编写一个Yaklang脚本,自动将从Wavely生成的测试Payload注入到经过Yakit代理的特定HTTP请求中,这比手动在Burp的Repeater里粘贴Payload要高效得多。此外,Yakit的“热加载”特性使得我们可以实时修改和调试攻击脚本,非常适合漏洞POC的开发阶段。

Wavely的选择则更具针对性。它并非一个广谱的Web扫描器,而是深度聚焦于现代前端框架和API的安全测试。它对RSC协议有原生支持,能够理解RSC响应中的特殊标记(如 M S J 等)和序列化格式。这意味着Wavely可以智能地变异这些数据结构的字段,比如尝试注入非法的组件引用、篡改属性值类型、或构造循环引用等,这些都是手动构造难以穷尽且容易出错的场景。Wavely通常以命令行工具或库的形式提供,这正好方便我们通过脚本将其集成到Yakit的流程中。

整个工作流的思路可以概括为: “流量捕获 -> 协议识别 -> 载荷生成 -> 精准投递 -> 异常监测”

  1. 流量捕获与识别 :使用Yakit的代理功能,拦截目标Next.js应用的所有流量。通过分析响应头的 Content-Type (如 text/x-component application/octet-stream )和响应体特征,自动识别出哪些请求/响应属于RSC通信。
  2. 载荷生成 :将识别出的“干净”RSC响应样本提供给Wavely。Wavely基于其内置的RSC模型和变异规则,生成数百甚至上千个潜在的恶意变体(即Fuzzing Payload)。
  3. 精准投递 :通过Yakit的Fuzzer模块或自定义Yaklang脚本,将这些Payload逐一替换到原始请求的对应位置(可能是特定的URL参数、POST body中的某个字段,或者直接替换整个RSC响应体),并向目标服务器重放请求。
  4. 异常监测 :密切监控服务器的响应。漏洞触发的迹象可能包括:服务器返回5xx错误、响应时间异常延迟、响应体结构崩坏、或者在客户端浏览器控制台出现特定的反序列化错误(如 TypeError , RangeError )。Yakit可以配置匹配规则来高亮显示这些异常响应。

2.2 环境准备与初始配置

接下来是具体的环境搭建步骤。假设你已经在本地或测试服务器上部署了一个存在潜在风险的Next.js 15应用(可以是故意留有漏洞的测试靶场,如一些CTF题目或安全实验室项目)。

Yakit的安装与代理配置:

  1. 从Yakit官网下载最新版本的客户端。安装过程很简单,一路下一步即可。
  2. 启动Yakit,首先需要启动核心引擎。点击主界面左下角的引擎连接状态,按照提示启动本地引擎。
  3. 配置MITM代理。进入“MITM”模块,点击“启动”按钮。Yakit会默认监听本地的8083端口(例如 127.0.0.1:8083 )。你需要将系统或浏览器的HTTP/HTTPS代理设置为这个地址和端口。

    注意 :对于HTTPS流量,需要安装Yakit的根证书到系统的受信任根证书颁发机构。Yakit会提供详细的安装指引。这是中间人攻击能够解密HTTPS流量的关键步骤,务必正确操作,否则只能看到加密的乱码。

  4. 配置过滤规则(可选但推荐)。在MITM设置中,可以添加“过滤主机”,只拦截目标测试域名的流量,避免被无关的网页请求刷屏。

Wavely的安装: Wavely通常是一个Node.js工具或Python库。这里以Node.js环境为例。

# 假设使用npm进行安装
npm install -g wavely
# 或者从GitHub克隆其仓库
git clone https://github.com/某个安全研究组织/wavely.git
cd wavely
npm install

安装后,可以通过命令行 wavely --help 查看其基本用法。我们需要重点关注的是其Fuzzing相关子命令,例如 wavely fuzz rsc

浏览器配置: 将你的测试浏览器(推荐Chrome或Firefox)的代理设置为Yakit的代理地址( 127.0.0.1:8083 )。之后,访问你的目标Next.js应用,确保所有流量都能在Yakit的“HTTP History”标签页中看到。

3. RSC流量特征分析与漏洞原理深潜

在开始Fuzzing之前,我们必须先理解我们要攻击的对象。RSC的通信并非传统的RESTful JSON,而是一种高效的二进制序列化格式(在开发模式下也可能是可读性更强的文本格式)。

3.1 识别RSC流量

在Yakit的HTTP历史记录中,当你浏览一个使用RSC的Next.js页面时,可能会看到类似以下的请求:

  • 请求 GET /_next/static/chunks/app/your-page.rsc?__flight__=1 POST /_next/action 等。关键特征是可能带有 __flight__ 查询参数,或者请求头包含 RSC: 1 Next-Action: 1
  • 响应 :响应头 Content-Type: text/x-component application/octet-stream 。响应体在开发者工具中查看可能是乱码,但通过Yakit的“十六进制”视图或将其保存为文件后分析,可以发现其结构。

一个简化的、人类可读的RSC响应示例(开发模式或经过解码)可能如下:

M1:["$@1",["$L1","div",null,{"className":"container","children":[["$L2","h1",null,{"children":"Hello Server Component"}],["$L3","p",null,{"children":"$L4"}]]}]]
S2:["message","This is from server"]
J0:["$@1"]

这里, M S J 是RSC流中的不同行类型,分别代表“模块”、“字符串”和“JSON”。数字是引用ID。反序列化器需要根据这些指令重建React组件树。CVE-2025-55182类的漏洞往往就潜藏在解析这些行、处理引用ID或评估特定指令的逻辑中。

3.2 CVE-2025-55182漏洞原理推测与攻击面分析

虽然CVE-2025-55182的具体细节在公开时可能已被修复,但基于React RSC的常见反序列化漏洞模式,我们可以进行合理的推测和分析。这类漏洞通常出现在以下几个环节:

  1. 自定义序列化/反序列化器的实现缺陷 :虽然React核心库提供了默认的序列化器,但框架或用户可能允许注入自定义的序列化逻辑(例如,通过 experimental_serialize 等API)。如果这些自定义逻辑对输入验证不严,攻击者可能注入包含特殊属性(如 __proto__ constructor )的对象,导致原型污染。
  2. “引用”解析逻辑错误 :RSC流中大量使用引用(如 $L1 , $@1 )。反序列化器需要维护一个引用映射表。如果攻击者能够构造一个循环引用(例如,对象A引用B,B又引用A)或一个指向未定义引用的ID,可能触发堆栈溢出或未定义行为,进而可能导致内存损坏或绕过某些安全检查。
  3. 指令评估时的副作用 :某些RSC指令可能被设计为在反序列化时执行一些轻量级的操作。如果评估这些指令的JavaScript引擎(如V8)存在某些边界情况,畸形输入可能导致引擎抛出异常,而异常处理不当可能泄露信息或影响应用状态。
  4. 与服务器端Action的混淆 :Next.js的Server Actions也使用类似的序列化机制。如果RSC反序列化与Action反序列化共享了某些不安全的代码路径,攻击者可能通过RSC流来触发本应在Action中才被执行的敏感操作。

我们的Fuzzing策略就是要针对这些攻击面。Wavely的强大之处在于,它可以自动生成覆盖这些情况的测试用例:

  • 生成深度嵌套、循环引用的对象图。
  • 在字符串或属性名中插入特殊字符和Unicode字符。
  • 尝试使用非法的类型标识符。
  • 故意破坏流的完整性(如截断、重复、错位)。

4. 构建自动化Fuzzing工作流与POC编写

有了理论基础和工具,现在我们将它们串联起来,形成一个自动化的测试流程。

4.1 步骤一:捕获基准RSC流量

首先,我们需要一个“正常”的RSC响应作为Fuzzing的种子。

  1. 在配置好代理的浏览器中,正常访问目标Next.js应用中一个明确使用了RSC的页面。
  2. 在Yakit的“HTTP历史”中,找到那条返回RSC格式的请求/响应对。右键点击该请求,选择“发送到Web Fuzzer”。
  3. 在Web Fuzzer中,你现在可以看到原始的请求和响应。将响应体完整地复制出来,保存为一个文件,例如 seed.rsc.bin (如果是二进制)或 seed.rsc.txt

4.2 步骤二:使用Wavely生成Fuzzing Payload

接下来,我们使用Wavely对这个种子进行变异。

# 假设Wavely提供了命令行fuzz工具
wavely fuzz rsc -i seed.rsc.txt -o payloads/ -n 1000 --strategy deep

这个命令告诉Wavely:读取 seed.rsc.txt 作为输入种子,使用“深度”变异策略,生成1000个变异Payload,并输出到 payloads/ 目录下,每个Payload一个文件。

关键参数解析:

  • -n 1000 :生成数量。对于初步测试,500-1000个是合理的起点。太多可能导致测试时间过长,太少可能覆盖不全。
  • --strategy deep :变异策略。“deep”策略会尝试更激进的结构性变异,如修改嵌套层次、引用关系等,更适合挖掘复杂的逻辑漏洞。此外可能还有“quick”(浅层变异,如修改值)、“syntax”(破坏语法)等策略,可以组合使用。

4.3 步骤三:在Yakit中集成并投递Payload

这是最核心的一步,我们需要在Yakit中自动化地加载这些Payload并发送给目标。

方法A:使用Yakit Web Fuzzer的“Payload”模块

  1. 回到刚才发送到Web Fuzzer的那个标签页。
  2. 在请求体(或URL参数,取决于漏洞点)中,选中你认为可能是反序列化入口的数据部分(例如整个响应体,或者某个特定的参数值)。点击鼠标右键,选择“添加Payload位置”。
  3. 在右侧的“Payload”配置面板,选择“文件”类型。点击“选择文件”,然后选择Wavely生成的所有Payload文件(可能需要先将它们合并到一个大文件中,或使用目录选择)。Yakit支持从目录加载所有文件。
  4. 配置并发线程数和延迟,以避免对目标服务器造成过大压力或触发WAF。建议初始设置为线程数5,延迟200毫秒。
  5. 点击“开始执行”。Yakit会自动用每个Payload替换你标记的位置,并发送请求。

方法B:编写Yaklang脚本实现更精细的控制 对于更复杂的场景,比如需要根据响应动态修改Payload,或者需要处理多步交互,编写脚本更灵活。

// 示例:一个简单的Yaklang脚本,用于读取Payload文件并发送请求
target = “http://target-app.com/_next/action”
payloads = file.ReadDir(“./payloads/“) // 读取所有Payload文件

for p in payloads {
    payloadContent = file.ReadFile(p)
    rsp, req = poc.HTTP(
        poc.Post(target),
        poc.WithBody(payloadContent), // 将Payload作为POST body
        poc.WithHeader(“Content-Type”, “text/x-component”), // 关键:模拟RSC响应
        poc.WithHeader(“RSC”, “1”)
    )
    
    // 检查异常响应
    if rsp.StatusCode >= 500 {
        println(“发现潜在漏洞!Payload文件: “, p)
        println(“响应状态码: “, rsp.StatusCode)
        println(“响应摘要: “, string(rsp.RawPacket)[:500]) // 打印前500字节
        // 可以将详细信息保存到文件或数据库
        db.Save(“/tmp/vuln_findings.txt”, sprintf(“File:%s, Code:%d\n”, p, rsp.StatusCode))
    }
    sleep(0.1) // 延迟100毫秒
}

这个脚本会遍历 payloads 目录下的所有文件,将其内容作为POST body发送到目标Action端点,并检查服务器是否返回5xx错误。你可以在此基础上增加更复杂的检测逻辑,比如检查响应体中是否包含特定的错误信息、响应时间是否超长等。

4.4 步骤四:分析结果与提炼POC

Fuzzing执行完毕后,我们需要在Yakit的Fuzzer结果列表或脚本日志中筛选出“异常”的响应。异常不仅仅是服务器错误(5xx),还包括:

  • 状态码异常 :如404(可能路径被破坏)、400(Bad Request,但原始请求是好的)。
  • 响应长度异常 :与基准响应长度差异巨大(极短或极长)。
  • 响应内容异常 :包含堆栈跟踪信息、V8引擎错误(如 RangeError: Invalid array length )、或React特有的错误信息。
  • 响应时间异常 :某个请求的处理时间远超其他请求,可能触发了死循环或高复杂度计算。

找到一个稳定的、可复现的异常响应后,我们就得到了一个初步的“崩溃POC”。下一步是将其简化和提炼。

提炼POC的技巧:

  1. 二分法定位 :Wavely生成的Payload可能很复杂。使用二分法:将导致异常的Payload分成两半,分别测试,看哪一半能触发异常。重复这个过程,直到找到最小的、必需的恶意数据片段。
  2. 理解触发点 :分析这个最小Payload。它可能是一个畸形的JSON结构、一个特殊的引用ID、一个包含特定字符的字符串。尝试理解它为什么会导致错误。是造成了递归解析?还是赋值给了非法属性?
  3. 构建可读POC :将最小Payload以清晰的形式呈现。例如,如果漏洞是通过一个特殊的对象触发的,可以写成:
    // CVE-2025-55182 疑似POC (简化示例)
    const maliciousRSCPayload = `
    M1:["$@1", {"__proto__": {"polluted": "yes"}, "constructor": {"prototype": {"isAdmin": true}}}]
    J0:["$@1"]
    `;
    
    并附上发送该Payload的HTTP请求示例。
  4. 验证影响 :尝试利用这个POC做更多事情。例如,如果触发了原型污染,检查是否能在客户端全局对象上注入属性。如果触发了异常,看是否能导致信息泄露(如错误信息中包含路径或代码片段)。

5. 实战中的疑难杂症与排查技巧

在实际操作中,你几乎一定会遇到各种问题。下面是我在多次测试中总结的一些常见坑点和解决方法。

5.1 WAF与速率限制规避

现代应用通常部署有WAF(Web应用防火墙)或速率限制。我们的Fuzzing请求很容易被拦截。

  • 症状 :大量请求返回403、429状态码,或被直接断开连接。
  • 对策
    1. 降低频率 :在Yakit Fuzzer或脚本中大幅增加请求间隔(如1秒以上)。
    2. 随机化User-Agent和IP头 :使用Yakit的“Payload”功能,为每个请求随机选择一个常见的浏览器UA。
    3. 使用代理池 (如果条件允许):通过不同的出口IP发送请求。
    4. 模拟正常流量模式 :在Fuzzing序列中插入一些完全正常的请求,打乱攻击节奏。
    5. 研究WAF规则 :如果可能,先手动测试一些简单的恶意负载,观察WAF的拦截模式,然后调整Payload避开常见的关键字检测。

5.2 处理二进制与编码问题

RSC响应可能是二进制格式,直接作为文本处理会出错。

  • 症状 :Wavely无法解析种子文件,或者Yakit发送的Payload导致服务器立即返回400错误。
  • 对策
    1. 确认格式 :先用十六进制编辑器或 xxd 命令查看种子文件。如果开头是二进制数据(非可打印字符),则需要让Wavely以二进制模式处理,或者先找到一种方式将其解码为文本格式(有时可能是经过gzip压缩的)。
    2. Yakit中的编码 :在Web Fuzzer中,如果原始请求是二进制的,确保在“添加Payload位置”时,选中“原始”模式,而不是文本模式。在Yaklang脚本中,使用 poc.WithBody(payloadContent) 会自动处理字节数据。
    3. Content-Type :确保发送请求时, Content-Type 头与原始响应一致(如 application/octet-stream ),这是服务器正确解析的前提。

5.3 区分客户端与服务器端漏洞

RSC漏洞可能发生在服务端(处理RSC请求时)或客户端(反序列化RSC响应时)。定位至关重要。

  • 症状 :服务器返回了200 OK,但Payload似乎也“生效”了(比如后续请求行为异常)。
  • 排查方法
    1. 服务端错误 :如果服务器返回5xx、4xx,或者响应体包含服务端堆栈跟踪,漏洞很可能在服务端。
    2. 客户端错误 :如果服务器返回200,但响应体是一个“看似正常”的RSC流。你需要在一个受控的、安装了调试工具的浏览器中,使用这个恶意RSC响应来渲染页面。打开浏览器开发者工具的Console,观察是否有JavaScript错误抛出。这是验证客户端反序列化漏洞的关键步骤。
    3. 双向测试 :设计Payload,使其在服务端处理时触发一种异常(如日志记录),在客户端触发另一种异常(如JS错误),从而帮助区分。

5.4 提升Fuzzing效率与深度

盲目地生成海量Payload效率低下。

  • 策略组合 :不要只依赖一种Fuzzing策略。先进行一轮“quick”策略的浅层Fuzzing,快速覆盖表面错误。然后针对那些返回了有趣状态码(如500)的请求,用其对应的Payload作为新种子,进行“deep”策略的深度Fuzzing。
  • 代码覆盖引导 (如果条件允许):如果目标应用是开源的,可以尝试在测试环境中运行代码覆盖率工具(如Istanbul),然后分析Fuzzing过程中哪些代码分支被执行了,重点变异那些能触发新分支的输入。
  • 关注边界值 :手动构造一些边界情况Payload作为种子输入给Wavely,比如超长的字符串、极大/极小的数字、特殊的浮点数(NaN, Infinity)。

6. 从Fuzzing到稳定利用:编写健壮的漏洞验证脚本

一个优秀的POC(Proof of Concept)脚本不仅能触发漏洞,还应能自动验证漏洞的影响,并给出明确的成功/失败指示。下面我们基于假设的漏洞场景,编写一个更健壮的Yaklang验证脚本。

假设我们通过Fuzzing发现,当RSC流中包含一个特定结构的循环引用时,客户端React会在反序列化时抛出一个 TypeError ,并且错误信息中会泄露一个内部文件路径。

// CVE-2025-55182 客户端信息泄露POC验证脚本
targetUrl = “http://vulnerable-app.com/api/rsc-endpoint“
leakIndicator = “/internal/path/to/source“

// 构造恶意RSC Payload (基于分析的最小化Payload)
maliciousPayload = `
M1:["$@1", ["$L1", "div", null, {"ref": ["$@1"]}]]
J0:["$@1"]
`

// 发送请求
rsp, req = poc.HTTP(
    poc.Post(targetUrl),
    poc.WithBody(maliciousPayload),
    poc.WithHeader(“Content-Type”, “text/x-component”),
    poc.WithHeader(“Accept”, “text/x-component”),
    poc.WithTimeout(10), // 设置超时
)

// 检查1: 服务器是否正常响应(200 OK是客户端漏洞的前提)
if rsp.StatusCode != 200 {
    die(sprintf(“服务器响应异常: %d,可能不是客户端漏洞或请求格式错误”, rsp.StatusCode))
}

// 检查2: 响应体是否包含预期的泄漏信息
rspBody = string(rsp.RawPacket)
// 注意:实际泄漏可能出现在响应体的任何位置,这里简单用字符串包含判断
if str.Contains(rspBody, leakIndicator) {
    println(“[SUCCESS] 漏洞验证成功!发现信息泄露。”)
    println(“泄漏路径: “, leakIndicator)
    // 可以进一步解析出完整的泄漏信息
    // 使用正则匹配更精确的模式
    re, _ = re.Compile(`(/[^\s<>”‘]*\.(jsx?|tsx?))`)
    matches = re.FindAllString(rspBody, -1)
    if len(matches) > 0 {
        println(“提取到的潜在内部文件路径:”)
        for m in matches {
            println(“  - “, m)
        }
    }
} else {
    // 检查3: 响应体是否包含其他错误特征(如React序列化错误)
    if str.Contains(rspBody, “TypeError”) || str.Contains(rspBody, “ReactFlight”) || str.Contains(rspBody, “serialize”) {
        println(“[PARTIAL] 触发了客户端反序列化错误,但未检测到预设的信息泄露。可能需要调整Payload或检查错误信息格式。”)
        println(“响应摘要:”, str.Substr(rspBody, 0, 1000))
    } else {
        println(“[FAILURE] 请求成功,但未检测到明显的漏洞触发迹象。”)
    }
}

这个脚本做了几件事:

  1. 发送精准Payload :使用我们提炼出的最小恶意负载。
  2. 基础健康检查 :确认服务器返回200,排除服务端错误。
  3. 核心验证 :检查响应中是否包含预期的泄漏信息(这里是文件路径)。
  4. 降级判断 :如果没有发现预期泄漏,但发现了React相关的错误信息,则提示“部分成功”,可能需要进一步分析。
  5. 增强信息提取 :使用正则表达式尝试提取所有可能的内源文件路径,提高发现意外信息泄露的能力。

编写POC的注意事项:

  • 无害化 :确保你的POC只用于验证漏洞存在,不会对目标系统造成破坏(如删除数据、篡改内容)。本例中的信息泄露POC是相对安全的。
  • 明确性 :POC的输出应该非常清晰,是成功就是成功,并给出证据(如匹配到的字符串);是失败就是失败,并尽可能说明原因。
  • 可移植性 :尽量不依赖绝对路径或特定环境配置。将必要的参数(如目标URL、检测标志)作为变量放在脚本开头。
  • 错误处理 :加入超时、网络错误等处理,避免脚本因意外情况而崩溃。

通过这样一套从流量分析、工具链搭建、自动化Fuzzing到精准POC编写的完整流程,我们就能系统化地应对像CVE-2025-55182这类新兴的、针对特定框架协议的深层安全威胁。这套方法不仅适用于React RSC,其核心思想——协议理解、工具集成、自动化测试、结果分析——可以迁移到GraphQL、gRPC-Web、tRPC等各种现代Web API的安全测试中。安全研究是一个持续对抗和学习的过

更多推荐