React2Shell
First Day
https://github.com/ejpir/CVE-2025-55182-poc
这是第一天给出来的POC,一开始也没说是AI写的,等到后来才在README里面说是AI写的。
在这里给了什么关键函数decodeAction。
跟进分析了半天,触发条件都很奇怪。需要用户主动使用'use server' 暴露危险的函数,那这种写法也太愚蠢了。所以一开始评估这个漏洞危险不高。

Second Day
第二天醒来的时候(醒的有点晚),无条件RCE的payload已经满天飞了。
分析
https://github.com/kavienanj/CVE-2025-55182/blob/main/01-background.md
回到这个漏洞本身吧
1 | POST / HTTP/1.1 |
Prototype Pollution
1 | > ({})['__proto__']['new'] = 123 |
但是这里不像这样,这里是没有赋值的。
1 | 1. 传统的原型污染 (The "Polluter") |
1 | 结构通常如下: |
比如这里 "then":"$1:__proto__:then", 因为 1的内容是"$@0", "$@<ID>"通常是表示一个promise,所有他有then属性。
一些WAF的绕过
- 主要是一个unicode编码。
- 引用的位置可以变换
- 超大数据包
1 | POST / HTTP/1.1 |
安全的探测获取版本
先判断是不是next的服务,然后遍历js,匹配对应的版本。
u, err := url.Parse(URL)
if err != nil {
fmt.Printf("解析 URL 失败: %v\n", err)
return false
}
// 2. 获取 Pathname
pathname := u.Path
if pathname == "" {
URL = URL + "/_next"
}
if pathname == "/" {
URL = URL + "_next"
}
client := &http.Client{
Timeout: 5 * time.Second,
// Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
}
req, err := http.NewRequest("GET", URL, nil)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
return false
}
// 模拟浏览器 UA,防止被某些 WAF 拦截
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
fmt.Printf("请求失败: %v\n", err)
return false
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false
}
bodyString := string(bodyBytes)
// 识别逻辑
isNext := false
detectedInfo := []string{}
// 1. 检查 HTTP Header (X-Powered-By)
poweredBy := resp.Header.Get("X-Powered-By")
if strings.Contains(strings.ToLower(poweredBy), "next.js") {
isNext = true
detectedInfo = append(detectedInfo, "Header: "+poweredBy)
}
// 2. 检查 HTML 源码中的 __NEXT_DATA__
if strings.Contains(bodyString, "__NEXT_DATA__") || strings.Contains(bodyString, "/_next/static/") {
isNext = true
detectedInfo = append(detectedInfo, "Header: "+poweredBy)
}
if isNext {
// 1. 定义版本匹配的正则
// 覆盖两种情况:
// A: window.next={version:"15.4.6", ...} -> 匹配 version:"..."
// B: t.version="12.2.5" -> 匹配 .version="..."
reJsVersion := regexp.MustCompile(`window\.next\s*=\s*\{.*?version\s*:\s*"([^"]+)"`)
// 2. 提取 script src
reScriptSrc := regexp.MustCompile(`<script[^>]+src=["']([^"']+)["']`)
scriptMatches := reScriptSrc.FindAllStringSubmatch(bodyString, -1)
foundVersion := ""
// 3. 遍历找到的 JS 链接
for _, match := range scriptMatches {
src := match[1]
// 优化:只访问 Next.js 的静态资源文件 (/_next/static/),忽略第三方统计脚本等
if !strings.Contains(src, "/_next/static/") {
continue
}
// 4. URL 拼接处理 (处理相对路径)
jsURL := src
if strings.HasPrefix(src, "/") {
// 既然 Check 函数里已经 parse 过 URL,这里最好重新 parse 一下 base URL
u, _ := url.Parse(URL)
jsURL = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, src)
} else if !strings.HasPrefix(src, "http") {
// 简单的相对路径处理
u, _ := url.Parse(URL)
// 去掉 path,只留 host
jsURL = fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, src)
}
// 5. 请求 JS 文件内容
// 创建短超时的 Client,避免卡死
jsClient := &http.Client{Timeout: 5 * time.Second}
fmt.Println("js url: ", jsURL)
jsResp, err := jsClient.Get(jsURL)
if err != nil {
continue
}
jsBodyBytes, err := ioutil.ReadAll(jsResp.Body)
jsResp.Body.Close()
if err != nil {
continue
}
jsContent := string(jsBodyBytes)
// 6. 在 JS 内容中匹配版本
vMatch := reJsVersion.FindStringSubmatch(jsContent)
if len(vMatch) > 1 {
foundVersion = vMatch[1]
detectedInfo = append(detectedInfo, "Version: "+foundVersion)
// detectedInfo = append(detectedInfo, "Source: "+src) // 记录是从哪个js发现的
break // 找到一个版本号就停止,节省时间
}
}
fmt.Println(detectedInfo)
// 之前保留的 BuildId 逻辑(可选,建议保留作为补充)
//reBuildId := regexp.MustCompile(`"buildId":"(.*?)"`)
//matches := reBuildId.FindStringSubmatch(bodyString)
//if len(matches) > 1 {
// detectedInfo = append(detectedInfo, "BuildId: "+matches[1])
//}
// 构造返回结果
result := d.info
result.Response = strings.Join(detectedInfo, " | ")
result.Request = URL
if result.Response == "" {
result.Response = "Next.js Detected (No explicit version found in JS files)"
}
2025-12-12更新
出了一个dos漏洞,看了下poc,和我之前调试原理一模一样,flight协议可以使用$、$@互相引用chunk,我用了两个chunk互相引用形成了死循环,poc直接引用自身导致死循环…..就这样擦肩而过….
又重新分析了一下,那些说什么用了vite什么也受影响的都是在瞎扯,搭建环境真的去调试验证才是真道理。
这里面关键入口就是decodeReply。
以后都用rwsdk调试,不知道调试next为什么那么麻烦,rwsdk打印调用栈非常清晰。
除了最后命令执行不了。解释执行的worker就已经限制死了。Code generation from strings disallowed for this context
真正去调试分析,验证了,才有真的发言权。其他都是扯淡。