lib/msf/core/exploit/file_dropper.rb
# -*- 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