# -*- coding: binary -*- module Msf::Post::File # # Change directory in the remote session to +path+ # def cd(path) if session.type == "meterpreter" e_path = session.fs.file.expand_path(path) rescue path session.fs.dir.chdir(e_path) else session.shell_command_token("cd '#{path}'") end end # # Returns the current working directory in the remote session # 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 # # See if +path+ exists on the remote system and is a directory # 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/ # XXX else f = session.shell_command_token("test -d '#{path}' && echo true") return false if f.nil? or f.empty? return false unless f =~ /true/ return true end end end # # Expand any environment variables to return the full path specified by +path+. # 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 # 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/ # XXX else f = session.shell_command_token("test -f '#{path}' && echo true") return false if f.nil? or f.empty? return false unless f =~ /true/ return true end end end alias file_exist? file? # # Check for existence of +path+ on the remote file system # def exist?(path) if session.type == "meterpreter" stat = session.fs.file.stat(path) rescue nil return !!(stat) else if session.platform =~ /win/ # XXX else f = session.shell_command_token("test -e '#{path}' && echo true") return false if f.nil? or f.empty? return false unless f =~ /true/ return true end end end # # Remove a remote file # def file_rm(file) if session.type == "meterpreter" session.fs.file.rm(file) else if session.platform =~ /win/ session.shell_command_token("del \"#{file}\"") else session.shell_command_token("rm -f '#{file}'") end end end # # Writes a given string to a given local file # def file_local_write(file2wrt, data2wrt) if not ::File.exists?(file2wrt) ::FileUtils.touch(file2wrt) end output = ::File.open(file2wrt, "a") data2wrt.each_line do |d| output.puts(d) end output.close end # # Returns a MD5 checksum of a given local file # def file_local_digestmd5(file2md5) if not ::File.exists?(file2md5) raise "File #{file2md5} does not exists!" else require 'digest/md5' chksum = nil chksum = Digest::MD5.hexdigest(::File.open(file2md5, "rb") { |f| f.read}) return chksum end end # # Returns a MD5 checksum of a given remote file # def file_remote_digestmd5(file2md5) data = read_file(file2md5) chksum = nil if data chksum = Digest::MD5.hexdigest(data) end return chksum end # # Returns a SHA1 checksum of a given local file # def file_local_digestsha1(file2sha1) if not ::File.exists?(file2sha1) raise "File #{file2sha1} does not exists!" else require 'digest/sha1' chksum = nil chksum = Digest::SHA1.hexdigest(::File.open(file2sha1, "rb") { |f| f.read}) return chksum end end # # Returns a SHA1 checksum of a given remote file # def file_remote_digestsha1(file2sha1) data = read_file(file2sha1) chksum = nil if data chksum = Digest::SHA1.hexdigest(data) end return chksum end # # Returns a SHA256 checksum of a given local file # def file_local_digestsha2(file2sha2) if not ::File.exists?(file2sha2) raise "File #{file2sha2} does not exists!" else require 'digest/sha2' chksum = nil chksum = Digest::SHA256.hexdigest(::File.open(file2sha2, "rb") { |f| f.read}) return chksum end end # # Returns a SHA2 checksum of a given remote file # def file_remote_digestsha2(file2sha2) data = read_file(file2sha2) 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. # 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. # Returns Boolean true if successful # # NOTE: *This is not binary-safe on Windows shell sessions!* # 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!* # 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("> \"#{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 # def upload_file(remote, local) write_file(remote, ::File.read(local)) end # # Delete remote files # def rm_f(*remote_files) remote_files.each do |remote| if session.type == "meterpreter" session.fs.file.delete(remote) else if session.platform =~ /win/ cmd_exec("del /q /f #{remote}") else cmd_exec("rm -f #{remote}") end end end end # # Rename a remote file. # def rename_file(old_file, new_file) if session.respond_to? :commands and session.commands.include?("stdapi_fs_file_move") session.fs.file.mv(old_file, new_file) else if session.platform =~ /win/ cmd_exec(%Q|move /y "#{old_file}" "#{new_file}"|) else cmd_exec(%Q|mv -f "#{old_file}" "#{new_file}"|) 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. # 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}") 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. # 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"}' :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. # 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