Adding TFTP client and lib to the next release

Squashed commit of the following:

commit 11a27a1e61
Author: Tod Beardsley <todb@metasploit.com>
Date:   Tue Dec 20 10:06:44 2011 -0600

    Renaming TFTP transfer util.

    See #5291. Just renaming the file.

commit 24d53efa7c
Author: Tod Beardsley <todb@metasploit.com>
Date:   Tue Dec 20 10:03:04 2011 -0600

    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.

commit 677cb4b152
Author: Tod Beardsley <todb@metasploit.com>
Date:   Mon Dec 19 21:56:03 2011 -0600

    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.

commit 2b3e3725ac
Author: Tod Beardsley <todb@metasploit.com>
Date:   Mon Dec 19 18:15:19 2011 -0600

    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.

commit 431ef826c9
Author: Tod Beardsley <todb@metasploit.com>
Date:   Mon Dec 19 16:33:25 2011 -0600

    TFTP client now uses constants, preserves trailing spaces/nulls in data

    See #5291, just rediscovered the bug on this.

commit 5eaf2e7535
Author: Tod Beardsley <todb@metasploit.com>
Date:   Mon Dec 19 15:50:50 2011 -0600

    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.

commit aecde6fea4
Author: Tod Beardsley <todb@metasploit.com>
Date:   Mon Dec 19 12:14:40 2011 -0600

    Updating TFTP client. Now with grown-up thread handling.

    No longer blocks on successful connections.

commit 902d7f5ea7
Author: Tod Beardsley <todb@metasploit.com>
Date:   Sun Dec 18 21:05:27 2011 -0600

    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.

commit 23aadd04f7
Author: Tod Beardsley <todb@metasploit.com>
Date:   Sun Dec 18 13:28:52 2011 -0600

    Fixing merge conflict cruft

    Dangit teach me to merge quickly. TFTP module now loads again.

commit 1201d7fbf2
Merge: 0b89140 a6867ef
Author: Tod Beardsley <todb@metasploit.com>
Date:   Fri Dec 16 22:41:22 2011 -0600

    Merge branch 'tftp_client' of github_r7:rapid7/metasploit-framework into tftp_client

    Conflicts:
    	modules/auxiliary/admin/tftp/tftp_upload_file.rb

commit 0b8914021c
Author: Tod Beardsley <todb@metasploit.com>
Date:   Fri Dec 16 21:06:10 2011 -0600

    Switch to vprint_status, also add skeletal cleanup def.

commit 50fa10679b
Author: Tod Beardsley <todb@metasploit.com>
Date:   Fri Dec 16 18:39:09 2011 -0600

    First draft of a TFTP client.

    Could use some actual error checking and also needs to expose
    more options.

commit a6867ef128
Author: Tod Beardsley <todb@metasploit.com>
Date:   Fri Dec 16 18:39:09 2011 -0600

    First draft of a TFTP client.

    Could use some actual error checking and also needs to expose
    more options.
This commit is contained in:
Tod Beardsley 2011-12-20 11:25:08 -06:00
parent b58097a2a7
commit f997a7fc31
3 changed files with 553 additions and 0 deletions

View File

@ -10,3 +10,4 @@
require 'rex/proto/tftp/constants'
require 'rex/proto/tftp/server'
require 'rex/proto/tftp/client'

View File

@ -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

View File

@ -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