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

563 lines
16 KiB
Ruby

# -*- coding: binary -*-
module Msf::Post::File
#
# Change directory in the remote session to +path+, which may be relative or
# absolute.
#
# @return [void]
def cd(path)
e_path = expand_path(path) rescue path
if session.type == "meterpreter"
session.fs.dir.chdir(e_path)
else
session.shell_command_token("cd \"#{e_path}\"")
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]
def pwd
if session.type == "meterpreter"
return session.fs.dir.getwd
else
if session.platform =~ /win/
# 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
# Returns a list of the contents of the specified directory
# @param directory [String] the directory to list
# @return [Array] the contents of the directory
def dir(directory)
if session.type == 'meterpreter'
return session.fs.dir.entries(directory)
else
if session.platform =~ /win/
return session.shell_command_token("dir #{directory}").split(/[\r\n]+/)
else
return session.shell_command_token("ls #{directory}").split(/[\r\n]+/)
end
end
end
alias ls dir
#
# See if +path+ exists on the remote system and is a directory
#
# @param path [String] Remote filename to check
def directory?(path)
if session.type == "meterpreter"
stat = session.fs.file.stat(path) rescue nil
return false unless stat
return stat.directory?
else
if session.platform =~ /win/
f = cmd_exec("cmd.exe /C IF exist \"#{path}\\*\" ( echo true )")
else
f = session.shell_command_token("test -d \"#{path}\" && echo true")
end
return false if f.nil? or f.empty?
return false unless f =~ /true/
return true
end
end
#
# Expand any environment variables to return the full path specified by +path+.
#
# @return [String]
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
def file?(path)
if session.type == "meterpreter"
stat = session.fs.file.stat(path) rescue nil
return false unless stat
return stat.file?
else
if session.platform =~ /win/
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
else
f = session.shell_command_token("test -f \"#{path}\" && echo true")
end
return false if f.nil? or f.empty?
return false unless f =~ /true/
return true
end
end
alias file_exist? file?
#
# Check for existence of +path+ on the remote file system
#
# @param path [String] Remote filename to check
def exist?(path)
if session.type == "meterpreter"
stat = session.fs.file.stat(path) rescue nil
return !!(stat)
else
if session.platform =~ /win/
f = cmd_exec("cmd.exe /C IF exist \"#{path}\" ( echo true )")
else
f = cmd_exec("test -e \"#{path}\" && echo true")
end
return false if f.nil? or f.empty?
return false unless f =~ /true/
return true
end
end
#
# 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)
unless ::File.exists?(local_file_name)
::FileUtils.touch(local_file_name)
end
output = ::File.open(local_file_name, "a")
data.each_line do |d|
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)
if ::File.exists?(local_file_name)
require 'digest/md5'
chksum = nil
chksum = Digest::MD5.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
return chksum
else
raise "File #{local_file_name} does not exists!"
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)
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)
if ::File.exists?(local_file_name)
require 'digest/sha1'
chksum = nil
chksum = Digest::SHA1.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
return chksum
else
raise "File #{local_file_name} does not exists!"
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)
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)
if ::File.exists?(local_file_name)
require 'digest/sha2'
chksum = nil
chksum = Digest::SHA256.hexdigest(::File.open(local_file_name, "rb") { |f| f.read})
return chksum
else
raise "File #{local_file_name} does not exists!"
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)
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
def read_file(file_name)
data = nil
if session.type == "meterpreter"
data = _read_file_meterpreter(file_name)
elsif session.type == "shell"
if session.platform =~ /win/
data = session.shell_command_token("type \"#{file_name}\"")
else
data = session.shell_command_token("cat \"#{file_name}\"")
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]
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 =~ /win/
session.shell_command_token("echo #{data} > \"#{file_name}\"")
else
_write_file_unix_shell(file_name, data)
end
end
return true
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]
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 =~ /win/
session.shell_command_token("<nul set /p=\"#{data}\" >> \"#{file_name}\"")
else
_write_file_unix_shell(file_name, data, true)
end
end
return true
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)
def upload_file(remote, local)
write_file(remote, ::File.read(local))
end
#
# Delete remote files
#
# @param remote_files [Array<String>] List of remote filenames to
# delete
# @return [void]
def rm_f(*remote_files)
remote_files.each do |remote|
if session.type == "meterpreter"
session.fs.file.delete(remote) if exist?(remote)
else
if session.platform =~ /win/
cmd_exec("del /q /f \"#{remote}\"")
else
cmd_exec("rm -f \"#{remote}\"")
end
end
end
end
alias :file_rm :rm_f
#
# Rename a remote file.
#
# @param old_file [String] Remote file name to move
# @param new_file [String] The new name for the remote file
def rename_file(old_file, new_file)
if session.respond_to? :commands && session.commands.include?("stdapi_fs_file_move")
return (session.fs.file.mv(old_file, new_file).result == 0)
else
if session.platform =~ /win/
if cmd_exec(%Q|move /y "#{old_file}" "#{new_file}"|) =~ /moved/
return true
else
return false
end
else
if cmd_exec(%Q|mv -f "#{old_file}" "#{new_file}"|).empty?
return true
else
return false
end
end
end
end
alias :move_file :rename_file
alias :mv_file :rename_file
protected
#
# 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]
def _read_file_meterpreter(file_name)
begin
fd = session.fs.file.new(file_name, "rb")
rescue ::Rex::Post::Meterpreter::RequestError => e
print_error("Failed to open file: #{file_name}: #{e}")
return nil
end
data = fd.read
begin
until fd.eof?
data << fd.read
end
ensure
fd.close
end
data
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
# session.
#
# @return [void]
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}\"")
# 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 [Fixnum]
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