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.
unstable
Tod Beardsley 2011-12-19 18:15:19 -06:00
parent 431ef826c9
commit 2b3e3725ac
2 changed files with 161 additions and 47 deletions

View File

@ -12,12 +12,31 @@ module TFTP
# TFTP Client class # TFTP Client class
# #
# Note that TFTP has blocks, and so does Ruby. Watch out with the variable names! # 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 class Client
attr_accessor :local_host, :local_port, :peer_host, :peer_port attr_accessor :local_host, :local_port, :peer_host, :peer_port
attr_accessor :threads, :context, :server_sock, :client_sock attr_accessor :threads, :context, :server_sock, :client_sock
attr_accessor :local_file, :remote_file, :mode, :action 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 # Returns an array of [code, type, msg]. Data packets
# specifically will /not/ unpack, since that would drop any trailing spaces or nulls. # 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_host = params["PeerHost"] || (raise ArgumentError, "Need a peer host.")
self.peer_port = params["PeerPort"] || 69 self.peer_port = params["PeerPort"] || 69
self.context = params["Context"] || {} 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.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last
self.mode = params["Mode"] || "octet" self.mode = params["Mode"] || "octet"
self.action = params["Action"] || (raise ArgumentError, "Need an action.") 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}" yield "Started TFTP client listener on #{local_host}:#{local_port}"
end end
self.threads << Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { 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 end
@ -65,9 +88,19 @@ class Client
if res and res[0] if res and res[0]
code, type, data = parse_tftp_msg(res[0]) code, type, data = parse_tftp_msg(res[0])
if code == OpAck && self.action == :upload 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 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 else
yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given?
stop stop
@ -107,6 +140,8 @@ class Client
end end
def send_read_request(&block) def send_read_request(&block)
self.return_data = nil
self.complete = false
if block_given? if block_given?
start_server_socket {|msg| yield msg} start_server_socket {|msg| yield msg}
else else
@ -121,8 +156,15 @@ class Client
) )
self.client_sock.sendto(rrq_packet, peer_host, peer_port) self.client_sock.sendto(rrq_packet, peer_host, peer_port)
self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { 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 end
def recv_data(host, port, first_block) 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 "Source file: #{self.remote_file}, destination file: #{self.local_file}"
yield "Received and acknowledged #{first_block.size} in block #{recvd_blocks}" yield "Received and acknowledged #{first_block.size} in block #{recvd_blocks}"
end 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 current_block = first_block
while current_block.size == 512 while current_block.size == 512
res = self.server_sock.recvfrom(65535) res = self.server_sock.recvfrom(65535)
if res and res[0] if res and res[0]
code, block_num, current_block = parse_tftp_msg(res[0]) code, block_num, current_block = parse_tftp_msg(res[0])
if code == 3 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 recvd_blocks += 1
else else
yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given?
@ -147,7 +197,15 @@ class Client
end end
end 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 self.recv_tempfile.close
stop stop
end end
@ -157,7 +215,7 @@ class Client
self.recv_tempfile.flush self.recv_tempfile.flush
req = ack_packet(blocknum) req = ack_packet(blocknum)
self.server_sock.sendto(req, host, port) 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 end
# #
@ -170,12 +228,20 @@ class Client
req.pack(packstr) req.pack(packstr)
end end
def blockify_file # Note that the local filename for uploading need not be a real filename --
data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} # 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}/) data.scan(/.{1,512}/)
end end
def send_write_request(&block) def send_write_request(&block)
self.return_data = nil
self.complete = false
if block_given? if block_given?
start_server_socket {|msg| yield msg} start_server_socket {|msg| yield msg}
else else
@ -190,18 +256,25 @@ class Client
) )
self.client_sock.sendto(wrq_packet, peer_host, peer_port) self.client_sock.sendto(wrq_packet, peer_host, peer_port)
self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { 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 end
def send_data(host,port) def send_data(host,port)
data_blocks = blockify_file() data_blocks = blockify_file_or_data()
sent_data = 0 sent_data = 0
sent_blocks = 0 sent_blocks = 0
expected_blocks = data_blocks.size expected_blocks = data_blocks.size
expected_size = data_blocks.join.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 "Source file: #{self.local_file =~ /^DATA:/ ? "(Data)" : self.remote_file}, destination file: #{self.remote_file}"
yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)"
end end
data_blocks.each_with_index do |data_block,idx| data_blocks.each_with_index do |data_block,idx|
@ -225,11 +298,19 @@ class Client
end end
if block_given? if block_given?
if(sent_data == expected_size) if(sent_data == expected_size)
yield "Upload complete!" yield("Transferred #{sent_data} bytes in #{sent_blocks} blocks, upload complete!")
else else
yield "Upload complete, but with errors." yield "Upload complete, but with errors."
end end
end end
if sent_data == expected_size
self.return_data = {:success => [
self.local_file,
self.remote_file,
sent_data,
sent_blocks
] }
end
end end
end end

View File

@ -17,9 +17,17 @@ class Metasploit3 < Msf::Auxiliary
super( super(
'Name' => 'TFTP File Transfer Utility', 'Name' => 'TFTP File Transfer Utility',
'Description' => %q{ 'Description' => %q{
This module will send file to a remote TFTP server. Note that the target This module will transfer a file to or from a remote TFTP server.
must be able to connect back to the Metasploit system, and NAT traversal Note that the target must be able to connect back to the Metasploit system,
for TFTP is often unsupported. 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' ], 'Author' => [ 'todb' ],
'References' => 'References' =>
@ -28,32 +36,25 @@ class Metasploit3 < Msf::Auxiliary
['URL', 'http://www.networksorcery.com/enp/protocol/tftp.htm'] ['URL', 'http://www.networksorcery.com/enp/protocol/tftp.htm']
], ],
'Actions' => [ 'Actions' => [
[ 'Download', {'Description' => "Download REMOTE_FILENAME as FILENAME."}], [ 'Download', {'Description' => "Download REMOTE_FILENAME as FILENAME from the server."}],
[ 'Upload', {'Description' => "Upload FILENAME as REMOTE_FILENAME to the server."}] [ 'Upload', {'Description' => "Upload FILENAME as REMOTE_FILENAME to the server."}]
], ],
'DefaultAction' => 'Upload', 'DefaultAction' => 'Upload',
'License' => MSF_LICENSE 'License' => MSF_LICENSE
) )
register_options([ 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"]), OptString.new( 'REMOTE_FILENAME', [false, "The remote filename"]),
OptAddress.new('RHOST', [true, "The remote TFTP server"]), OptAddress.new('RHOST', [true, "The remote TFTP server"]),
OptPort.new( 'LPORT', [false, "The local port the TFTP client should listen on (default is random)" ]), 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"]), OptAddress.new('LHOST', [false, "The local address the TFTP client should bind to"]),
OptBool.new( 'VERBOSE', [false, "Display verbose details about the transfer", false]), 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) Opt::RPORT(69)
], self.class) ], self.class)
end end
def file
if action.name == "Upload"
datastore['FILENAME']
else # "Download
fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last
end
end
def mode def mode
datastore['MODE'] || "octect" datastore['MODE'] || "octect"
end end
@ -70,6 +71,8 @@ class Metasploit3 < Msf::Auxiliary
datastore['RHOST'] datastore['RHOST']
end end
# Used only to store loot, doesn't actually have any semantic meaning
# for the TFTP protocol.
def datatype def datatype
case datastore['MODE'] case datastore['MODE']
when "netascii" when "netascii"
@ -79,6 +82,34 @@ class Metasploit3 < Msf::Auxiliary
end end
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) def rtarget(ip=nil)
if (ip or rhost) and rport if (ip or rhost) and rport
[(ip || rhost),rport].map {|x| x.to_s}.join(":") << " " [(ip || rhost),rport].map {|x| x.to_s}.join(":") << " "
@ -89,12 +120,16 @@ class Metasploit3 < Msf::Auxiliary
end end
end end
# At least one, but not both, are required.
def check_valid_filename def check_valid_filename
not (datastore['FILENAME'].to_s.empty? and datastore['REMOTE_FILENAME'].to_s.empty?) not (datastore['FILENAME'].to_s.empty? and datastore['REMOTE_FILENAME'].to_s.empty?)
end end
# # This all happens before run(), and should give an idea on how to use
# TFTP is a funny service and needs to kind of be a server on our side, too. # 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 def setup
unless check_valid_filename() unless check_valid_filename()
print_error "Need at least one valid filename." print_error "Need at least one valid filename."
@ -106,14 +141,14 @@ class Metasploit3 < Msf::Auxiliary
@remote_file = remote_file @remote_file = remote_file
@tftp_client = Rex::Proto::TFTP::Client.new( @tftp_client = Rex::Proto::TFTP::Client.new(
"LocalHost" => @lhost, "LocalHost" => @lhost,
"LocalPort" => @lport, "LocalPort" => @lport,
"PeerHost" => rhost, "PeerHost" => rhost,
"PeerPort" => rport, "PeerPort" => rport,
"LocalFile" => @local_file, "LocalFile" => @local_file,
"RemoteFile" => @remote_file, "RemoteFile" => @remote_file,
"Mode" => mode, "Mode" => mode,
"Action" => action.name.to_s.downcase.intern "Action" => action.name.to_s.downcase.intern
) )
end end
@ -124,28 +159,26 @@ class Metasploit3 < Msf::Auxiliary
while true while true
if @tftp_client.complete if @tftp_client.complete
print_status [rtarget,"TFTP transfer operation complete."].join print_status [rtarget,"TFTP transfer operation complete."].join
if action.name == 'Download' save_downloaded_file() if action.name == 'Download'
save_downloaded_file()
end
break break
else else
select(nil,nil,nil,1) select(nil,nil,nil,1) # 1 second delays are just fine.
end end
end end
end end
def run_upload def run_upload
print_status "Sending '#{file}' to #{rhost}:#{rport} as '#{remote_file}'" 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 end
def run_download def run_download
print_status "Receiving '#{remote_file}' from #{rhost}:#{rport} as '#{file}'" 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 end
def save_downloaded_file def save_downloaded_file
print_status "Saving #{remote_file} as #{file}" print_status "Saving #{remote_file} as '#{file}'"
fh = @tftp_client.recv_tempfile fh = @tftp_client.recv_tempfile
data = File.open(fh,"rb") {|f| f.read f.stat.size} rescue nil data = File.open(fh,"rb") {|f| f.read f.stat.size} rescue nil
if data and not data.empty? if data and not data.empty?