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
parent
431ef826c9
commit
2b3e3725ac
|
@ -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) {
|
||||
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
|
||||
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
|
||||
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) {
|
||||
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
|
||||
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
|
||||
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
|
||||
# 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) {
|
||||
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
|
||||
|
|
|
@ -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,14 +36,15 @@ class Metasploit3 < Msf::Auxiliary
|
|||
['URL', 'http://www.networksorcery.com/enp/protocol/tftp.htm']
|
||||
],
|
||||
'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."}]
|
||||
],
|
||||
'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)" ]),
|
||||
|
@ -46,14 +55,6 @@ class Metasploit3 < Msf::Auxiliary
|
|||
], 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."
|
||||
|
@ -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?
|
||||
|
|
Loading…
Reference in New Issue