Skip to content

Commit b46e1be

Browse files
committed
Land rapid7#5371, Add file checking to the on_new_session cleanup
2 parents 8e848c3 + 0c608e2 commit b46e1be

File tree

2 files changed

+125
-61
lines changed

2 files changed

+125
-61
lines changed

lib/msf/core/exploit/file_dropper.rb

Lines changed: 124 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,64 +8,10 @@ def initialize(info = {})
88

99
register_advanced_options(
1010
[
11-
OptInt.new( 'FileDropperDelay', [ false, 'Delay in seconds before attempting file cleanup' ])
11+
OptInt.new('FileDropperDelay', [false, 'Delay in seconds before attempting file cleanup'])
1212
], self.class)
1313
end
1414

15-
#
16-
# When a new session is created, attempt to delete any files that the
17-
# exploit created.
18-
#
19-
# @param (see Msf::Exploit#on_new_session)
20-
# @return [void]
21-
#
22-
def on_new_session(session)
23-
super
24-
25-
if session.type == "meterpreter"
26-
session.core.use("stdapi") unless session.ext.aliases.include?("stdapi")
27-
end
28-
29-
if not @dropped_files or @dropped_files.empty?
30-
return true
31-
end
32-
33-
@dropped_files.delete_if do |file|
34-
win_file = file.gsub("/", "\\\\")
35-
if session.type == "meterpreter"
36-
begin
37-
# Meterpreter should do this automatically as part of
38-
# fs.file.rm(). Until that has been implemented, remove the
39-
# read-only flag with a command.
40-
if session.platform =~ /win/
41-
session.shell_command_token(%Q|attrib.exe -r #{win_file}|)
42-
end
43-
session.fs.file.rm(file)
44-
print_good("Deleted #{file}")
45-
true
46-
rescue ::Rex::Post::Meterpreter::RequestError
47-
false
48-
end
49-
else
50-
win_cmds = [
51-
%Q|attrib.exe -r "#{win_file}"|,
52-
%Q|del.exe /f /q "#{win_file}"|
53-
]
54-
# We need to be platform-independent here. Since we can't be
55-
# certain that {#target} is accurate because exploits with
56-
# automatic targets frequently change it, we just go ahead and
57-
# run both a windows and a unix command in the same line. One
58-
# of them will definitely fail and the other will probably
59-
# succeed. Doing it this way saves us an extra round-trip.
60-
# Trick shared by @mihi42
61-
session.shell_command_token("rm -f \"#{file}\" >/dev/null ; echo ' & #{win_cmds.join(" & ")} & echo \" ' >/dev/null")
62-
print_good("Deleted #{file}")
63-
true
64-
end
65-
end
66-
end
67-
68-
#
6915
# Record file as needing to be cleaned up
7016
#
7117
# @param files [Array<String>] List of paths on the target that should
@@ -84,7 +30,32 @@ def register_files_for_cleanup(*files)
8430
# Singular version
8531
alias register_file_for_cleanup register_files_for_cleanup
8632

33+
# When a new session is created, attempt to delete any files that the
34+
# exploit created.
8735
#
36+
# @param (see Msf::Exploit#on_new_session)
37+
# @return [void]
38+
def on_new_session(session)
39+
super
40+
41+
if session.type == 'meterpreter'
42+
session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
43+
end
44+
45+
unless @dropped_files && @dropped_files.length > 0
46+
return
47+
end
48+
49+
@dropped_files.delete_if do |file|
50+
exists_before = file_dropper_file_exist?(session, file)
51+
if file_dropper_delete(session, file)
52+
file_dropper_deleted?(session, file, exists_before)
53+
else
54+
false
55+
end
56+
end
57+
end
58+
8859
# While the exploit cleanup do a last attempt to delete any files created
8960
# if there is a file_rm method available. Warn the user if any files were
9061
# not cleaned up.
@@ -109,9 +80,8 @@ def cleanup
10980
@dropped_files.delete_if do |file|
11081
begin
11182
file_rm(file)
112-
print_good("Deleted #{file}")
113-
true
114-
#rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE, ::Rex::Post::Meterpreter::RequestError => e
83+
# We don't know for sure if file has been deleted, so always warn about it to the user
84+
false
11585
rescue ::Exception => e
11686
vprint_error("Failed to delete #{file}: #{e}")
11787
elog("Failed to delete #{file}: #{e.class}: #{e}")
@@ -126,5 +96,101 @@ def cleanup
12696
end
12797

12898
end
99+
100+
private
101+
102+
# See if +path+ exists on the remote system and is a regular file
103+
#
104+
# @param path [String] Remote filename to check
105+
# @return [Boolean] True if the file exists, otherwise false.
106+
def file_dropper_file_exist?(session, path)
107+
if session.platform =~ /win/
108+
normalized = file_dropper_win_file(path)
109+
else
110+
normalized = path
111+
end
112+
113+
if session.type == 'meterpreter'
114+
stat = session.fs.file.stat(normalized) rescue nil
115+
return false unless stat
116+
stat.file?
117+
else
118+
if session.platform =~ /win/
119+
f = shell_command_token("cmd.exe /C IF exist \"#{normalized}\" ( echo true )")
120+
if f =~ /true/
121+
f = shell_command_token("cmd.exe /C IF exist \"#{normalized}\\\\\" ( echo false ) ELSE ( echo true )")
122+
end
123+
else
124+
f = session.shell_command_token("test -f \"#{normalized}\" && echo true")
125+
end
126+
127+
return false if f.nil? || f.empty?
128+
return false unless f =~ /true/
129+
true
130+
end
131+
end
132+
133+
# Sends a file deletion command to the remote +session+
134+
#
135+
# @param [String] file The file to delete
136+
# @return [Boolean] True if the delete command has been executed in the remote machine, otherwise false.
137+
def file_dropper_delete(session, file)
138+
win_file = file_dropper_win_file(file)
139+
140+
if session.type == 'meterpreter'
141+
begin
142+
# Meterpreter should do this automatically as part of
143+
# fs.file.rm(). Until that has been implemented, remove the
144+
# read-only flag with a command.
145+
if session.platform =~ /win/
146+
session.shell_command_token(%Q|attrib.exe -r #{win_file}|)
147+
end
148+
session.fs.file.rm(file)
149+
true
150+
rescue ::Rex::Post::Meterpreter::RequestError
151+
false
152+
end
153+
else
154+
win_cmds = [
155+
%Q|attrib.exe -r "#{win_file}"|,
156+
%Q|del.exe /f /q "#{win_file}"|
157+
]
158+
# We need to be platform-independent here. Since we can't be
159+
# certain that {#target} is accurate because exploits with
160+
# automatic targets frequently change it, we just go ahead and
161+
# run both a windows and a unix command in the same line. One
162+
# of them will definitely fail and the other will probably
163+
# succeed. Doing it this way saves us an extra round-trip.
164+
# Trick shared by @mihi42
165+
session.shell_command_token("rm -f \"#{file}\" >/dev/null ; echo ' & #{win_cmds.join(" & ")} & echo \" ' >/dev/null")
166+
true
167+
end
168+
end
169+
170+
# Checks if a file has been deleted by the current job
171+
#
172+
# @param [String] file The file to check
173+
# @return [Boolean] If the file has been deleted, otherwise false.
174+
def file_dropper_deleted?(session, file, exists_before)
175+
if exists_before && file_dropper_file_exist?(session, file)
176+
print_error("Unable to delete #{file}")
177+
false
178+
elsif exists_before
179+
print_good("Deleted #{file}")
180+
true
181+
else
182+
print_warning("Tried to delete #{file}, unknown result")
183+
true
184+
end
185+
end
186+
187+
# Converts a file path to use the windows separator '\'
188+
#
189+
# @param [String] file The file path to convert
190+
# @return [String] The file path converted
191+
def file_dropper_win_file(file)
192+
file.gsub('/', '\\\\')
193+
end
194+
129195
end
130196
end

modules/exploits/multi/http/struts_code_exec_classloader.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,7 @@ def class_loader_exploit
271271
fail_with(Failure::Unknown, "#{peer} - The log file hasn't been flushed")
272272
end
273273

274-
# This path depends on CWD. May require manual cleanup
275-
# See https://github.com/rapid7/metasploit-framework/issues/4667
276-
print_warning("This exploit requires manual cleanup of '#{@jsp_file}' on the target")
274+
register_files_for_cleanup(@jsp_file)
277275

278276
# Prepare the JSP
279277
print_status("#{peer} - Generating JSP...")

0 commit comments

Comments
 (0)