-
Notifications
You must be signed in to change notification settings - Fork 13.9k
/
file_dropper.rb
309 lines (272 loc) · 9.44 KB
/
file_dropper.rb
1
2
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# -*- coding: binary -*-
module Msf
module Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
stdapi_fs_delete_dir
stdapi_fs_delete_file
stdapi_fs_getwd
stdapi_fs_stat
]
}
}
)
)
self.needs_cleanup = true
@dropped_files = []
@dropped_dirs = []
register_advanced_options(
[
OptInt.new('FileDropperDelay', [false, 'Delay in seconds before attempting cleanup']),
OptBool.new('AllowNoCleanup', [false, 'Allow exploitation without the possibility of cleaning up files'])
])
end
# Record file as needing to be cleaned up
#
# @param files [Array<String>] List of paths on the target that should
# be deleted during cleanup. Each filename should be either a full
# path or relative to the current working directory of the session
# (not necessarily the same as the cwd of the server we're
# exploiting).
# @return [void]
def register_files_for_cleanup(*files)
@dropped_files += files.map(&:dup)
end
def allow_no_cleanup
datastore['AllowNoCleanup']
end
# Record directory as needing to be cleaned up
#
# @param dirs [Array<String>] List of paths on the target that should
# be deleted during cleanup. Each directory should be either a full
# path or relative to the current working directory of the session
# (not necessarily the same as the cwd of the server we're
# exploiting).
# @return [void]
def register_dirs_for_cleanup(*dirs)
@dropped_dirs += dirs.map(&:dup)
end
# Singular versions
alias register_file_for_cleanup register_files_for_cleanup
alias register_dir_for_cleanup register_dirs_for_cleanup
# When a new session is created, attempt to delete any paths that the
# exploit created.
#
# @param (see Msf::Exploit#on_new_session)
# @return [void]
def on_new_session(session)
super
if session.type == 'meterpreter'
session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
end
if @dropped_files.empty? && @dropped_dirs.empty?
return
end
@dropped_files.delete_if do |file|
exists_before = file_dropper_exist?(session, file)
if file_dropper_delete_file(session, file)
file_dropper_deleted?(session, file, exists_before)
end
end
@dropped_dirs.delete_if do |dir|
if file_dropper_check_cwd?(session, dir)
print_warning("Attempting to delete working directory #{dir}")
end
exists_before = file_dropper_exist?(session, dir)
if file_dropper_delete_dir(session, dir)
file_dropper_deleted?(session, dir, exists_before)
end
end
end
# While the exploit cleanup do a last attempt to delete any paths created
# if there is a file_rm/dir_rm method available. Warn the user if any paths were
# not cleaned up.
#
# @see Msf::Exploit#cleanup
# @see Msf::Post::File#file_rm
# @see Msf::Post::File#dir_rm
def cleanup
super
if @dropped_files.empty? && @dropped_dirs.empty?
return
end
delay = datastore['FileDropperDelay']
if delay
print_status("Waiting #{delay}s before cleanup...")
sleep(delay)
end
# Check if file_rm method is available (local exploit, mixin support, module support)
if respond_to?(:file_rm)
@dropped_files.delete_if do |file|
begin
file_rm(file)
rescue ::Exception => e
vprint_error("Failed to delete #{file}: #{e}")
elog("Failed to delete #{file}", error: e)
end
end
end
# Check if dir_rm method is available (local exploit, mixin support, module support)
if respond_to?(:dir_rm)
@dropped_dirs.delete_if do |dir|
if respond_to?(:pwd) && pwd.include?(dir)
print_warning("Attempting to delete working directory #{dir}")
end
begin
dir_rm(dir)
rescue ::Exception => e
vprint_error("Failed to delete #{dir}: #{e}")
elog("Failed to delete #{dir}", error: e)
end
end
end
# We don't know for sure if paths have been deleted, so always warn about it to the user
(@dropped_files + @dropped_dirs).each do |p|
print_warning("This exploit may require manual cleanup of '#{p}' on the target")
end
end
private
# See if +path+ exists on the remote system and is a regular file or directory
#
# @param path [String] Remote pathname to check
# @return [Boolean] True if the path exists, otherwise false.
def file_dropper_exist?(session, path)
if session.platform == 'windows'
normalized = file_dropper_win_path(path)
else
normalized = path
end
if session.type == 'meterpreter'
stat = session.fs.file.stat(normalized) rescue nil
return false unless stat
stat.file? || stat.directory?
else
if session.platform == 'windows'
f = session.shell_command_token("cmd.exe /C IF exist \"#{normalized}\" ( echo true )")
else
f = session.shell_command_token("test -f \"#{normalized}\" -o -d \"#{normalized}\" && echo true")
end
return false if f.nil? || f.empty?
return false unless f =~ /true/
true
end
end
# Sends a file deletion command to the remote +session+
#
# @param [String] file The file to delete
# @return [Boolean] True if the delete command has been executed in the remote machine, otherwise false.
def file_dropper_delete_file(session, file)
win_file = file_dropper_win_path(file)
if session.type == 'meterpreter'
begin
# Meterpreter should do this automatically as part of
# fs.file.rm(). Until that has been implemented, remove the
# read-only flag with a command.
if session.platform == 'windows'
session.shell_command_token(%Q|attrib.exe -r #{win_file}|)
end
session.fs.file.rm(file)
true
rescue ::Rex::Post::Meterpreter::RequestError
false
end
else
win_cmds = [
%Q|attrib.exe -r "#{win_file}"|,
%Q|del.exe /f /q "#{win_file}"|
]
# We need to be platform-independent here. Since we can't be
# certain that {#target} is accurate because exploits with
# automatic targets frequently change it, we just go ahead and
# run both a windows and a unix command in the same line. One
# of them will definitely fail and the other will probably
# succeed. Doing it this way saves us an extra round-trip.
# Trick shared by @mihi42
session.shell_command_token("rm -f \"#{file}\" >/dev/null ; echo ' & #{win_cmds.join(" & ")} & echo \" ' >/dev/null")
true
end
end
# Sends a directory deletion command to the remote +session+
#
# @param [String] dir The directory to delete
# @return [Boolean] True if the delete command has been executed in the remote machine, otherwise false.
def file_dropper_delete_dir(session, dir)
win_dir = file_dropper_win_path(dir)
if session.type == 'meterpreter'
begin
# Meterpreter should do this automatically as part of
# fs.dir.rmdir(). Until that has been implemented, remove the
# read-only flag with a command.
if session.platform == 'windows'
session.shell_command_token(%Q|attrib.exe -r #{win_dir}|)
end
session.fs.dir.rmdir(dir)
true
rescue ::Rex::Post::Meterpreter::RequestError
false
end
else
win_cmds = [
%Q|attrib.exe -r "#{win_dir}"|,
%Q|rd.exe /s /q "#{win_dir}"|
]
# We need to be platform-independent here. Since we can't be
# certain that {#target} is accurate because exploits with
# automatic targets frequently change it, we just go ahead and
# run both a windows and a unix command in the same line. One
# of them will definitely fail and the other will probably
# succeed. Doing it this way saves us an extra round-trip.
# Trick shared by @mihi42
session.shell_command_token("rm -rf \"#{dir}\" >/dev/null ; echo ' & #{win_cmds.join(" & ")} & echo \" ' >/dev/null")
true
end
end
# Checks if a path has been deleted by the current job
#
# @param [String] path The path to check
# @return [Boolean] If the path has been deleted, otherwise false.
def file_dropper_deleted?(session, path, exists_before)
if exists_before && file_dropper_exist?(session, path)
print_error("Unable to delete #{path}")
false
elsif exists_before
print_good("Deleted #{path}")
true
else
print_warning("Tried to delete #{path}, unknown result")
true
end
end
# Check if the path being removed is the same as the working directory
#
# @param [String] path The path to check
# @return [Boolean] true if the path is the same, otherwise false
def file_dropper_check_cwd?(session, path)
if session.type == 'meterpreter'
return true if path == session.fs.dir.pwd
else
pwd =
if session.platform == 'windows'
session.shell_command_token('echo %cd%')
else
session.shell_command_token('pwd')
end
# Check for subdirectories and relative paths
return true if pwd.include?(path)
end
false
end
# Converts a path to use the windows separator '\'
#
# @param [String] path The path to convert
# @return [String] The path converted
def file_dropper_win_path(path)
path.gsub('/', '\\\\')
end
end
end