metasploit-framework/lib/msf/core/post/file.rb

628 lines
18 KiB
Ruby
Raw Normal View History

# -*- coding: binary -*-
2012-07-17 21:48:06 +00:00
module Msf::Post::File
2013-08-30 21:28:33 +00:00
#
# Change directory in the remote session to +path+, which may be relative or
# absolute.
2013-08-30 21:28:33 +00:00
#
# @return [void]
2013-08-30 21:28:33 +00:00
def cd(path)
e_path = expand_path(path) rescue path
2013-08-30 21:28:33 +00:00
if session.type == "meterpreter"
session.fs.dir.chdir(e_path)
else
session.shell_command_token("cd \"#{e_path}\"")
2013-08-30 21:28:33 +00:00
end
end
#
# Returns the current working directory in the remote session
#
# @note This may be inaccurate on shell sessions running on Windows before
# XP/2k3
#
# @return [String]
2013-08-30 21:28:33 +00:00
def pwd
if session.type == "meterpreter"
return session.fs.dir.getwd
else
if session.platform == 'windows'
2013-08-30 21:28:33 +00:00
# XXX: %CD% only exists on XP and newer, figure something out for NT4
# and 2k
return session.shell_command_token("echo %CD%")
else
return session.shell_command_token("pwd")
end
end
end
2014-11-17 18:03:37 +00:00
# Returns a list of the contents of the specified directory
# @param directory [String] the directory to list
# @return [Array] the contents of the directory
2014-11-17 18:15:33 +00:00
def dir(directory)
2014-08-01 00:51:15 +00:00
if session.type == 'meterpreter'
2014-11-17 17:15:08 +00:00
return session.fs.dir.entries(directory)
2014-08-01 00:51:15 +00:00
else
if session.platform == 'windows'
2014-11-17 17:15:08 +00:00
return session.shell_command_token("dir #{directory}").split(/[\r\n]+/)
else
2014-11-17 17:15:08 +00:00
return session.shell_command_token("ls #{directory}").split(/[\r\n]+/)
end
2014-08-01 00:51:15 +00:00
end
end
alias ls dir
2013-08-30 21:28:33 +00:00
#
# See if +path+ exists on the remote system and is a directory
#
# @param path [String] Remote filename to check
2013-08-30 21:28:33 +00:00
def directory?(path)
2018-01-23 07:12:42 +00:00
if session.type == 'meterpreter'
2013-08-30 21:28:33 +00:00
stat = session.fs.file.stat(path) rescue nil
return false unless stat
return stat.directory?
else
if session.platform == 'windows'
2014-02-02 19:04:38 +00:00
f = cmd_exec("cmd.exe /C IF exist \"#{path}\\*\" ( echo true )")
2013-08-30 21:28:33 +00:00
else
f = session.shell_command_token("test -d \"#{path}\" && echo true")
2013-08-30 21:28:33 +00:00
end
2014-02-02 19:04:38 +00:00
2018-01-23 07:12:42 +00:00
return false if f.nil? || f.empty?
2014-02-02 19:04:38 +00:00
return false unless f =~ /true/
2018-01-23 07:12:42 +00:00
true
2013-08-30 21:28:33 +00:00
end
end
#
# Expand any environment variables to return the full path specified by +path+.
#
# @return [String]
2013-08-30 21:28:33 +00:00
def expand_path(path)
if session.type == "meterpreter"
return session.fs.file.expand_path(path)
else
return cmd_exec("echo #{path}")
end
end
#
# See if +path+ exists on the remote system and is a regular file
#
# @param path [String] Remote filename to check
2013-08-30 21:28:33 +00:00
def file?(path)
2018-01-23 07:12:42 +00:00
if session.type == 'meterpreter'
2013-08-30 21:28:33 +00:00
stat = session.fs.file.stat(path) rescue nil
return false unless stat
return stat.file?
else
if session.platform == 'windows'
2014-02-02 19:04:38 +00:00
f = cmd_exec("cmd.exe /C IF exist \"#{path}\" ( echo true )")
if f =~ /true/
f = cmd_exec("cmd.exe /C IF exist \"#{path}\\\\\" ( echo false ) ELSE ( echo true )")
end
2013-08-30 21:28:33 +00:00
else
f = session.shell_command_token("test -f \"#{path}\" && echo true")
2013-08-30 21:28:33 +00:00
end
2014-02-02 19:04:38 +00:00
2018-01-23 07:12:42 +00:00
return false if f.nil? || f.empty?
2014-02-02 19:04:38 +00:00
return false unless f =~ /true/
2018-01-23 07:12:42 +00:00
true
2013-08-30 21:28:33 +00:00
end
end
alias file_exist? file?
2018-01-23 07:12:42 +00:00
#
# See if +path+ on the remote system is a setuid file
#
# @param path [String] Remote filename to check
def setuid?(path)
if session.type == 'meterpreter'
stat = session.fs.file.stat(path) rescue nil
return false unless stat
return stat.setuid?
else
if session.platform != 'windows'
f = session.shell_command_token("test -u \"#{path}\" && echo true")
end
return false if f.nil? || f.empty?
return false unless f =~ /true/
true
end
end
#
# See if +path+ on the remote system exists and is writable
#
# @param path [String] Remote path to check
#
# @return [Boolean] true if +path+ exists and is writable
#
def writable?(path)
raise "`writable?' method does not support Windows systems" if session.platform == 'windows'
cmd_exec("test -w '#{path}' && echo true").to_s.include? 'true'
end
2013-08-30 21:28:33 +00:00
#
# Check for existence of +path+ on the remote file system
#
# @param path [String] Remote filename to check
2013-08-30 21:28:33 +00:00
def exist?(path)
2018-01-23 07:12:42 +00:00
if session.type == 'meterpreter'
2013-08-30 21:28:33 +00:00
stat = session.fs.file.stat(path) rescue nil
return !!(stat)
else
if session.platform == 'windows'
f = cmd_exec("cmd.exe /C IF exist \"#{path}\" ( echo true )")
2013-08-30 21:28:33 +00:00
else
f = cmd_exec("test -e \"#{path}\" && echo true")
2013-08-30 21:28:33 +00:00
end
2014-02-10 23:24:26 +00:00
2018-01-23 07:12:42 +00:00
return false if f.nil? || f.empty?
2014-02-10 23:24:26 +00:00
return false unless f =~ /true/
2018-01-23 07:12:42 +00:00
true
2013-08-30 21:28:33 +00:00
end
end
alias :exists? :exist?
2013-08-30 21:28:33 +00:00
#
# Writes a given string to a given local file
#
# @param local_file_name [String]
# @param data [String]
# @return [void]
def file_local_write(local_file_name, data)
2016-04-20 12:11:34 +00:00
unless ::File.exist?(local_file_name)
::FileUtils.touch(local_file_name)
2013-08-30 21:28:33 +00:00
end
output = ::File.open(local_file_name, "a")
data.each_line do |d|
2013-08-30 21:28:33 +00:00
output.puts(d)
end
output.close
end
#
# Returns a MD5 checksum of a given local file
#
# @param local_file_name [String] Local file name
# @return [String] Hex digest of file contents
def file_local_digestmd5(local_file_name)
2016-04-20 12:11:34 +00:00
if ::File.exist?(local_file_name)
2013-08-30 21:28:33 +00:00
require 'digest/md5'
chksum = nil
chksum = Digest::MD5.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
2013-08-30 21:28:33 +00:00
return chksum
else
raise "File #{local_file_name} does not exists!"
2013-08-30 21:28:33 +00:00
end
end
#
# Returns a MD5 checksum of a given remote file
#
# @note THIS DOWNLOADS THE FILE
# @param file_name [String] Remote file name
# @return [String] Hex digest of file contents
def file_remote_digestmd5(file_name)
data = read_file(file_name)
2013-08-30 21:28:33 +00:00
chksum = nil
if data
chksum = Digest::MD5.hexdigest(data)
end
return chksum
end
#
# Returns a SHA1 checksum of a given local file
#
# @param local_file_name [String] Local file name
# @return [String] Hex digest of file contents
def file_local_digestsha1(local_file_name)
2016-04-20 12:11:34 +00:00
if ::File.exist?(local_file_name)
2013-08-30 21:28:33 +00:00
require 'digest/sha1'
chksum = nil
chksum = Digest::SHA1.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
2013-08-30 21:28:33 +00:00
return chksum
else
raise "File #{local_file_name} does not exists!"
2013-08-30 21:28:33 +00:00
end
end
#
# Returns a SHA1 checksum of a given remote file
#
# @note THIS DOWNLOADS THE FILE
# @param file_name [String] Remote file name
# @return [String] Hex digest of file contents
def file_remote_digestsha1(file_name)
data = read_file(file_name)
2013-08-30 21:28:33 +00:00
chksum = nil
if data
chksum = Digest::SHA1.hexdigest(data)
end
return chksum
end
#
# Returns a SHA256 checksum of a given local file
#
# @param local_file_name [String] Local file name
# @return [String] Hex digest of file contents
def file_local_digestsha2(local_file_name)
2016-04-20 12:11:34 +00:00
if ::File.exist?(local_file_name)
2013-08-30 21:28:33 +00:00
require 'digest/sha2'
chksum = nil
chksum = Digest::SHA256.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
2013-08-30 21:28:33 +00:00
return chksum
else
raise "File #{local_file_name} does not exists!"
2013-08-30 21:28:33 +00:00
end
end
#
# Returns a SHA2 checksum of a given remote file
#
# @note THIS DOWNLOADS THE FILE
# @param file_name [String] Remote file name
# @return [String] Hex digest of file contents
def file_remote_digestsha2(file_name)
data = read_file(file_name)
2013-08-30 21:28:33 +00:00
chksum = nil
if data
chksum = Digest::SHA256.hexdigest(data)
end
return chksum
end
#
# Platform-agnostic file read. Returns contents of remote file +file_name+
# as a String.
#
# @param file_name [String] Remote file name to read
# @return [String] Contents of the file
2013-08-30 21:28:33 +00:00
def read_file(file_name)
data = nil
if session.type == "meterpreter"
data = _read_file_meterpreter(file_name)
elsif session.type == "shell"
if session.platform == 'windows'
2013-08-30 21:28:33 +00:00
data = session.shell_command_token("type \"#{file_name}\"")
else
data = session.shell_command_token("cat \"#{file_name}\"")
2013-08-30 21:28:33 +00:00
end
end
data
end
# Platform-agnostic file write. Writes given object content to a remote file.
#
# NOTE: *This is not binary-safe on Windows shell sessions!*
#
# @param file_name [String] Remote file name to write
# @param data [String] Contents to put in the file
# @return [void]
2013-08-30 21:28:33 +00:00
def write_file(file_name, data)
if session.type == "meterpreter"
fd = session.fs.file.new(file_name, "wb")
fd.write(data)
fd.close
elsif session.respond_to? :shell_command_token
if session.platform == 'windows'
2013-08-30 21:28:33 +00:00
session.shell_command_token("echo #{data} > \"#{file_name}\"")
else
_write_file_unix_shell(file_name, data)
end
end
2018-01-23 07:12:42 +00:00
true
2013-08-30 21:28:33 +00:00
end
#
# Platform-agnostic file append. Appends given object content to a remote file.
# Returns Boolean true if successful
#
# NOTE: *This is not binary-safe on Windows shell sessions!*
#
# @param file_name [String] Remote file name to write
# @param data [String] Contents to put in the file
# @return [void]
2013-08-30 21:28:33 +00:00
def append_file(file_name, data)
if session.type == "meterpreter"
fd = session.fs.file.new(file_name, "ab")
fd.write(data)
fd.close
elsif session.respond_to? :shell_command_token
if session.platform == 'windows'
2013-08-30 21:28:33 +00:00
session.shell_command_token("<nul set /p=\"#{data}\" >> \"#{file_name}\"")
else
_write_file_unix_shell(file_name, data, true)
end
end
2018-01-23 07:12:42 +00:00
true
2013-08-30 21:28:33 +00:00
end
#
# Read a local file +local+ and write it as +remote+ on the remote file
# system
#
# @param remote [String] Destination file name on the remote filesystem
# @param local [String] Local file whose contents will be uploaded
# @return (see #write_file)
2013-08-30 21:28:33 +00:00
def upload_file(remote, local)
write_file(remote, ::File.read(local))
end
2018-09-24 07:23:44 +00:00
#
2018-09-24 17:37:15 +00:00
# Sets the permissions on a remote file
2018-09-24 07:23:44 +00:00
#
2018-09-24 17:37:15 +00:00
# @param path [String] Path on the remote filesystem
# @param mode [Fixnum] Mode as an octal number
def chmod(path, mode = 0700)
if session.platform == 'windows'
2018-09-24 20:17:59 +00:00
raise "`chmod' method does not support Windows systems"
2018-09-24 17:37:15 +00:00
end
if session.type == 'meterpreter' && session.commands.include?('stdapi_fs_chmod')
session.fs.file.chmod(path, mode)
2018-09-24 07:23:44 +00:00
else
2018-09-24 17:37:15 +00:00
cmd_exec("chmod #{mode.to_s(8)} '#{path}'")
2018-09-24 07:23:44 +00:00
end
end
2013-08-30 21:28:33 +00:00
#
# Delete remote files
#
# @param remote_files [Array<String>] List of remote filenames to
# delete
# @return [void]
2013-08-30 21:28:33 +00:00
def rm_f(*remote_files)
remote_files.each do |remote|
if session.type == "meterpreter"
session.fs.file.delete(remote) if exist?(remote)
2013-08-30 21:28:33 +00:00
else
if session.platform == 'windows'
cmd_exec("del /q /f \"#{remote}\"")
2013-08-30 21:28:33 +00:00
else
cmd_exec("rm -f \"#{remote}\"")
2013-08-30 21:28:33 +00:00
end
end
end
end
2017-12-31 06:14:57 +00:00
#
# Delete remote directories
#
# @param remote_dirs [Array<String>] List of remote directories to
# delete
# @return [void]
def rm_rf(*remote_dirs)
remote_dirs.each do |remote|
if session.type == "meterpreter"
session.fs.dir.rmdir(remote) if exist?(remote)
else
if session.platform == 'windows'
cmd_exec("rd /s /q \"#{remote}\"")
else
cmd_exec("rm -rf \"#{remote}\"")
end
end
end
end
alias :file_rm :rm_f
2017-12-31 06:14:57 +00:00
alias :dir_rm :rm_rf
2013-08-30 21:28:33 +00:00
#
# Rename a remote file.
#
# @param old_file [String] Remote file name to move
# @param new_file [String] The new name for the remote file
2013-08-30 21:28:33 +00:00
def rename_file(old_file, new_file)
2015-01-27 15:13:40 +00:00
if session.type == "meterpreter"
return (session.fs.file.mv(old_file, new_file).result == 0)
2013-08-30 21:28:33 +00:00
else
if session.platform == 'windows'
2015-01-27 15:13:40 +00:00
cmd_exec(%Q|move /y "#{old_file}" "#{new_file}"|) =~ /moved/
else
2015-01-27 15:13:40 +00:00
cmd_exec(%Q|mv -f "#{old_file}" "#{new_file}"|).empty?
end
2013-08-30 21:28:33 +00:00
end
end
alias :move_file :rename_file
alias :mv_file :rename_file
protected
2013-08-30 21:28:33 +00:00
#
# Meterpreter-specific file read. Returns contents of remote file
# +file_name+ as a String or nil if there was an error
#
# You should never call this method directly. Instead, call {#read_file}
# which will call this if it is appropriate for the given session.
#
# @return [String]
2013-08-30 21:28:33 +00:00
def _read_file_meterpreter(file_name)
fd = session.fs.file.new(file_name, "rb")
2013-08-30 21:28:33 +00:00
data = fd.read
until fd.eof?
data << fd.read
2013-08-30 21:28:33 +00:00
end
2013-08-30 21:28:33 +00:00
data
rescue EOFError
# Sometimes fd isn't marked EOF in time?
''
rescue ::Rex::Post::Meterpreter::RequestError => e
print_error("Failed to open file: #{file_name}: #{e}")
return nil
ensure
fd.close if fd
2013-08-30 21:28:33 +00:00
end
#
# Write +data+ to the remote file +file_name+.
#
# Truncates if +append+ is false, appends otherwise.
#
# You should never call this method directly. Instead, call {#write_file}
# or {#append_file} which will call this if it is appropriate for the given
2013-08-30 21:28:33 +00:00
# session.
#
# @return [void]
2013-08-30 21:28:33 +00:00
def _write_file_unix_shell(file_name, data, append=false)
redirect = (append ? ">>" : ">")
# Short-circuit an empty string. The : builtin is part of posix
# standard and should theoretically exist everywhere.
if data.length == 0
session.shell_command_token(": #{redirect} #{file_name}")
return
end
d = data.dup
d.force_encoding("binary") if d.respond_to? :force_encoding
chunks = []
command = nil
encoding = :hex
cmd_name = ""
line_max = _unix_max_line_length
# Leave plenty of room for the filename we're writing to and the
# command to echo it out
line_max -= file_name.length
line_max -= 64
# Ordered by descending likeliness to work
[
# POSIX standard requires %b which expands octal (but not hex)
# escapes in the argument. However, some versions (notably
# FreeBSD) truncate input on nulls, so "printf %b '\0\101'"
# produces a 0-length string. Some also allow octal escapes
# without a format string, and do not truncate, so start with
# that and try %b if it doesn't work. The standalone version seems
# to be more likely to work than the buitin version, so try it
# first.
#
# Both of these work for sure on Linux and FreeBSD
{ :cmd => %q^/usr/bin/printf 'CONTENTS'^ , :enc => :octal, :name => "printf" },
{ :cmd => %q^printf 'CONTENTS'^ , :enc => :octal, :name => "printf" },
# Works on Solaris
{ :cmd => %q^/usr/bin/printf %b 'CONTENTS'^ , :enc => :octal, :name => "printf" },
{ :cmd => %q^printf %b 'CONTENTS'^ , :enc => :octal, :name => "printf" },
# Perl supports both octal and hex escapes, but octal is usually
# shorter (e.g. 0 becomes \0 instead of \x00)
{ :cmd => %q^perl -e 'print("CONTENTS")'^ , :enc => :octal, :name => "perl" },
# POSIX awk doesn't have \xNN escapes, use gawk to ensure we're
# getting the GNU version.
{ :cmd => %q^gawk 'BEGIN {ORS="";print "CONTENTS"}' </dev/null^ , :enc => :hex, :name => "awk" },
# xxd's -p flag specifies a postscript-style hexdump of unadorned hex
# digits, e.g. ABCD would be 41424344
{ :cmd => %q^echo 'CONTENTS'|xxd -p -r^ , :enc => :bare_hex, :name => "xxd" },
# Use echo as a last resort since it frequently doesn't support -e
# or -n. bash and zsh's echo builtins are apparently the only ones
# that support both. Most others treat all options as just more
# arguments to print. In particular, the standalone /bin/echo or
# /usr/bin/echo appear never to have -e so don't bother trying
# them.
{ :cmd => %q^echo -ne 'CONTENTS'^ , :enc => :hex },
].each { |foo|
# Some versions of printf mangle %.
test_str = "\0\xff\xfeABCD\x7f%%\r\n"
#test_str = "\0\xff\xfe"
case foo[:enc]
when :hex
cmd = foo[:cmd].sub("CONTENTS"){ Rex::Text.to_hex(test_str) }
when :octal
cmd = foo[:cmd].sub("CONTENTS"){ Rex::Text.to_octal(test_str) }
when :bare_hex
cmd = foo[:cmd].sub("CONTENTS"){ Rex::Text.to_hex(test_str,'') }
end
a = session.shell_command_token("#{cmd}")
if test_str == a
command = foo[:cmd]
encoding = foo[:enc]
cmd_name = foo[:name]
break
else
vprint_status("#{cmd} Failed: #{a.inspect} != #{test_str.inspect}")
end
}
if command.nil?
raise RuntimeError, "Can't find command on the victim for writing binary data", caller
end
# each byte will balloon up to 4 when we encode
# (A becomes \x41 or \101)
max = line_max/4
i = 0
while (i < d.length)
slice = d.slice(i...(i+max))
case encoding
when :hex
chunks << Rex::Text.to_hex(slice)
when :octal
chunks << Rex::Text.to_octal(slice)
when :bare_hex
chunks << Rex::Text.to_hex(slice,'')
end
i += max
end
vprint_status("Writing #{d.length} bytes in #{chunks.length} chunks of #{chunks.first.length} bytes (#{encoding}-encoded), using #{cmd_name}")
# The first command needs to use the provided redirection for either
# appending or truncating.
cmd = command.sub("CONTENTS") { chunks.shift }
session.shell_command_token("#{cmd} #{redirect} \"#{file_name}\"")
2013-08-30 21:28:33 +00:00
# After creating/truncating or appending with the first command, we
# need to append from here on out.
chunks.each { |chunk|
vprint_status("Next chunk is #{chunk.length} bytes")
cmd = command.sub("CONTENTS") { chunk }
session.shell_command_token("#{cmd} >> '#{file_name}'")
}
true
end
#
# Calculate the maximum line length for a unix shell.
#
# @return [Integer]
2013-08-30 21:28:33 +00:00
def _unix_max_line_length
# Based on autoconf's arg_max calculator, see
# http://www.in-ulm.de/~mascheck/various/argmax/autoconf_check.html
calc_line_max = 'i=0 max= new= str=abcd; \
while (test "X"`echo "X$str" 2>/dev/null` = "XX$str") >/dev/null 2>&1 && \
new=`expr "X$str" : ".*" 2>&1` && \
test "$i" != 17 && \
max=$new; do \
i=`expr $i + 1`; str=$str$str;\
done; echo $max'
line_max = session.shell_command_token(calc_line_max).to_i
# Fall back to a conservative 4k which should work on even the most
# restrictive of embedded shells.
line_max = (line_max == 0 ? 4096 : line_max)
vprint_status("Max line length is #{line_max}")
line_max
end
end