CraftCMS RCE
Craft is a flexible, user-friendly CMS for creating custom digital experiences on the web—and beyond.
You have a ton of options when it comes to choosing a CMS. Craft is uniquely equipped to deliver high-quality, content-driven experiences to your clients and their audiences, in large part due to its blank-slate approach to content modeling.
We recently encountered a target running CraftCMS in an engagement, and discovered a Remote Code Execution vulnerability (CVE-2023-41892) that allowed us to compromise the target. While the patch is now available, all CraftCMS users are strongly encouraged to apply the additional mitigation at the end of this post to keep your instance secure.
The vulnerability
Like other content management systems, the pre-auth attack surface of CraftCMS is relatively limited. However, the \craft\controllers\ConditionsController
class quickly got our attention because its beforeAction
method seems to do something with object creation.
public function beforeAction($action): bool
{
$baseConfig = Json::decodeIfJson($this->request->getBodyParam('config'));
$config = $this->request->getBodyParam($baseConfig['name']);
$newRuleType = ArrayHelper::remove($config, 'new-rule-type');
$conditionsService = Craft::$app->getConditions();
$this->_condition = $conditionsService->createCondition($config);
Craft::configure($this->_condition, $baseConfig);
...
\yii\BaseYii::configure
\yii\base\Component::__set
\yii\BaseYii::createObject
...
After spending some time understanding the code, we confirmed that the endpoint gave us the ability to create an arbitrary object.
The codebase of CraftCMS and its dependencies contains several gadgets that can be used to escalate the object creation into something meaningful, like limitedly calling some methods:
\GuzzleHttp\Psr7\FnStream
public function __destruct()
{
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);
}
}
Or including arbitrary files:
\yii\base\BaseObject::__construct
\yii\rbac\PhpManager::init
\yii\rbac\PhpManager::load
\yii\rbac\PhpManager::loadFromFile
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
The latter seemed to be a quick win for us, as we could inject some PHP code into the CraftCMS’s log file and then include it, just like in a CTF challenge (and we can even use the @storage
variable to locate the log file in case it is placed somewhere other than the default location, which makes the exploit easier).
Unfortunately, in the case of our target, no log files are available on the server. We could leverage a PHP behavior to create temporary files, but we didn’t know the exact filename to include (the FindFirstFile
/ <
trick could not be applied here since we were dealing with a Linux server). On the other hand, including a remote file was also not possible.
That’s when we remembered an excellent research from Arseniy Sharoglazov, because the described vulnerability is identical to what we were working on. In his post, Arseniy revealed that creating an Imagick
object with the VID
scheme would result in an arbitrary file write, and the beauty of the VID
scheme is its ability to reference a file without knowing the filename. That’s all we needed.
The rest of the work was straightforward, and after some testing, we got an exploit that worked perfectly on the target environment and achieved RCE.
Recommendations
Besides applying the patch, we highly recommend all CraftCMS users to rotate the CRAFT_SECURITY_KEY
immediately. We have confirmed that knowing the key will lead to an unauthenticated RCE on a widely used CraftCMS plugin, and there may be more.