From a6867ef1287af1786b2e0cd6e7b13fdb8099ce2a Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 16 Dec 2011 18:39:09 -0600 Subject: [PATCH 01/11] First draft of a TFTP client. Could use some actual error checking and also needs to expose more options. --- lib/rex/proto/tftp.rb | 1 + lib/rex/proto/tftp/client.rb | 144 ++++++++++++++++++ .../auxiliary/admin/tftp/tftp_upload_file.rb | 99 ++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 lib/rex/proto/tftp/client.rb create mode 100644 modules/auxiliary/admin/tftp/tftp_upload_file.rb 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..c2f122167c --- /dev/null +++ b/lib/rex/proto/tftp/client.rb @@ -0,0 +1,144 @@ +# $Id$ +require 'rex/socket' +require 'rex/proto/tftp' + +module Rex +module Proto +module TFTP + + +# +# TFTP Client class +# +# Note that TFTP has blocks, and so does Ruby. Watch out with the variable names! +class Client + + attr_accessor :local_host, :local_port, :peer_host, :peer_port + attr_accessor :thread, :context, :sock, :write_sock + attr_accessor :local_file, :remote_file + + def initialize(params) + 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"] || (raise ArgumentError, "Need a file to send.") + self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last + self.sock = nil + @shutting_down = false + end + + def blockify_file + data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} + data.scan(/.{1,512}/) + end + + def send_data(host,port) + data_blocks = blockify_file() + 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 "Sending #{expected_size} bytes (#{expected_blocks} blocks)" + end + data_blocks.each_with_index do |data_block,idx| + req = ["\x00\x03", (idx + 1), data_block].pack("A2nA*") + if self.sock.sendto(req, host, port) > 0 + sent_data += data_block.size + end + res = self.sock.recvfrom(65535, 5) + if res[0] and res[0] =~ /^\x00\x04/ + # emit a status + sent_blocks += 1 + yield "Sent #{data_block.size} bytes in block #{sent_blocks}" if block_given? + else + yield "Got an unexpected response: `#{res[0].inspect}' ; Aborting." if block_given? + break # and probably yell about it + end + end + if block_given? + if(sent_data == expected_size) + yield "Transfer complete!" + else + yield "Transfer complete, but with errors." + end + end + end + + def monitor_socket + yield "Listening for incoming ACKs" if block_given? + res = self.sock.recvfrom(65535, 5) + if res[0] and res[0] =~ /^\x00\x04/ + send_data(res[1], res[2]) {|msg| yield msg} + stop + end + end + + def start_client_port + self.sock = Rex::Socket::Udp.create( + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + if self.sock and block_given? + yield "Started TFTP client listener on #{local_host}:#{local_port}" + end + + self.thread = Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { + monitor_socket {|msg| yield msg} + } + end + + def wrq_packet + req = "\x00\x02" + req += self.remote_file + req += "\x00" + req += "netascii" + req += "\x00" + end + + def send_write_request(&block) + if block_given? + start_client_port {|msg| yield msg} + else + start_client_port + end + self.write_sock = Rex::Socket::Udp.create( + 'PeerHost' => peer_host, + 'PeerPort' => peer_port, + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + self.write_sock.sendto(wrq_packet, peer_host, peer_port) + self.write_sock.close rescue nil + end + + def stop + @shutting_down = true + self.thread.kill + self.sock.close rescue nil # might be closed already + end + + # + # Send an error packet w/the specified code and string + # + def send_error(from, num) + if (num < 1 or num >= ERRCODES.length) + # ignore.. + return + end + pkt = [OpError, num].pack('nn') + pkt << ERRCODES[num] + pkt << "\x00" + send_packet(from, pkt) + 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..886d277f34 --- /dev/null +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -0,0 +1,99 @@ +## +# 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 + + def initialize + 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. + }, + 'Author' => [ 'todb' ], + 'License' => MSF_LICENSE + ) + register_options([ + OptPath.new('FILENAME', [true, "The local file to upload" ]), + OptString.new('REMOTE_FILENAME', [false, "The filename to provide to the TFTP server" ]), + OptAddress.new('RHOST', [true, "The remote TFTP server"]), + OptPort.new('LPORT', [false, "The local port the TFTP client should listen on" ]), + OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]), + OptBool.new('VERBOSE', [false, "Provide more details about the transfer", false]), + Opt::RPORT(69) + ], self.class) + end + + def file + datastore['FILENAME'] + end + + def remote_file + datastore['REMOTE_FILENAME'] || ::File.split(file).last + end + + def rport + datastore['RPORT'] + end + + def rhost + datastore['RHOST'] + end + + 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 + + # + # TFTP is a funny service and needs to kind of be a server on our side, too. + # Setup is called only once + def setup + @rport = datastore['RPORT'] || 69 + @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) + @lhost = datastore['LHOST'] || "0.0.0.0" + @path = datastore['FILENAME'] + @filename = ::File.split(@path).last + + @tftp_client = Rex::Proto::TFTP::Client.new( + "LocalHost" => @lhost, + "LocalPort" => @lport, + "PeerHost" => rhost, + "PeerPort" => rport, + "LocalFile" => file, + "RemoteFile" => remote_file + ) + end + + def run + print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" + @tftp_client.send_write_request do |msg| + case msg + when /Aborting.$/, /errors.$/ + print_error [rtarget,msg].join + when /^Sending/, /complete!$/ + print_good [rtarget,msg].join + else + print_status [rtarget,msg].join if datastore['VERBOSE'] + end + end + @tftp_client.thread.join + end + +end + From 50fa10679bc36b7ef33d74de3dfc1ffff9f9192f Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 16 Dec 2011 18:39:09 -0600 Subject: [PATCH 02/11] First draft of a TFTP client. Could use some actual error checking and also needs to expose more options. --- lib/rex/proto/tftp.rb | 1 + lib/rex/proto/tftp/client.rb | 144 ++++++++++++++++++ .../auxiliary/admin/tftp/tftp_upload_file.rb | 99 ++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 lib/rex/proto/tftp/client.rb create mode 100644 modules/auxiliary/admin/tftp/tftp_upload_file.rb 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..c2f122167c --- /dev/null +++ b/lib/rex/proto/tftp/client.rb @@ -0,0 +1,144 @@ +# $Id$ +require 'rex/socket' +require 'rex/proto/tftp' + +module Rex +module Proto +module TFTP + + +# +# TFTP Client class +# +# Note that TFTP has blocks, and so does Ruby. Watch out with the variable names! +class Client + + attr_accessor :local_host, :local_port, :peer_host, :peer_port + attr_accessor :thread, :context, :sock, :write_sock + attr_accessor :local_file, :remote_file + + def initialize(params) + 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"] || (raise ArgumentError, "Need a file to send.") + self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last + self.sock = nil + @shutting_down = false + end + + def blockify_file + data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} + data.scan(/.{1,512}/) + end + + def send_data(host,port) + data_blocks = blockify_file() + 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 "Sending #{expected_size} bytes (#{expected_blocks} blocks)" + end + data_blocks.each_with_index do |data_block,idx| + req = ["\x00\x03", (idx + 1), data_block].pack("A2nA*") + if self.sock.sendto(req, host, port) > 0 + sent_data += data_block.size + end + res = self.sock.recvfrom(65535, 5) + if res[0] and res[0] =~ /^\x00\x04/ + # emit a status + sent_blocks += 1 + yield "Sent #{data_block.size} bytes in block #{sent_blocks}" if block_given? + else + yield "Got an unexpected response: `#{res[0].inspect}' ; Aborting." if block_given? + break # and probably yell about it + end + end + if block_given? + if(sent_data == expected_size) + yield "Transfer complete!" + else + yield "Transfer complete, but with errors." + end + end + end + + def monitor_socket + yield "Listening for incoming ACKs" if block_given? + res = self.sock.recvfrom(65535, 5) + if res[0] and res[0] =~ /^\x00\x04/ + send_data(res[1], res[2]) {|msg| yield msg} + stop + end + end + + def start_client_port + self.sock = Rex::Socket::Udp.create( + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + if self.sock and block_given? + yield "Started TFTP client listener on #{local_host}:#{local_port}" + end + + self.thread = Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { + monitor_socket {|msg| yield msg} + } + end + + def wrq_packet + req = "\x00\x02" + req += self.remote_file + req += "\x00" + req += "netascii" + req += "\x00" + end + + def send_write_request(&block) + if block_given? + start_client_port {|msg| yield msg} + else + start_client_port + end + self.write_sock = Rex::Socket::Udp.create( + 'PeerHost' => peer_host, + 'PeerPort' => peer_port, + 'LocalHost' => local_host, + 'LocalPort' => local_port, + 'Context' => context + ) + self.write_sock.sendto(wrq_packet, peer_host, peer_port) + self.write_sock.close rescue nil + end + + def stop + @shutting_down = true + self.thread.kill + self.sock.close rescue nil # might be closed already + end + + # + # Send an error packet w/the specified code and string + # + def send_error(from, num) + if (num < 1 or num >= ERRCODES.length) + # ignore.. + return + end + pkt = [OpError, num].pack('nn') + pkt << ERRCODES[num] + pkt << "\x00" + send_packet(from, pkt) + 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..886d277f34 --- /dev/null +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -0,0 +1,99 @@ +## +# 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 + + def initialize + 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. + }, + 'Author' => [ 'todb' ], + 'License' => MSF_LICENSE + ) + register_options([ + OptPath.new('FILENAME', [true, "The local file to upload" ]), + OptString.new('REMOTE_FILENAME', [false, "The filename to provide to the TFTP server" ]), + OptAddress.new('RHOST', [true, "The remote TFTP server"]), + OptPort.new('LPORT', [false, "The local port the TFTP client should listen on" ]), + OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]), + OptBool.new('VERBOSE', [false, "Provide more details about the transfer", false]), + Opt::RPORT(69) + ], self.class) + end + + def file + datastore['FILENAME'] + end + + def remote_file + datastore['REMOTE_FILENAME'] || ::File.split(file).last + end + + def rport + datastore['RPORT'] + end + + def rhost + datastore['RHOST'] + end + + 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 + + # + # TFTP is a funny service and needs to kind of be a server on our side, too. + # Setup is called only once + def setup + @rport = datastore['RPORT'] || 69 + @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) + @lhost = datastore['LHOST'] || "0.0.0.0" + @path = datastore['FILENAME'] + @filename = ::File.split(@path).last + + @tftp_client = Rex::Proto::TFTP::Client.new( + "LocalHost" => @lhost, + "LocalPort" => @lport, + "PeerHost" => rhost, + "PeerPort" => rport, + "LocalFile" => file, + "RemoteFile" => remote_file + ) + end + + def run + print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" + @tftp_client.send_write_request do |msg| + case msg + when /Aborting.$/, /errors.$/ + print_error [rtarget,msg].join + when /^Sending/, /complete!$/ + print_good [rtarget,msg].join + else + print_status [rtarget,msg].join if datastore['VERBOSE'] + end + end + @tftp_client.thread.join + end + +end + From 0b8914021c6151d84a0776c2757fce1fa2ee2875 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 16 Dec 2011 21:06:10 -0600 Subject: [PATCH 03/11] Switch to vprint_status, also add skeletal cleanup def. --- modules/auxiliary/admin/tftp/tftp_upload_file.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 886d277f34..33afbc8350 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -89,11 +89,15 @@ class Metasploit3 < Msf::Auxiliary when /^Sending/, /complete!$/ print_good [rtarget,msg].join else - print_status [rtarget,msg].join if datastore['VERBOSE'] + vprint_status [rtarget,msg].join end end @tftp_client.thread.join end + def cleanup + # Need to kill the server in case of interruption + end + end From 23aadd04f70253841a856e069752b5727a79bf6d Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Sun, 18 Dec 2011 13:28:52 -0600 Subject: [PATCH 04/11] Fixing merge conflict cruft Dangit teach me to merge quickly. TFTP module now loads again. --- modules/auxiliary/admin/tftp/tftp_upload_file.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index d3cb201dc1..33afbc8350 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -95,12 +95,9 @@ class Metasploit3 < Msf::Auxiliary @tftp_client.thread.join end -<<<<<<< HEAD def cleanup # Need to kill the server in case of interruption end -======= ->>>>>>> a6867ef1287af1786b2e0cd6e7b13fdb8099ce2a end From 902d7f5ea7c374920e13fa20dbc1aff6e953f301 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Sun, 18 Dec 2011 21:05:27 -0600 Subject: [PATCH 05/11] Adding more to TFTP. Still need a read tho Adds error checking and some helpful messaging in the event of an error. In the event of a failed transfer the module exits immediately, but in success, I'm still hanging around for several seconds after. Not a deal breaker but can be annoying. Also, need to implement a read as well as a write and store it as loot, to be actually useful for most TFTP checking. --- lib/rex/proto/tftp/client.rb | 78 +++++++++++-------- .../auxiliary/admin/tftp/tftp_upload_file.rb | 15 +++- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index c2f122167c..a6d7430608 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -15,7 +15,14 @@ class Client attr_accessor :local_host, :local_port, :peer_host, :peer_port attr_accessor :thread, :context, :sock, :write_sock - attr_accessor :local_file, :remote_file + attr_accessor :local_file, :remote_file, :mode + + # Returns an array of [code, type, msg] + def parse_tftp_msg(str) + return nil unless str.length >= 4 + ret = str.unpack("nnA*") + return ret + end def initialize(params) self.local_host = params["LocalHost"] || "0.0.0.0" @@ -25,8 +32,8 @@ class Client self.context = params["Context"] || {} self.local_file = params["LocalFile"] || (raise ArgumentError, "Need a file to send.") self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last + self.mode = params["Mode"] || "octet" self.sock = nil - @shutting_down = false end def blockify_file @@ -45,18 +52,22 @@ class Client yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" end data_blocks.each_with_index do |data_block,idx| - req = ["\x00\x03", (idx + 1), data_block].pack("A2nA*") + req = [3, (idx + 1), data_block].pack("nnA*") if self.sock.sendto(req, host, port) > 0 sent_data += data_block.size end - res = self.sock.recvfrom(65535, 5) - if res[0] and res[0] =~ /^\x00\x04/ - # emit a status - sent_blocks += 1 - yield "Sent #{data_block.size} bytes in block #{sent_blocks}" if block_given? - else - yield "Got an unexpected response: `#{res[0].inspect}' ; Aborting." if block_given? - break # and probably yell about it + res = self.sock.recvfrom(65535) + if res + code, type, msg = parse_tftp_msg(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? @@ -69,12 +80,12 @@ class Client end def monitor_socket - yield "Listening for incoming ACKs" if block_given? - res = self.sock.recvfrom(65535, 5) - if res[0] and res[0] =~ /^\x00\x04/ - send_data(res[1], res[2]) {|msg| yield msg} - stop - end + yield "Listening for incoming ACKs" if block_given? + res = self.sock.recvfrom(65535) + if res[0] and res[0] =~ /^\x00\x04/ + send_data(res[1], res[2]) {|msg| yield msg} + end + stop end def start_client_port @@ -96,7 +107,7 @@ class Client req = "\x00\x02" req += self.remote_file req += "\x00" - req += "netascii" + req += self.mode req += "\x00" end @@ -114,27 +125,26 @@ class Client 'Context' => context ) self.write_sock.sendto(wrq_packet, peer_host, peer_port) - self.write_sock.close rescue nil + res = self.write_sock.recvfrom(65535) + if res + code, type, msg = parse_tftp_msg(res[0]) + case code + when 1 + yield "WRQ accepted, sending file" if block_given? + self.write_sock.close rescue nil + when nil + stop + else + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + stop + end + end end def stop - @shutting_down = true self.thread.kill self.sock.close rescue nil # might be closed already - end - - # - # Send an error packet w/the specified code and string - # - def send_error(from, num) - if (num < 1 or num >= ERRCODES.length) - # ignore.. - return - end - pkt = [OpError, num].pack('nn') - pkt << ERRCODES[num] - pkt << "\x00" - send_packet(from, pkt) + self.write_sock.close rescue nil # might be closed already end end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 33afbc8350..b5a5aabd79 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -30,6 +30,7 @@ class Metasploit3 < Msf::Auxiliary OptPort.new('LPORT', [false, "The local port the TFTP client should listen on" ]), OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]), OptBool.new('VERBOSE', [false, "Provide more details about the transfer", false]), + OptString.new('MODE', [false, "The TFTP mode. Usual choices are netsacii and octet.", "octet"]), Opt::RPORT(69) ], self.class) end @@ -38,6 +39,10 @@ class Metasploit3 < Msf::Auxiliary datastore['FILENAME'] end + def mode + datastore['MODE'] || "octect" + end + def remote_file datastore['REMOTE_FILENAME'] || ::File.split(file).last end @@ -76,7 +81,8 @@ class Metasploit3 < Msf::Auxiliary "PeerHost" => rhost, "PeerPort" => rport, "LocalFile" => file, - "RemoteFile" => remote_file + "RemoteFile" => remote_file, + "Mode" => mode ) end @@ -84,15 +90,16 @@ class Metasploit3 < Msf::Auxiliary print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" @tftp_client.send_write_request do |msg| case msg - when /Aborting.$/, /errors.$/ + when /Aborting/, /errors.$/ print_error [rtarget,msg].join - when /^Sending/, /complete!$/ + @tftp_client.thread.kill + when /^WRQ accepted/, /^Sending/, /complete!$/ print_good [rtarget,msg].join + when /complete!$/ else vprint_status [rtarget,msg].join end end - @tftp_client.thread.join end def cleanup From aecde6fea453586319d8ddd009d020e1041f620c Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 12:14:40 -0600 Subject: [PATCH 06/11] Updating TFTP client. Now with grown-up thread handling. No longer blocks on successful connections. --- lib/rex/proto/tftp/client.rb | 80 ++++++++++--------- .../auxiliary/admin/tftp/tftp_upload_file.rb | 27 ++++--- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index a6d7430608..3fb190f7c0 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -14,8 +14,9 @@ module TFTP class Client attr_accessor :local_host, :local_port, :peer_host, :peer_port - attr_accessor :thread, :context, :sock, :write_sock + attr_accessor :threads, :context, :server_sock, :client_sock attr_accessor :local_file, :remote_file, :mode + attr_accessor :complete # Returns an array of [code, type, msg] def parse_tftp_msg(str) @@ -25,6 +26,7 @@ class Client 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.") @@ -33,7 +35,6 @@ class Client self.local_file = params["LocalFile"] || (raise ArgumentError, "Need a file to send.") self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last self.mode = params["Mode"] || "octet" - self.sock = nil end def blockify_file @@ -53,10 +54,10 @@ class Client end data_blocks.each_with_index do |data_block,idx| req = [3, (idx + 1), data_block].pack("nnA*") - if self.sock.sendto(req, host, port) > 0 + if self.server_sock.sendto(req, host, port) > 0 sent_data += data_block.size end - res = self.sock.recvfrom(65535) + res = self.server_sock.recvfrom(65535) if res code, type, msg = parse_tftp_msg(res[0]) if code == 4 @@ -72,35 +73,36 @@ class Client end if block_given? if(sent_data == expected_size) - yield "Transfer complete!" + yield "Upload complete!" else - yield "Transfer complete, but with errors." + yield "Upload complete, but with errors." end end end - def monitor_socket - yield "Listening for incoming ACKs" if block_given? - res = self.sock.recvfrom(65535) - if res[0] and res[0] =~ /^\x00\x04/ - send_data(res[1], res[2]) {|msg| yield msg} - end - stop - end - - def start_client_port - self.sock = Rex::Socket::Udp.create( + def start_server_socket + self.server_sock = Rex::Socket::Udp.create( 'LocalHost' => local_host, 'LocalPort' => local_port, 'Context' => context ) - if self.sock and block_given? + if self.server_sock and block_given? yield "Started TFTP client listener on #{local_host}:#{local_port}" end - self.thread = Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { - monitor_socket {|msg| yield msg} + self.threads << Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { + monitor_server_sock {|msg| yield msg} } + + end + + def monitor_server_sock + yield "Listening for incoming ACKs" if block_given? + res = self.server_sock.recvfrom(65535) + if res[0] and res[0] =~ /^\x00\x04/ + send_data(res[1], res[2]) {|msg| yield msg} + end + stop end def wrq_packet @@ -113,38 +115,38 @@ class Client def send_write_request(&block) if block_given? - start_client_port {|msg| yield msg} + start_server_socket {|msg| yield msg} else - start_client_port + start_server_socket end - self.write_sock = Rex::Socket::Udp.create( + self.client_sock = Rex::Socket::Udp.create( 'PeerHost' => peer_host, 'PeerPort' => peer_port, 'LocalHost' => local_host, 'LocalPort' => local_port, 'Context' => context ) - self.write_sock.sendto(wrq_packet, peer_host, peer_port) - res = self.write_sock.recvfrom(65535) - if res + self.client_sock.sendto(wrq_packet, peer_host, peer_port) + + self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { + monitor_client_sock {|msg| yield msg} + } + 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, msg = parse_tftp_msg(res[0]) - case code - when 1 - yield "WRQ accepted, sending file" if block_given? - self.write_sock.close rescue nil - when nil - stop - else - yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? - stop - end + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + stop end end def stop - self.thread.kill - self.sock.close rescue nil # might be closed already - self.write_sock.close rescue nil # might be closed already + self.complete = true + self.threads.each {|t| t.kill} + self.server_sock.close rescue nil # might be closed already + self.client_sock.close rescue nil # might be closed already end end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index b5a5aabd79..0015f544da 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -67,7 +67,6 @@ class Metasploit3 < Msf::Auxiliary # # TFTP is a funny service and needs to kind of be a server on our side, too. - # Setup is called only once def setup @rport = datastore['RPORT'] || 69 @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) @@ -89,21 +88,27 @@ class Metasploit3 < Msf::Auxiliary def run print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" @tftp_client.send_write_request do |msg| - case msg - when /Aborting/, /errors.$/ - print_error [rtarget,msg].join - @tftp_client.thread.kill - when /^WRQ accepted/, /^Sending/, /complete!$/ - print_good [rtarget,msg].join - when /complete!$/ + print_tftp_status(msg) + end + while true + if @tftp_client.complete + print_status [rtarget,"TFTP transfer operation complete."].join + break else - vprint_status [rtarget,msg].join + select(nil,nil,nil,1) end end end - def cleanup - # Need to kill the server in case of interruption + 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 From 5eaf2e75356337855b4e30bec7fbb69ff8609e19 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 15:50:50 -0600 Subject: [PATCH 07/11] Adding download and loot functionality. Still need to deal with the use case of not passing a block; blocks should not be required, it should be okay to invoke and just wait for the complete attribute to be true. You'll miss out on error messages but eh, maybe those should be return values. --- lib/rex/proto/tftp/client.rb | 239 ++++++++++++------ .../auxiliary/admin/tftp/tftp_upload_file.rb | 108 ++++++-- 2 files changed, 250 insertions(+), 97 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index 3fb190f7c0..04ab1411ed 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -1,6 +1,7 @@ # $Id$ require 'rex/socket' require 'rex/proto/tftp' +require 'tempfile' module Rex module Proto @@ -15,13 +16,16 @@ 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 - attr_accessor :complete + attr_accessor :local_file, :remote_file, :mode, :action + attr_accessor :complete, :recv_tempfile - # Returns an array of [code, type, msg] - def parse_tftp_msg(str) + # Returns an array of [code, type, msg]. Data packets + # should set strip to false, or else trailing spaces and nulls + # will be dropped during unpacking. + def parse_tftp_msg(str,strip=true) return nil unless str.length >= 4 ret = str.unpack("nnA*") + ret[2] = str[4,str.size] unless strip return ret end @@ -32,9 +36,142 @@ 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 file to send.") + self.local_file = params["LocalFile"] || (raise ArgumentError, "Need a local file.") 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.") + 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) { + monitor_server_sock {|msg| yield msg} + } + 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_msg(res[0]) + if code == 4 && self.action == :upload + send_data(res[1], res[2]) {|msg| yield msg} + elsif code == 3 && self.action == :download + recv_data(res[1], res[2], data) {|msg| yield msg} + else + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + stop + 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, msg = parse_tftp_msg(res[0]) + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + stop + end + end + + def stop + self.complete = true + self.threads.each {|t| t.kill} + self.server_sock.close rescue nil # might be closed already + self.client_sock.close rescue nil # might be closed already + 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) + 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) { + monitor_client_sock {|msg| yield msg} + } + 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 + write_and_ack_data(first_block,1,host,port) {|msg| yield msg} + 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} + recvd_blocks += 1 + else + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + stop + end + end + end + yield("Transferred #{recvd_blocks} blocks, download complete!") + 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}" + end + + + # + # Methods for upload + # + + def wrq_packet + req = "\x00\x02" + req += self.remote_file + req += "\x00" + req += self.mode + req += "\x00" end def blockify_file @@ -42,13 +179,32 @@ class Client data.scan(/.{1,512}/) end + def send_write_request(&block) + 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) { + monitor_client_sock {|msg| yield msg} + } + end + def send_data(host,port) data_blocks = blockify_file() sent_data = 0 sent_blocks = 0 expected_blocks = data_blocks.size expected_size = data_blocks.join.size - if block_given? + if block_given? yield "Source file: #{self.local_file}, destination file: #{self.remote_file}" yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" end @@ -67,7 +223,7 @@ class Client if block_given? yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg] end - break + break end end end @@ -80,75 +236,6 @@ class Client end end - 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) { - monitor_server_sock {|msg| yield msg} - } - - end - - def monitor_server_sock - yield "Listening for incoming ACKs" if block_given? - res = self.server_sock.recvfrom(65535) - if res[0] and res[0] =~ /^\x00\x04/ - send_data(res[1], res[2]) {|msg| yield msg} - end - stop - end - - def wrq_packet - req = "\x00\x02" - req += self.remote_file - req += "\x00" - req += self.mode - req += "\x00" - end - - def send_write_request(&block) - 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) { - monitor_client_sock {|msg| yield msg} - } - 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, msg = parse_tftp_msg(res[0]) - yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? - stop - end - end - - def stop - self.complete = true - self.threads.each {|t| t.kill} - self.server_sock.close rescue nil # might be closed already - self.client_sock.close rescue nil # might be closed already - end - end end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 0015f544da..85f16c96d2 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -11,6 +11,7 @@ require 'msf/core' class Metasploit3 < Msf::Auxiliary include Rex::Proto::TFTP + include Msf::Auxiliary::Report def initialize super( @@ -21,22 +22,36 @@ class Metasploit3 < Msf::Auxiliary for TFTP is often unsupported. }, '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."}], + [ 'Upload', {'Description' => "Upload FILENAME as REMOTE_FILENAME to the server."}] + ], + 'DefaultAction' => 'Upload', 'License' => MSF_LICENSE ) register_options([ - OptPath.new('FILENAME', [true, "The local file to upload" ]), - OptString.new('REMOTE_FILENAME', [false, "The filename to provide to the TFTP server" ]), - OptAddress.new('RHOST', [true, "The remote TFTP server"]), - OptPort.new('LPORT', [false, "The local port the TFTP client should listen on" ]), - OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]), - OptBool.new('VERBOSE', [false, "Provide more details about the transfer", false]), - OptString.new('MODE', [false, "The TFTP mode. Usual choices are netsacii and octet.", "octet"]), + OptPath.new( 'FILENAME', [false, "The local filename" ]), + 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 file - datastore['FILENAME'] + if action.name == "Upload" + datastore['FILENAME'] + else # "Download + fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last + end end def mode @@ -44,17 +59,26 @@ class Metasploit3 < Msf::Auxiliary end def remote_file - datastore['REMOTE_FILENAME'] || ::File.split(file).last + datastore['REMOTE_FILENAME'] || ::File.split(datastore['FILENAME']).last end def rport - datastore['RPORT'] + datastore['RPORT'] || 69 end def rhost datastore['RHOST'] end + def datatype + case datastore['MODE'] + when "netascii" + "text/plain" + else + "application/octet-stream" + end + end + def rtarget(ip=nil) if (ip or rhost) and rport [(ip || rhost),rport].map {|x| x.to_s}.join(":") << " " @@ -65,34 +89,44 @@ class Metasploit3 < Msf::Auxiliary end end + 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. def setup - @rport = datastore['RPORT'] || 69 + unless check_valid_filename() + print_error "Need at least one valid filename." + return + end @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) @lhost = datastore['LHOST'] || "0.0.0.0" - @path = datastore['FILENAME'] - @filename = ::File.split(@path).last + @local_file = file + @remote_file = remote_file @tftp_client = Rex::Proto::TFTP::Client.new( "LocalHost" => @lhost, "LocalPort" => @lport, "PeerHost" => rhost, "PeerPort" => rport, - "LocalFile" => file, - "RemoteFile" => remote_file, - "Mode" => mode + "LocalFile" => @local_file, + "RemoteFile" => @remote_file, + "Mode" => mode, + "Action" => action.name.to_s.downcase.intern ) end def run - print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" - @tftp_client.send_write_request do |msg| - print_tftp_status(msg) - end + return unless check_valid_filename() + run_upload() if action.name == 'Upload' + run_download() if action.name == 'Download' while true if @tftp_client.complete print_status [rtarget,"TFTP transfer operation complete."].join + if action.name == 'Download' + save_downloaded_file() + end break else select(nil,nil,nil,1) @@ -100,6 +134,38 @@ class Metasploit3 < Msf::Auxiliary 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) } + end + + def run_download + print_status "Receiving '#{remote_file}' from #{rhost}:#{rport} as '#{file}'" + @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 + unless framework.db.active + print_status "No database connected, so not actually saving the data:" + print_line data + end + if data and not data.empty? + 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.$/ @@ -107,7 +173,7 @@ class Metasploit3 < Msf::Auxiliary when /^WRQ accepted/, /^Sending/, /complete!$/ print_good [rtarget,msg].join else - vprint_status [rtarget,msg].join + vprint_status [rtarget,msg].join end end From 431ef826c99593d91c8ce194837220464c9d6491 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 16:33:25 -0600 Subject: [PATCH 08/11] TFTP client now uses constants, preserves trailing spaces/nulls in data See #5291, just rediscovered the bug on this. --- lib/rex/proto/tftp/client.rb | 24 ++++++++----------- .../auxiliary/admin/tftp/tftp_upload_file.rb | 8 +++---- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index 04ab1411ed..609c96419b 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -20,12 +20,11 @@ class Client attr_accessor :complete, :recv_tempfile # Returns an array of [code, type, msg]. Data packets - # should set strip to false, or else trailing spaces and nulls - # will be dropped during unpacking. - def parse_tftp_msg(str,strip=true) + # specifically will /not/ unpack, since that would drop any trailing spaces or nulls. + def parse_tftp_msg(str) return nil unless str.length >= 4 ret = str.unpack("nnA*") - ret[2] = str[4,str.size] unless strip + ret[2] = str[4,str.size] if ret[0] == OpData return ret end @@ -65,9 +64,9 @@ class Client res = self.server_sock.recvfrom(65535) if res and res[0] code, type, data = parse_tftp_msg(res[0]) - if code == 4 && self.action == :upload + if code == OpAck && self.action == :upload send_data(res[1], res[2]) {|msg| yield msg} - elsif code == 3 && self.action == :download + elsif code == OpData && self.action == :download recv_data(res[1], res[2], data) {|msg| yield msg} else yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? @@ -148,7 +147,7 @@ class Client end end end - yield("Transferred #{recvd_blocks} blocks, download complete!") + yield("Transferred #{recvd_blocks} blocks, #{self.recv_tempfile.size} bytes, download complete!") self.recv_tempfile.close stop end @@ -161,17 +160,14 @@ class Client yield "Received and acknowledged #{data.size} in block #{blocknum}" end - # # Methods for upload # def wrq_packet - req = "\x00\x02" - req += self.remote_file - req += "\x00" - req += self.mode - req += "\x00" + req = [OpWrite, self.remote_file, self.mode] + packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}" + req.pack(packstr) end def blockify_file @@ -209,7 +205,7 @@ class Client yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" end data_blocks.each_with_index do |data_block,idx| - req = [3, (idx + 1), data_block].pack("nnA*") + req = [OpData, (idx + 1), data_block].pack("nnA*") if self.server_sock.sendto(req, host, port) > 0 sent_data += data_block.size end diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 85f16c96d2..0c401ffcf7 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -148,11 +148,11 @@ class Metasploit3 < Msf::Auxiliary 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 - unless framework.db.active - print_status "No database connected, so not actually saving the data:" - print_line data - end 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, From 2b3e3725ac9550d741fb7e2c8af7f3a32d912dc8 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 18:15:19 -0600 Subject: [PATCH 09/11] 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? From 677cb4b15240c663517f7c696ef4875b7f99e6e7 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Mon, 19 Dec 2011 21:56:03 -0600 Subject: [PATCH 10/11] Handle empty data sends sanely for TFTP. Don't just hang forever -- let the user know they just send empty data. TFTP servers don't like this of course. --- lib/rex/proto/tftp/client.rb | 60 +++++++++++-------- .../auxiliary/admin/tftp/tftp_upload_file.rb | 51 +++++----------- 2 files changed, 52 insertions(+), 59 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index 011cc43aa9..4ba70c3fb3 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -19,9 +19,9 @@ module TFTP # # 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 +# 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 :return_data attribute. It's a little +# 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 @@ -36,11 +36,11 @@ 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, :return_data + attr_accessor :complete, :recv_tempfile, :status # 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_msg(str) + 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 @@ -86,24 +86,26 @@ class Client yield "Listening for incoming ACKs" if block_given? res = self.server_sock.recvfrom(65535) if res and res[0] - code, type, data = parse_tftp_msg(res[0]) - if code == OpAck && self.action == :upload + 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 - 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 + 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, msg]) if block_given? - stop + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given? + self.status = {:error => [code, type, data]} end end stop @@ -112,17 +114,18 @@ class Client 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, msg = parse_tftp_msg(res[0]) - yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? + 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 - self.threads.each {|t| t.kill} self.server_sock.close rescue nil # might be closed already self.client_sock.close rescue nil # might be closed already + self.threads.each {|t| t.kill} end # @@ -140,7 +143,7 @@ class Client end def send_read_request(&block) - self.return_data = nil + self.status = nil self.complete = false if block_given? start_server_socket {|msg| yield msg} @@ -163,7 +166,7 @@ class Client end } until self.complete - return self.return_data + return self.status end end @@ -183,7 +186,7 @@ class Client 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]) + 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} @@ -200,7 +203,7 @@ class Client if block_given? yield("Transferred #{self.recv_tempfile.size} bytes in #{recvd_blocks} blocks, download complete!") end - self.return_data = {:success => [ + self.status = {:success => [ self.local_file, self.remote_file, self.recv_tempfile.size, @@ -221,7 +224,7 @@ class Client # # 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}" @@ -229,18 +232,21 @@ class Client 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 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 - data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} + return [] end data.scan(/.{1,512}/) end def send_write_request(&block) - self.return_data = nil + self.status = nil self.complete = false if block_given? start_server_socket {|msg| yield msg} @@ -263,12 +269,18 @@ class Client end } until self.complete - return self.return_data + 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 @@ -284,7 +296,7 @@ class Client end res = self.server_sock.recvfrom(65535) if res - code, type, msg = parse_tftp_msg(res[0]) + 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? @@ -304,7 +316,7 @@ class Client end end if sent_data == expected_size - self.return_data = {:success => [ + self.status = {:success => [ self.local_file, self.remote_file, sent_data, diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 4405e07d81..41bf1ef917 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -82,28 +82,22 @@ 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" + fdata = datastore['FILEDATA'].to_s fname = datastore['FILENAME'].to_s - if fname.empty? - fdata = "DATA:#{datastore['FILEDATA']}" - return fdata + if not fdata.empty? + fdata_decorated = "DATA:#{datastore['FILEDATA']}" + elsif ::File.readable? fname + fname 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 + 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 + else # "Download" fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last rescue nil end end @@ -120,21 +114,12 @@ 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 - # 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." - return - end @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025)) @lhost = datastore['LHOST'] || "0.0.0.0" @local_file = file @@ -153,17 +138,13 @@ class Metasploit3 < Msf::Auxiliary end def run - return unless check_valid_filename() run_upload() if action.name == 'Upload' run_download() if action.name == 'Download' - while true - if @tftp_client.complete - print_status [rtarget,"TFTP transfer operation complete."].join - save_downloaded_file() if action.name == 'Download' - break - else - select(nil,nil,nil,1) # 1 second delays are just fine. - end + 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 From 24d53efa7cd1881f3c8ef8a09f1ff29643006505 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Tue, 20 Dec 2011 10:03:04 -0600 Subject: [PATCH 11/11] Final touches on TFTP client See #5291. Adds an option to mess with the block size in case someone wants to write a fuzzer or exploit that leverages that. Adds a cleanup method to the module (pretty much required, it turns out). Looking nearly final, just need to rename the module and I think we're good to push to master. --- lib/rex/proto/tftp/client.rb | 23 ++++++++++++++----- .../auxiliary/admin/tftp/tftp_upload_file.rb | 14 +++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/rex/proto/tftp/client.rb b/lib/rex/proto/tftp/client.rb index 4ba70c3fb3..fb595489b0 100644 --- a/lib/rex/proto/tftp/client.rb +++ b/lib/rex/proto/tftp/client.rb @@ -1,4 +1,3 @@ -# $Id$ require 'rex/socket' require 'rex/proto/tftp' require 'tempfile' @@ -7,7 +6,6 @@ module Rex module Proto module TFTP - # # TFTP Client class # @@ -37,6 +35,7 @@ class Client 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. @@ -58,6 +57,7 @@ class Client 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 # @@ -123,9 +123,15 @@ class Client def stop self.complete = true - self.server_sock.close rescue nil # might be closed already - self.client_sock.close rescue nil # might be closed already - self.threads.each {|t| t.kill} + 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 # @@ -242,7 +248,12 @@ class Client else return [] end - data.scan(/.{1,512}/) + 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) diff --git a/modules/auxiliary/admin/tftp/tftp_upload_file.rb b/modules/auxiliary/admin/tftp/tftp_upload_file.rb index 41bf1ef917..351e56e980 100644 --- a/modules/auxiliary/admin/tftp/tftp_upload_file.rb +++ b/modules/auxiliary/admin/tftp/tftp_upload_file.rb @@ -148,6 +148,20 @@ class Metasploit3 < Msf::Auxiliary 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) }