Path: blob/master/lib/rex/post/meterpreter/extensions/stdapi/fs/file.rb
19516 views
# -*- coding: binary -*-12require 'rex/post/file'3require 'rex/post/meterpreter/channel'4require 'rex/post/meterpreter/channels/pools/file'5require 'rex/post/meterpreter/extensions/stdapi/stdapi'6require 'rex/post/meterpreter/extensions/stdapi/fs/io'7require 'rex/post/meterpreter/extensions/stdapi/fs/file_stat'8require 'fileutils'9require 'filesize'1011module Rex12module Post13module Meterpreter14module Extensions15module Stdapi16module Fs17###18#19# This class implements the Rex::Post::File interface and wraps interaction20# with files on the remote machine.21#22###23class File < Rex::Post::Meterpreter::Extensions::Stdapi::Fs::IO2425include Rex::Post::File2627MIN_BLOCK_SIZE = 102428STEP_SKIPPED_WOULD_OVERWRITE = 'Overwrite'29STEP_COMPLETED = 'Completed'30STEP_SKIPPED = 'Skipped'31STEP_COMPLETED_OVERWRITTEN = 'Overwritten'3233class << self34attr_accessor :client35end3637#38# Return the directory separator, i.e.: "/" on unix, "\\" on windows39#40def self.separator41# The separator won't change, so cache it to prevent sending42# unnecessary requests.43return @separator if @separator4445request = Packet.create_request(COMMAND_ID_STDAPI_FS_SEPARATOR)4647# Fall back to the old behavior of always assuming windows. This48# allows meterpreter executables built before the addition of this49# command to continue functioning.50begin51response = client.send_request(request)52@separator = response.get_tlv_value(TLV_TYPE_STRING)53rescue RequestError54@separator = '\\'55end5657return @separator58end5960class << self61alias Separator separator62alias SEPARATOR separator63end6465#66# Search for files matching +glob+ starting in directory +root+.67#68# Returns an Array (possibly empty) of Hashes. Each element has the following69# keys:70# 'path':: The directory in which the file was found71# 'name':: File name72# 'size':: Size of the file, in bytes73#74# Example:75# client.fs.file.search(client.fs.dir.pwd, "*.txt")76# # => [{"path"=>"C:\\Documents and Settings\\user\\Desktop", "name"=>"foo.txt", "size"=>0}]77#78# Raises a RequestError if +root+ is not a directory.79#80def self.search(root = nil, glob = '*.*', recurse = true, timeout = -1, modified_start_date = nil, modified_end_date = nil)81files = ::Array.new8283request = Packet.create_request(COMMAND_ID_STDAPI_FS_SEARCH)8485root = client.unicode_filter_decode(root) if root86root = root.chomp(separator) if root && !root.eql?('/')8788request.add_tlv(TLV_TYPE_SEARCH_ROOT, root)89request.add_tlv(TLV_TYPE_SEARCH_GLOB, glob)90request.add_tlv(TLV_TYPE_SEARCH_RECURSE, recurse)91request.add_tlv(TLV_TYPE_SEARCH_M_START_DATE, modified_start_date) if modified_start_date92request.add_tlv(TLV_TYPE_SEARCH_M_END_DATE, modified_end_date) if modified_end_date9394# we set the response timeout to -1 to wait indefinitely as a95# search could take an indeterminate amount of time to complete.96response = client.send_request(request, timeout)97if (response.result == 0)98response.each(TLV_TYPE_SEARCH_RESULTS) do |results|99files << {100'path' => client.unicode_filter_encode(results.get_tlv_value(TLV_TYPE_FILE_PATH).chomp(separator)),101'name' => client.unicode_filter_encode(results.get_tlv_value(TLV_TYPE_FILE_NAME)),102'size' => results.get_tlv_value(TLV_TYPE_FILE_SIZE),103'mtime' => results.get_tlv_value(TLV_TYPE_SEARCH_MTIME)104}105end106end107108return files109end110111#112# Returns the base name of the supplied file path to the caller.113#114def self.basename(*a)115path = a[0]116117# Allow both kinds of dir serparators since lots and lots of code118# assumes one or the other so this ends up getting called with strings119# like: "C:\\foo/bar"120path =~ %r{.*[/\\](.*)$}121122Rex::FileUtils.clean_path(::Regexp.last_match(1) || path)123end124125#126# Expands a file path, substituting all environment variables, such as127# %TEMP% on Windows or $HOME on Unix128#129# Examples:130# client.fs.file.expand_path("%appdata%")131# # => "C:\\Documents and Settings\\user\\Application Data"132# client.fs.file.expand_path("~")133# # => "/home/user"134# client.fs.file.expand_path("$HOME/dir")135# # => "/home/user/dir"136# client.fs.file.expand_path("asdf")137# # => "asdf"138#139def self.expand_path(path)140case client.platform141when 'osx', 'freebsd', 'bsd', 'linux', 'android', 'apple_ios'142# For unix-based systems, do some of the work here143# First check for ~144path_components = path.split(separator)145if path_components.length > 0 && path_components[0] == '~'146path = "$HOME#{path[1..-1]}"147end148149# Now find the environment variables we'll need from the client150env_regex = /\$(?:([A-Za-z0-9_]+)|\{([A-Za-z0-9_]+)\})/151matches = path.to_enum(:scan, env_regex).map { Regexp.last_match }152env_vars = matches.map { |match| (match[1] || match[2]).to_s }.uniq153154# Retrieve them155env_vals = client.sys.config.getenvs(*env_vars)156157# Now fill them in158path.gsub(env_regex) do |_z|159envvar = ::Regexp.last_match(1)160envvar = ::Regexp.last_match(2) if envvar.nil?161env_vals[envvar]162end163else164request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_EXPAND_PATH)165166request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(path))167168response = client.send_request(request)169170return client.unicode_filter_encode(response.get_tlv_value(TLV_TYPE_FILE_PATH))171end172end173174#175# Calculates the MD5 (16-bytes raw) of a remote file176#177def self.md5(path)178request = Packet.create_request(COMMAND_ID_STDAPI_FS_MD5)179180request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(path))181182response = client.send_request(request)183184# older meterpreter binaries will send FILE_NAME containing the hash185hash = response.get_tlv_value(TLV_TYPE_FILE_HASH) ||186response.get_tlv_value(TLV_TYPE_FILE_NAME)187return hash188end189190#191# Calculates the SHA1 (20-bytes raw) of a remote file192#193def self.sha1(path)194request = Packet.create_request(COMMAND_ID_STDAPI_FS_SHA1)195196request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(path))197198response = client.send_request(request)199200# older meterpreter binaries will send FILE_NAME containing the hash201hash = response.get_tlv_value(TLV_TYPE_FILE_HASH) ||202response.get_tlv_value(TLV_TYPE_FILE_NAME)203return hash204end205206#207# Performs a stat on a file and returns a FileStat instance.208#209def self.stat(name)210return client.fs.filestat.new(name)211end212213#214# Returns true if the remote file +name+ exists, false otherwise215#216def self.exist?(name)217r = begin218client.fs.filestat.new(name)219rescue StandardError220nil221end222r ? true : false223end224225#226# Performs a delete on the remote file +name+227#228def self.rm(name)229request = Packet.create_request(COMMAND_ID_STDAPI_FS_DELETE_FILE)230231request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(name))232233response = client.send_request(request)234235return response236end237238class << self239alias unlink rm240alias delete rm241end242243#244# Performs a rename from oldname to newname245#246def self.mv(oldname, newname)247request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_MOVE)248249request.add_tlv(TLV_TYPE_FILE_NAME, client.unicode_filter_decode(oldname))250request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(newname))251252response = client.send_request(request)253254return response255end256257class << self258alias move mv259alias rename mv260end261262#263# Performs a copy from oldname to newname264#265def self.cp(oldname, newname)266request = Packet.create_request(COMMAND_ID_STDAPI_FS_FILE_COPY)267268request.add_tlv(TLV_TYPE_FILE_NAME, client.unicode_filter_decode(oldname))269request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(newname))270271response = client.send_request(request)272273return response274end275276class << self277alias copy cp278end279280#281# Performs a chmod on the remote file282#283def self.chmod(name, mode)284request = Packet.create_request(COMMAND_ID_STDAPI_FS_CHMOD)285286request.add_tlv(TLV_TYPE_FILE_PATH, client.unicode_filter_decode(name))287request.add_tlv(TLV_TYPE_FILE_MODE_T, mode)288289response = client.send_request(request)290291return response292end293294#295# Upload one or more files to the remote remote directory supplied in296# +destination+.297#298# If a block is given, it will be called before each file is uploaded and299# again when each upload is complete.300#301def self.upload(dest, *src_files, &stat)302src_files.each do |src|303if (basename(dest) != ::File.basename(src))304dest += separator unless dest.end_with?(separator)305dest += ::File.basename(src)306end307stat.call('Uploading', src, dest) if stat308309upload_file(dest, src)310stat.call(STEP_COMPLETED, src, dest) if stat311end312end313314#315# Upload a single file.316#317def self.upload_file(dest_file, src_file, &stat)318# Open the file on the remote side for writing and read319# all of the contents of the local file320stat.call('Uploading', src_file, dest_file) if stat321dest_fd = nil322src_fd = nil323buf_size = 8 * 1024 * 1024324325begin326dest_fd = client.fs.file.new(dest_file, 'wb')327src_fd = ::File.open(src_file, 'rb')328src_size = src_fd.stat.size329while (buf = src_fd.read(buf_size))330dest_fd.write(buf)331percent = dest_fd.pos.to_f / src_size.to_f * 100.0332msg = "Uploaded #{Filesize.new(dest_fd.pos).pretty} of " \333"#{Filesize.new(src_size).pretty} (#{percent.round(2)}%)"334stat.call(msg, src_file, dest_file) if stat335end336ensure337src_fd.close unless src_fd.nil?338dest_fd.close unless dest_fd.nil?339end340stat.call(STEP_COMPLETED, src_file, dest_file) if stat341end342343def self.is_glob?(name)344/\*|\[|\?/ === name345end346347#348# Download one or more files from the remote computer to the local349# directory supplied in destination.350#351# If a block is given, it will be called before each file is downloaded and352# again when each download is complete.353#354def self.download(dest, src_files, opts = {}, &stat)355dest.force_encoding('UTF-8')356timestamp = opts['timestamp']357[*src_files].each do |src|358src.force_encoding('UTF-8')359if (::File.basename(dest) != File.basename(src))360# The destination when downloading is a local file so use this361# system's separator362dest += ::File::SEPARATOR unless dest.end_with?(::File::SEPARATOR)363dest += File.basename(src)364end365366# XXX: dest can be the same object as src, so we use += instead of <<367if timestamp368dest += timestamp369end370371stat.call('Downloading', src, dest) if stat372result = download_file(dest, src, opts, &stat)373stat.call(result, src, dest) if stat374end375end376377#378# Download a single file.379#380def self.download_file(dest_file, src_file, opts = {}, &stat)381stat ||= ->(a, b, c) {}382383adaptive = opts['adaptive']384block_size = opts['block_size'] || 1024 * 1024385continue = opts['continue']386tries_no = opts['tries_no']387tries = opts['tries']388force_overwrite = opts['force_overwrite'] || false389overwrite_existing = false390src_fd = client.fs.file.new(src_file, 'rb')391392# Check for changes393src_stat = client.fs.filestat.new(src_file)394if ::File.exist?(dest_file)395dst_stat = ::File.stat(dest_file)396if src_stat.size == dst_stat.size && src_stat.mtime == dst_stat.mtime397src_fd.close398return STEP_SKIPPED399end400if !force_overwrite401src_fd.close402return STEP_SKIPPED_WOULD_OVERWRITE403else404overwrite_existing = true405end406end407408# Make the destination path if necessary409dir = ::File.dirname(dest_file)410::FileUtils.mkdir_p(dir) if dir and !::File.directory?(dir)411412src_size = Filesize.new(src_stat.size).pretty413414if continue415# continue downloading the file - skip downloaded part in the source416dst_fd = ::File.new(dest_file, 'ab')417begin418dst_fd.seek(0, ::IO::SEEK_END)419in_pos = dst_fd.pos420src_fd.seek(in_pos)421stat.call("Continuing from #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)422rescue StandardError423# if we can't seek, download again424stat.call('Error continuing - downloading from scratch', src_file, dest_file)425dst_fd.close426dst_fd = ::File.new(dest_file, 'wb')427end428else429dst_fd = ::File.new(dest_file, 'wb')430end431432# Keep transferring until EOF is reached...433begin434if tries435# resume when timeouts encountered436seek_back = false437adjust_block = false438tries_cnt = 0439begin # while440begin # exception441if seek_back442in_pos = dst_fd.pos443src_fd.seek(in_pos)444seek_back = false445stat.call("Resuming at #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)446else447# successfully read and wrote - reset the counter448tries_cnt = 0449end450adjust_block = true451data = src_fd.read(block_size)452adjust_block = false453rescue Rex::TimeoutError454# timeout encountered - either seek back and retry or quit455if (tries && (tries_no == 0 || tries_cnt < tries_no))456tries_cnt += 1457seek_back = true458# try a smaller block size for the next round459if adaptive && adjust_block460block_size = [block_size >> 1, MIN_BLOCK_SIZE].max461adjust_block = false462msg = "Error downloading, block size set to #{block_size} - retry # #{tries_cnt}"463stat.call(msg, src_file, dest_file)464else465stat.call("Error downloading - retry # #{tries_cnt}", src_file, dest_file)466end467retry468else469stat.call('Error downloading - giving up', src_file, dest_file)470raise471end472end473dst_fd.write(data) if !data.nil?474percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0475msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"476stat.call(msg, src_file, dest_file)477end while (!data.nil?)478else479# do the simple copying quitting on the first error480while ((data = src_fd.read(block_size)) != nil)481dst_fd.write(data)482percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0483msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"484stat.call(msg, src_file, dest_file)485end486end487rescue EOFError488ensure489src_fd.close490dst_fd.close491end492493# Clone the times from the remote file494::File.utime(src_stat.atime, src_stat.mtime, dest_file)495return overwrite_existing ? STEP_COMPLETED_OVERWRITTEN : STEP_COMPLETED496end497498#499# With no associated block, File.open is a synonym for ::new. If the optional500# code block is given, it will be passed the opened file as an argument, and501# the File object will automatically be closed when the block terminates. In502# this instance, File.open returns the value of the block.503#504# (doc stolen from http://www.ruby-doc.org/core-1.9.3/File.html#method-c-open)505#506def self.open(name, mode = 'r', perms = 0)507f = new(name, mode, perms)508if block_given?509ret = yield f510f.close511return ret512else513return f514end515end516517##518#519# Constructor520#521##522523#524# Initializes and opens the specified file with the specified permissions.525#526def initialize(name, mode = 'r', perms = 0)527self.client = self.class.client528self.filed = _open(name, mode, perms)529end530531##532#533# IO implementers534#535##536537#538# Returns whether or not the file has reach EOF.539#540def eof541return filed.eof542end543544#545# Returns the current position of the file pointer.546#547def pos548return filed.tell549end550551#552# Synonym for sysseek.553#554def seek(offset, whence = ::IO::SEEK_SET)555return sysseek(offset, whence)556end557558#559# Seeks to the supplied offset based on the supplied relativity.560#561def sysseek(offset, whence = ::IO::SEEK_SET)562return filed.seek(offset, whence)563end564565protected566567##568#569# Internal methods570#571##572573#574# Creates a File channel using the supplied information.575#576def _open(name, mode = 'r', perms = 0)577return Rex::Post::Meterpreter::Channels::Pools::File.open(578client, name, mode, perms579)580end581582attr_accessor :client # :nodoc:583584end585end; end; end; end; end; end586587588