From 2b3e3725ac9550d741fb7e2c8af7f3a32d912dc8 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 18:15:19 -0600 Subject: [PATCH] TFTP adding comment docs, ability to send w/out a file. Commenting the tricksy parts a little better for general usage. Adding the ability to set FILEDATA instead of FILENAME, in case only short bits of data are desired and the user doesn't want to go to the trouble of creating a source file to upload. --- lib/rex/proto/tftp/client.rb | 113 +++++++++++++++--- .../auxiliary/admin/tftp/tftp_upload_file.rb | 95 ++++++++++----- 2 files changed, 161 insertions(+), 47 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index 609c96419b..011cc43aa9 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -12,12 +12,31 @@ module TFTP # TFTP Client class # # Note that TFTP has blocks, and so does Ruby. Watch out with the variable names! +# +# The big gotcha right now is that setting the mode between octet, netascii, or +# anything else doesn't actually do anything other than declare it to the +# server. +# +# Also, since TFTP clients act as both clients and servers, we use two +# threads to handle transfers, regardless of the direction. For this reason, +# the action of sending data is nonblocking; if you need to see the +# results of a transfer before doing something else, check the boolean complete +# attribute and any return data in the :return_data attribute. It's a little +# weird like that. +# +# Finally, most (all?) clients will alter the data in netascii mode in order +# to try to conform to the RFC standard for what "netascii" means, but there are +# ambiguities in implementations on things like if nulls are allowed, what +# to do with Unicode, and all that. For this reason, "octet" is default, and +# if you want to send "netascii" data, it's on you to fix up your source data +# prior to sending it. +# class Client attr_accessor :local_host, :local_port, :peer_host, :peer_port attr_accessor :threads, :context, :server_sock, :client_sock attr_accessor :local_file, :remote_file, :mode, :action - attr_accessor :complete, :recv_tempfile + attr_accessor :complete, :recv_tempfile, :return_data # Returns an array of [code, type, msg]. Data packets # specifically will /not/ unpack, since that would drop any trailing spaces or nulls. @@ -35,7 +54,7 @@ class Client self.peer_host = params["PeerHost"] || (raise ArgumentError, "Need a peer host.") self.peer_port = params["PeerPort"] || 69 self.context = params["Context"] || {} - self.local_file = params["LocalFile"] || (raise ArgumentError, "Need a local file.") + self.local_file = params["LocalFile"] self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last self.mode = params["Mode"] || "octet" self.action = params["Action"] || (raise ArgumentError, "Need an action.") @@ -55,7 +74,11 @@ class Client yield "Started TFTP client listener on #{local_host}:#{local_port}" end self.threads << Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { - monitor_server_sock {|msg| yield msg} + if block_given? + monitor_server_sock {|msg| yield msg} + else + monitor_server_sock + end } end @@ -65,9 +88,19 @@ class Client if res and res[0] code, type, data = parse_tftp_msg(res[0]) if code == OpAck && self.action == :upload - send_data(res[1], res[2]) {|msg| yield msg} + if block_given? + yield "WRQ accepted, sending the file." if type == 0 + self.return_data = {:write_allowed => true} + send_data(res[1], res[2]) {|msg| yield msg} + else + send_data(res[1], res[2]) + end elsif code == OpData && self.action == :download - recv_data(res[1], res[2], data) {|msg| yield msg} + if block_given? + recv_data(res[1], res[2], data) {|msg| yield msg} + else + recv_data(res[1], res[2], data) + end else yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? stop @@ -107,6 +140,8 @@ class Client end def send_read_request(&block) + self.return_data = nil + self.complete = false if block_given? start_server_socket {|msg| yield msg} else @@ -121,8 +156,15 @@ class Client ) self.client_sock.sendto(rrq_packet, peer_host, peer_port) self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { - monitor_client_sock {|msg| yield msg} + if block_given? + monitor_client_sock {|msg| yield msg} + else + monitor_client_sock + end } + until self.complete + return self.return_data + end end def recv_data(host, port, first_block) @@ -132,14 +174,22 @@ class Client yield "Source file: #{self.remote_file}, destination file: #{self.local_file}" yield "Received and acknowledged #{first_block.size} in block #{recvd_blocks}" end - write_and_ack_data(first_block,1,host,port) {|msg| yield msg} + if block_given? + write_and_ack_data(first_block,1,host,port) {|msg| yield msg} + else + write_and_ack_data(first_block,1,host,port) + end current_block = first_block while current_block.size == 512 res = self.server_sock.recvfrom(65535) if res and res[0] code, block_num, current_block = parse_tftp_msg(res[0]) if code == 3 - write_and_ack_data(current_block,block_num,host,port) {|msg| yield msg} + if block_given? + write_and_ack_data(current_block,block_num,host,port) {|msg| yield msg} + else + write_and_ack_data(current_block,block_num,host,port) + end recvd_blocks += 1 else yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? @@ -147,7 +197,15 @@ class Client end end end - yield("Transferred #{recvd_blocks} blocks, #{self.recv_tempfile.size} bytes, download complete!") + if block_given? + yield("Transferred #{self.recv_tempfile.size} bytes in #{recvd_blocks} blocks, download complete!") + end + self.return_data = {:success => [ + self.local_file, + self.remote_file, + self.recv_tempfile.size, + recvd_blocks.size] + } self.recv_tempfile.close stop end @@ -157,7 +215,7 @@ class Client self.recv_tempfile.flush req = ack_packet(blocknum) self.server_sock.sendto(req, host, port) - yield "Received and acknowledged #{data.size} in block #{blocknum}" + yield "Received and acknowledged #{data.size} in block #{blocknum}" if block_given? end # @@ -170,12 +228,20 @@ class Client req.pack(packstr) end - def blockify_file - data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} + # Note that the local filename for uploading need not be a real filename -- + # if it begins with DATA: it can be any old string of bytes. + def blockify_file_or_data + if self.local_file =~ /^DATA:(.*)/m + data = $1 + else + data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} + end data.scan(/.{1,512}/) end def send_write_request(&block) + self.return_data = nil + self.complete = false if block_given? start_server_socket {|msg| yield msg} else @@ -190,18 +256,25 @@ class Client ) self.client_sock.sendto(wrq_packet, peer_host, peer_port) self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { - monitor_client_sock {|msg| yield msg} + if block_given? + monitor_client_sock {|msg| yield msg} + else + monitor_client_sock + end } + until self.complete + return self.return_data + end end def send_data(host,port) - data_blocks = blockify_file() + data_blocks = blockify_file_or_data() sent_data = 0 sent_blocks = 0 expected_blocks = data_blocks.size expected_size = data_blocks.join.size if block_given? - yield "Source file: #{self.local_file}, destination file: #{self.remote_file}" + yield "Source file: #{self.local_file =~ /^DATA:/ ? "(Data)" : self.remote_file}, destination file: #{self.remote_file}" yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" end data_blocks.each_with_index do |data_block,idx| @@ -225,11 +298,19 @@ class Client end if block_given? if(sent_data == expected_size) - yield "Upload complete!" + yield("Transferred #{sent_data} bytes in #{sent_blocks} blocks, upload complete!") else yield "Upload complete, but with errors." end end + if sent_data == expected_size + self.return_data = {:success => [ + self.local_file, + self.remote_file, + sent_data, + sent_blocks + ] } + end end end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 0c401ffcf7..4405e07d81 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -17,9 +17,17 @@ class Metasploit3 < Msf::Auxiliary super( 'Name' => 'TFTP File Transfer Utility', 'Description' => %q{ - This module will send file to a remote TFTP server. Note that the target - must be able to connect back to the Metasploit system, and NAT traversal - for TFTP is often unsupported. + This module will transfer a file to or from a remote TFTP server. + Note that the target must be able to connect back to the Metasploit system, + and NAT traversal for TFTP is often unsupported. + + Two actions are supported: "Upload" and "Download," which behave as one might + expect -- use 'set action Actionname' to use either mode of operation. + + If "Download" is selected, at least one of FILENAME or REMOTE_FILENAME + must be set. If "Upload" is selected, either FILENAME must be set to a valid path to + a source file, or FILEDATA must be populated. FILENAME may be a fully qualified path, + or the name of a file in the Msf::Config.local_directory or Msf::Config.data_directory. }, 'Author' => [ 'todb' ], 'References' => @@ -28,32 +36,25 @@ class Metasploit3 < Msf::Auxiliary ['URL', 'http://www.networksorcery.com/enp/protocol/tftp.htm'] ], 'Actions' => [ - [ 'Download', {'Description' => "Download REMOTE_FILENAME as FILENAME."}], - [ 'Upload', {'Description' => "Upload FILENAME as REMOTE_FILENAME to the server."}] + [ 'Download', {'Description' => "Download REMOTE_FILENAME as FILENAME from the server."}], + [ 'Upload', {'Description' => "Upload FILENAME as REMOTE_FILENAME to the server."}] ], 'DefaultAction' => 'Upload', 'License' => MSF_LICENSE ) register_options([ - OptPath.new( 'FILENAME', [false, "The local filename" ]), + OptString.new( 'FILENAME', [false, "The local filename" ]), + OptString.new( 'FILEDATA', [false, "Data to upload in lieu of a real local file." ]), OptString.new( 'REMOTE_FILENAME', [false, "The remote filename"]), OptAddress.new('RHOST', [true, "The remote TFTP server"]), OptPort.new( 'LPORT', [false, "The local port the TFTP client should listen on (default is random)" ]), OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]), OptBool.new( 'VERBOSE', [false, "Display verbose details about the transfer", false]), - OptString.new( 'MODE', [false, "The TFTP mode; usual choices are netascii and octet.", "octet"]), + OptString.new( 'MODE', [false, "The TFTP mode; usual choices are netascii and octet.", "octet"]), Opt::RPORT(69) ], self.class) end - def file - if action.name == "Upload" - datastore['FILENAME'] - else # "Download - fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last - end - end - def mode datastore['MODE'] || "octect" end @@ -70,6 +71,8 @@ class Metasploit3 < Msf::Auxiliary datastore['RHOST'] end + # Used only to store loot, doesn't actually have any semantic meaning + # for the TFTP protocol. def datatype case datastore['MODE'] when "netascii" @@ -79,6 +82,34 @@ class Metasploit3 < Msf::Auxiliary end end + + # The local filename must be real if you are uploading. Otherwise, + # it can be made up, since for downloading it's only used for the + # name of the loot entry. + def file + if action.name == "Upload" + fname = datastore['FILENAME'].to_s + if fname.empty? + fdata = "DATA:#{datastore['FILEDATA']}" + return fdata + else + if ::File.readable? fname + fname + else + fname_local = ::File.join(Msf::Config.local_directory,fname) + fname_data = ::File.join(Msf::Config.data_directory,fname) + return fname_local if ::File.readable? fname_local + return fname_data if ::File.readable? fname_data + return nil # Couldn't find it, giving up. + end + end + else # "Download + fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last rescue nil + end + end + + # Experimental message prepending thinger. Might make it up into the + # standard Metasploit lib like vprint_status and friends. def rtarget(ip=nil) if (ip or rhost) and rport [(ip || rhost),rport].map {|x| x.to_s}.join(":") << " " @@ -89,12 +120,16 @@ class Metasploit3 < Msf::Auxiliary end end + # At least one, but not both, are required. def check_valid_filename not (datastore['FILENAME'].to_s.empty? and datastore['REMOTE_FILENAME'].to_s.empty?) end - # - # TFTP is a funny service and needs to kind of be a server on our side, too. + # This all happens before run(), and should give an idea on how to use + # the TFTP client mixin. Essentially, you create an instance of the + # Rex::Proto::TFTP::Client class, fill it up with the relevant host and + # file data, set it to either :upload or :download, then kick off the + # transfer as you like. def setup unless check_valid_filename() print_error "Need at least one valid filename." @@ -106,14 +141,14 @@ class Metasploit3 < Msf::Auxiliary @remote_file = remote_file @tftp_client = Rex::Proto::TFTP::Client.new( - "LocalHost" => @lhost, - "LocalPort" => @lport, - "PeerHost" => rhost, - "PeerPort" => rport, - "LocalFile" => @local_file, + "LocalHost" => @lhost, + "LocalPort" => @lport, + "PeerHost" => rhost, + "PeerPort" => rport, + "LocalFile" => @local_file, "RemoteFile" => @remote_file, - "Mode" => mode, - "Action" => action.name.to_s.downcase.intern + "Mode" => mode, + "Action" => action.name.to_s.downcase.intern ) end @@ -124,28 +159,26 @@ class Metasploit3 < Msf::Auxiliary while true if @tftp_client.complete print_status [rtarget,"TFTP transfer operation complete."].join - if action.name == 'Download' - save_downloaded_file() - end + save_downloaded_file() if action.name == 'Download' break else - select(nil,nil,nil,1) + select(nil,nil,nil,1) # 1 second delays are just fine. end end end def run_upload print_status "Sending '#{file}' to #{rhost}:#{rport} as '#{remote_file}'" - @tftp_client.send_write_request { |msg| print_tftp_status(msg) } + ret = @tftp_client.send_write_request { |msg| print_tftp_status(msg) } end def run_download print_status "Receiving '#{remote_file}' from #{rhost}:#{rport} as '#{file}'" - @tftp_client.send_read_request { |msg| print_tftp_status(msg) } + ret = @tftp_client.send_read_request { |msg| print_tftp_status(msg) } end def save_downloaded_file - print_status "Saving #{remote_file} as #{file}" + print_status "Saving #{remote_file} as '#{file}'" fh = @tftp_client.recv_tempfile data = File.open(fh,"rb") {|f| f.read f.stat.size} rescue nil if data and not data.empty?