From a6867ef1287af1786b2e0cd6e7b13fdb8099ce2a Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 16 Dec 2011 18:39:09 -0600 Subject: [PATCH] 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 +