diff --git a/lib/rex/proto/tftp.rb b/lib/rex/proto/tftp.rb index 723acd0c5d..16bf85e582 100644 --- a/lib/rex/proto/tftp.rb +++ b/lib/rex/proto/tftp.rb @@ -10,3 +10,4 @@ require 'rex/proto/tftp/constants' require 'rex/proto/tftp/server' +require 'rex/proto/tftp/client' diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb new file mode 100644 index 0000000000..fb595489b0 --- /dev/null +++ b/lib/rex/proto/tftp/client.rb @@ -0,0 +1,343 @@ +require 'rex/socket' +require 'rex/proto/tftp' +require 'tempfile' + +module Rex +module Proto +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 transfer actions are 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 :status 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, :status + attr_accessor :block_size # This definitely breaks spec, should only use for fuzz/sploit. + + # Returns an array of [code, type, msg]. Data packets + # specifically will /not/ unpack, since that would drop any trailing spaces or nulls. + def parse_tftp_response(str) + return nil unless str.length >= 4 + ret = str.unpack("nnA*") + ret[2] = str[4,str.size] if ret[0] == OpData + return ret + end + + def initialize(params) + self.threads = [] + self.local_host = params["LocalHost"] || "0.0.0.0" + self.local_port = params["LocalPort"] || (1025 + rand(0xffff-1025)) + 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"] + 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.") + self.block_size = params["BlockSize"] || 512 + end + + # + # Methods for both upload and download + # + + def start_server_socket + self.server_sock = Rex::Socket::Udp.create( + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + if self.server_sock and block_given? + yield "Started TFTP client listener on #{local_host}:#{local_port}" + end + self.threads << Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { + if block_given? + monitor_server_sock {|msg| yield msg} + else + monitor_server_sock + end + } + end + + def monitor_server_sock + yield "Listening for incoming ACKs" if block_given? + res = self.server_sock.recvfrom(65535) + if res and res[0] + code, type, data = parse_tftp_response(res[0]) + if code == OpAck and self.action == :upload + if block_given? + yield "WRQ accepted, sending the file." if type == 0 + send_data(res[1], res[2]) {|msg| yield msg} + else + send_data(res[1], res[2]) + end + elsif code == OpData and self.action == :download + if block_given? + recv_data(res[1], res[2], data) {|msg| yield msg} + else + recv_data(res[1], res[2], data) + end + elsif code == OpError + yield("Aborting, got error type:%d, message:'%s'" % [type, data]) if block_given? + self.status = {:error => [code, type, data]} + else + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given? + self.status = {:error => [code, type, data]} + end + end + stop + end + + def monitor_client_sock + res = self.client_sock.recvfrom(65535) + if res[1] # Got a response back, so that's never good; Acks come back on server_sock. + code, type, data = parse_tftp_response(res[0]) + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given? + self.status = {:error => [code, type, data]} + stop + end + end + + def stop + self.complete = true + begin + self.server_sock.close + self.client_sock.close + self.server_sock = nil + self.client_sock = nil + self.threads.each {|t| t.kill} + rescue + nil + end + end + + # + # Methods for download + # + + def rrq_packet + req = [OpRead, self.remote_file, self.mode] + packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}" + req.pack(packstr) + end + + def ack_packet(blocknum=0) + req = [OpAck, blocknum].pack("nn") + end + + def send_read_request(&block) + self.status = nil + self.complete = false + if block_given? + start_server_socket {|msg| yield msg} + else + start_server_socket + end + self.client_sock = Rex::Socket::Udp.create( + 'PeerHost' => peer_host, + 'PeerPort' => peer_port, + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + self.client_sock.sendto(rrq_packet, peer_host, peer_port) + self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { + if block_given? + monitor_client_sock {|msg| yield msg} + else + monitor_client_sock + end + } + until self.complete + return self.status + end + end + + def recv_data(host, port, first_block) + self.recv_tempfile = Rex::Quickfile.new('msf-tftp') + recvd_blocks = 1 + if block_given? + yield "Source file: #{self.remote_file}, destination file: #{self.local_file}" + yield "Received and acknowledged #{first_block.size} in block #{recvd_blocks}" + end + 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_response(res[0]) + if code == 3 + 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? + stop + end + end + end + if block_given? + yield("Transferred #{self.recv_tempfile.size} bytes in #{recvd_blocks} blocks, download complete!") + end + self.status = {:success => [ + self.local_file, + self.remote_file, + self.recv_tempfile.size, + recvd_blocks.size] + } + self.recv_tempfile.close + stop + end + + def write_and_ack_data(data,blocknum,host,port) + self.recv_tempfile.write(data) + self.recv_tempfile.flush + req = ack_packet(blocknum) + self.server_sock.sendto(req, host, port) + yield "Received and acknowledged #{data.size} in block #{blocknum}" if block_given? + end + + # + # Methods for upload + # + + def wrq_packet + req = [OpWrite, self.remote_file, self.mode] + packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}" + req.pack(packstr) + end + + # 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. If it's missing + # completely, then just quit. + def blockify_file_or_data + if self.local_file =~ /^DATA:(.*)/m + data = $1 + elsif ::File.file?(self.local_file) and ::File.readable?(self.local_file) + data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} rescue [] + else + return [] + end + data_blocks = data.scan(/.{1,#{block_size}}/m) + # Drop any trailing empty blocks + if data_blocks.size > 1 and data_blocks.last.empty? + data_blocks.pop + end + return data_blocks + end + + def send_write_request(&block) + self.status = nil + self.complete = false + if block_given? + start_server_socket {|msg| yield msg} + else + start_server_socket + end + self.client_sock = Rex::Socket::Udp.create( + 'PeerHost' => peer_host, + 'PeerPort' => peer_port, + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + self.client_sock.sendto(wrq_packet, peer_host, peer_port) + self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { + if block_given? + monitor_client_sock {|msg| yield msg} + else + monitor_client_sock + end + } + until self.complete + return self.status + end + end + + def send_data(host,port) + self.status = {:write_allowed => true} + data_blocks = blockify_file_or_data() + if data_blocks.empty? + yield "Closing down since there is no data to send." if block_given? + self.status = {:success => [self.local_file, self.local_file, 0, 0]} + return nil + end + 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 =~ /^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| + req = [OpData, (idx + 1), data_block].pack("nnA*") + if self.server_sock.sendto(req, host, port) > 0 + sent_data += data_block.size + end + res = self.server_sock.recvfrom(65535) + if res + code, type, msg = parse_tftp_response(res[0]) + if code == 4 + sent_blocks += 1 + yield "Sent #{data_block.size} bytes in block #{sent_blocks}" if block_given? + else + if block_given? + yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg] + end + break + end + end + end + if block_given? + if(sent_data == expected_size) + 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.status = {:success => [ + self.local_file, + self.remote_file, + sent_data, + sent_blocks + ] } + end + end + +end + +end +end +end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb new file mode 100644 index 0000000000..351e56e980 --- /dev/null +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -0,0 +1,209 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/framework/ +## + + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + + include Rex::Proto::TFTP + include Msf::Auxiliary::Report + + def initialize + super( + 'Name' => 'TFTP File Transfer Utility', + 'Description' => %q{ + 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' => + [ + ['URL', 'http://www.faqs.org/rfcs/rfc1350.html'], + ['URL', 'http://www.networksorcery.com/enp/protocol/tftp.htm'] + ], + 'Actions' => [ + [ '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([ + 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"]), + Opt::RPORT(69) + ], self.class) + end + + def mode + datastore['MODE'] || "octect" + end + + def remote_file + datastore['REMOTE_FILENAME'] || ::File.split(datastore['FILENAME']).last + end + + def rport + datastore['RPORT'] || 69 + end + + def rhost + 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" + "text/plain" + else + "application/octet-stream" + end + end + + def file + if action.name == "Upload" + fdata = datastore['FILEDATA'].to_s + fname = datastore['FILENAME'].to_s + if not fdata.empty? + fdata_decorated = "DATA:#{datastore['FILEDATA']}" + elsif ::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.file?(fname_local) and ::File.readable?(fname_local) + return fname_data if ::File.file?(fname_data) and ::File.readable?(fname_data) + return nil # Couldn't find it, giving up. + 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(":") << " " + elsif (ip or rhost) + "#{rhost} " + else + "" + end + end + + # 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 + @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) + @lhost = datastore['LHOST'] || "0.0.0.0" + @local_file = file + @remote_file = remote_file + + @tftp_client = Rex::Proto::TFTP::Client.new( + "LocalHost" => @lhost, + "LocalPort" => @lport, + "PeerHost" => rhost, + "PeerPort" => rport, + "LocalFile" => @local_file, + "RemoteFile" => @remote_file, + "Mode" => mode, + "Action" => action.name.to_s.downcase.intern + ) + end + + def run + run_upload() if action.name == 'Upload' + run_download() if action.name == 'Download' + while not @tftp_client.complete + select(nil,nil,nil,1) + print_status [rtarget,"TFTP transfer operation complete."].join + save_downloaded_file() if action.name == 'Download' + break + end + end + + # Run in case something untoward happend with the connection and the + # client object didn't get stopped on its own. This can happen with + # transfers that got interrupted or malformed (like sending a 0 byte + # file). + def cleanup + if @tftp_client and @tftp_client.respond_to? :complete + while not @tftp_client.complete + select(nil,nil,nil,1) + vprint_status "Cleaning up the TFTP client ports and threads." + @tftp_client.stop + end + end + end + + def run_upload + print_status "Sending '#{file}' to #{rhost}:#{rport} as '#{remote_file}'" + 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}'" + ret = @tftp_client.send_read_request { |msg| print_tftp_status(msg) } + end + + def save_downloaded_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? + unless framework.db.active + print_status "No database connected, so not actually saving the data:" + print_line data + end + this_service = report_service( + :host => rhost, + :port => rport, + :name => "tftp", + :proto => "udp" + ) + store_loot("tftp.file",datatype,rhost,data,file,remote_file,this_service) + else + print_status [rtarget,"Did not find any data, so nothing to save."].join + end + fh.unlink rescue nil # Windows often complains about unlinking tempfiles + end + + def print_tftp_status(msg) + case msg + when /Aborting/, /errors.$/ + print_error [rtarget,msg].join + when /^WRQ accepted/, /^Sending/, /complete!$/ + print_good [rtarget,msg].join + else + vprint_status [rtarget,msg].join + end + end + +end +