CVE-2023-41892

Crafting Chaos: The Unauthenticated RCE in Craft CMS (CVE-2023-41892)

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 7, 2026·6 min read

Executive Summary (TL;DR)

Craft CMS developers put the cart before the horse—specifically, they executed object instantiation logic *before* checking if the user was allowed to be there. This 10.0 CVSS vulnerability allows unauthenticated attackers to turn a Craft CMS server into a shell playground using a simple JSON payload.

A critical, unauthenticated Remote Code Execution (RCE) vulnerability in Craft CMS caused by a logic flow error in controller lifecycle methods. Attackers can execute arbitrary PHP code via a crafted JSON payload.

The Hook: 10.0 on the Richter Scale

In the world of vulnerability management, a CVSS score of 10.0 is the unicorn of nightmares. It means no authentication, no user interaction, low complexity, and total system compromise. CVE-2023-41892 is exactly that. It affects Craft CMS, a popular content management system built on the Yii2 framework.

Usually, RCEs require a bit of gymnastics—maybe you need a low-privileged account, or perhaps you need to trick an admin into clicking a link. Not here. This vulnerability is the digital equivalent of a bank vault where the door is locked, but the back wall is missing.

What makes this particularly juicy for researchers (and terrifying for admins) is the component involved: the ConditionsController. This controller is designed to handle dynamic rule sets for content, but due to a catastrophic ordering error, it became a gateway for arbitrary object instantiation. If your server runs Craft CMS between versions 4.0.0-RC1 and 4.4.14, you are likely exposed to immediate remote compromise.

The Flaw: Logic Before Security

To understand this bug, you have to understand the Yii2 framework lifecycle. Controllers in Yii2 have a method called beforeAction($action). As the name suggests, this runs before the main action of the controller. It's the standard place to put access controls, CSRF checks, and authentication logic via parent::beforeAction($action).

Basic security engineering dictates that you check credentials at the door before letting someone order a drink. However, in the ConditionsController (and a few others), the developers decided to process user input before calling the parent method. They essentially let the user walk in, configure the room, and instantiate objects, and then asked to see their ID.

Specifically, the code extracted a config parameter from the POST request and passed it to Craft::createObject(). This function is powerful—it can create any class available in the PHP autoload path. By the time the code finally called parent::beforeAction() to check for CSRF or authentication, the malicious object had already been created, and the damage was done.

The Code: The Smoking Gun

Let's look at the PHP code that caused this mess. In the vulnerable version of ConditionsController.php, the logic flow looked something like this:

// VULNERABLE CODE
public function beforeAction($action): bool
{
    // 1. Grab untrusted input immediately
    $config = $this->request->getBodyParam('config');
 
    // 2. Process it (instantiate objects)
    if ($config) {
        $condition = Craft::createObject($config);
    }
 
    // 3. FINALLY check if the user is allowed to be here
    return parent::beforeAction($action);
}

See the problem? The createObject call happens on line 2, but the security check (parent::beforeAction) happens on line 3. An attacker can trigger whatever side effects exist in createObject before the server rejects the request.

The fix was embarrassingly simple: move the security check to the top.

// PATCHED CODE (v4.4.15)
public function beforeAction($action): bool
{
    // 1. Security check FIRST
    if (!parent::beforeAction($action)) {
        return false;
    }
 
    // 2. Then process input
    $config = $this->request->getBodyParam('config');
    // ...
}

This is a textbook example of why "Order of Operations" isn't just a math concept—it's a security requirement.

The Exploit: Weaponizing Guzzle

So we can create objects. How do we get from "creating an object" to "Remote Code Execution"? We need a gadget. In the PHP world, a gadget is a piece of existing code that does something useful for an attacker when misused. Craft CMS includes the Guzzle HTTP library, which contains a beautiful gadget: \GuzzleHttp\Psr7\FnStream.

This class has a __destruct() magic method. In PHP, __destruct() is called automatically when an object is destroyed (like when a script finishes execution). The FnStream class allows you to define a custom function that runs on destruction. It's practically designed for RCE if you can control the constructor arguments.

The attack chain looks like this:

  1. Target: Send a POST request to index.php?action=conditions/render.
  2. Payload: Send a JSON config that tells Craft to create a FnStream object.
  3. Trigger: Define the _fn_close property as phpinfo (or system, exec, etc.).

Here is the actual JSON payload used in the wild:

{
  "name": "test[userCondition]",
  "as xyz": {
    "class": "\\GuzzleHttp\\Psr7\\FnStream",
    "__construct()": [{"close": null}],
    "_fn_close": "system",
    "command": "id"
  }
}

Wait, where does the command go? The actual Nuclei payload often uses phpinfo just to prove the point because passing arguments to system via this specific gadget requires careful alignment of the array properties. But once phpinfo pops, you know you own the box.

The Impact: Total Ownership

Because this is unauthenticated RCE, the impact is absolute. An attacker can:

  1. Read Files: Dump the .env file to steal database credentials and the CRAFT_SECURITY_KEY.
  2. Modify Data: Deface the website or inject malicious JavaScript into CMS content to target visitors.
  3. Pivot: Use the compromised server as a jump box to attack internal networks.

The CRAFT_SECURITY_KEY leak is particularly dangerous. Even if you patch the RCE, if an attacker stole this key, they might be able to forge session cookies or sign malicious requests, maintaining persistence long after the initial hole is plugged.

The Fix: More Than Just an Update

The primary fix is to upgrade to Craft CMS 4.4.15 or higher. This reorders the beforeAction calls and introduces a new cleanseConfig method that strips out dangerous keys like on [event] and as [behavior] from user input, killing the injection vector entirely.

However, patching is not enough. You must assume that if your server was exposed, it has been compromised. The standard remediation protocol applies:

  • Rotate Secrets: Generate a new CRAFT_SECURITY_KEY in your .env file immediately.
  • Check Persistence: Look for unknown admin accounts, web shells in /public, or strange cron jobs.
  • Verify Files: Use git status or a checksum tool to ensure core CMS files haven't been modified.

Don't just lock the door after the thief has left—change the locks.

Technical Appendix

CVSS Score
10.0/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L
EPSS Probability
93.94%
Top 99% most exploited

Affected Systems

Craft CMS

Affected Versions Detail

Product
Affected Versions
Fixed Version
Craft CMS
Pixel & Tonic
>= 4.0.0-RC1, <= 4.4.144.4.15
AttributeDetail
CWE IDCWE-94 (Code Injection)
CVSS v3.110.0 (Critical)
Attack VectorNetwork (Unauthenticated)
EPSS Score93.94%
Exploit StatusActive / Weaponized
ImpactRemote Code Execution
CWE-94
Code Injection

Improper Control of Generation of Code ('Code Injection')

Vulnerability Timeline

Initial fix committed to repository
2023-06-27
Craft CMS 4.4.15 Released (Patch Available)
2023-07-03
CVE-2023-41892 Published
2023-09-13
Metasploit Module Released
2023-12-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.