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.
unstable
Tod Beardsley 2011-12-19 15:50:50 -06:00
parent aecde6fea4
commit 5eaf2e7535
2 changed files with 250 additions and 97 deletions

View File

@ -1,6 +1,7 @@
# $Id$ # $Id$
require 'rex/socket' require 'rex/socket'
require 'rex/proto/tftp' require 'rex/proto/tftp'
require 'tempfile'
module Rex module Rex
module Proto module Proto
@ -15,13 +16,16 @@ 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 attr_accessor :local_file, :remote_file, :mode, :action
attr_accessor :complete attr_accessor :complete, :recv_tempfile
# Returns an array of [code, type, msg] # Returns an array of [code, type, msg]. Data packets
def parse_tftp_msg(str) # 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 return nil unless str.length >= 4
ret = str.unpack("nnA*") ret = str.unpack("nnA*")
ret[2] = str[4,str.size] unless strip
return ret return ret
end end
@ -32,9 +36,142 @@ 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 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.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.")
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 end
def blockify_file def blockify_file
@ -42,13 +179,32 @@ class Client
data.scan(/.{1,512}/) data.scan(/.{1,512}/)
end 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) def send_data(host,port)
data_blocks = blockify_file() data_blocks = blockify_file()
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}, destination file: #{self.remote_file}"
yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)"
end end
@ -67,7 +223,7 @@ class Client
if block_given? if block_given?
yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg] yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg]
end end
break break
end end
end end
end end
@ -80,75 +236,6 @@ class Client
end end
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
end end

View File

@ -11,6 +11,7 @@ require 'msf/core'
class Metasploit3 < Msf::Auxiliary class Metasploit3 < Msf::Auxiliary
include Rex::Proto::TFTP include Rex::Proto::TFTP
include Msf::Auxiliary::Report
def initialize def initialize
super( super(
@ -21,22 +22,36 @@ class Metasploit3 < Msf::Auxiliary
for TFTP is often unsupported. for TFTP is often unsupported.
}, },
'Author' => [ 'todb' ], '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 'License' => MSF_LICENSE
) )
register_options([ register_options([
OptPath.new('FILENAME', [true, "The local file to upload" ]), OptPath.new( 'FILENAME', [false, "The local filename" ]),
OptString.new('REMOTE_FILENAME', [false, "The filename to provide to the TFTP server" ]), 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" ]), 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, "Provide more 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 netsacii 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 def file
datastore['FILENAME'] if action.name == "Upload"
datastore['FILENAME']
else # "Download
fname = ::File.split(datastore['FILENAME'] || datastore['REMOTE_FILENAME']).last
end
end end
def mode def mode
@ -44,17 +59,26 @@ class Metasploit3 < Msf::Auxiliary
end end
def remote_file def remote_file
datastore['REMOTE_FILENAME'] || ::File.split(file).last datastore['REMOTE_FILENAME'] || ::File.split(datastore['FILENAME']).last
end end
def rport def rport
datastore['RPORT'] datastore['RPORT'] || 69
end end
def rhost def rhost
datastore['RHOST'] datastore['RHOST']
end end
def datatype
case datastore['MODE']
when "netascii"
"text/plain"
else
"application/octet-stream"
end
end
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(":") << " "
@ -65,34 +89,44 @@ class Metasploit3 < Msf::Auxiliary
end end
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. # TFTP is a funny service and needs to kind of be a server on our side, too.
def setup 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)) @lport = datastore['LPORT'] || (1025 + rand(0xffff-1025))
@lhost = datastore['LHOST'] || "0.0.0.0" @lhost = datastore['LHOST'] || "0.0.0.0"
@path = datastore['FILENAME'] @local_file = file
@filename = ::File.split(@path).last @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" => file, "LocalFile" => @local_file,
"RemoteFile" => remote_file, "RemoteFile" => @remote_file,
"Mode" => mode "Mode" => mode,
"Action" => action.name.to_s.downcase.intern
) )
end end
def run def run
print_status "Sending '#{file}' to #{@lhost}:#{@lport} as '#{remote_file}'" return unless check_valid_filename()
@tftp_client.send_write_request do |msg| run_upload() if action.name == 'Upload'
print_tftp_status(msg) run_download() if action.name == 'Download'
end
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()
end
break break
else else
select(nil,nil,nil,1) select(nil,nil,nil,1)
@ -100,6 +134,38 @@ class Metasploit3 < Msf::Auxiliary
end 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) }
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) def print_tftp_status(msg)
case msg case msg
when /Aborting/, /errors.$/ when /Aborting/, /errors.$/
@ -107,7 +173,7 @@ class Metasploit3 < Msf::Auxiliary
when /^WRQ accepted/, /^Sending/, /complete!$/ when /^WRQ accepted/, /^Sending/, /complete!$/
print_good [rtarget,msg].join print_good [rtarget,msg].join
else else
vprint_status [rtarget,msg].join vprint_status [rtarget,msg].join
end end
end end