class Rex::Proto::TFTP::Server
Attributes
context[RW]
files[RW]
incoming_file_hook[RW]
listen_host[RW]
listen_port[RW]
sock[RW]
thread[RW]
transfers[RW]
uploaded[RW]
Public Class Methods
new(port = 69, listen_host = '0.0.0.0', context = {})
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 28 def initialize(port = 69, listen_host = '0.0.0.0', context = {}) self.listen_host = listen_host self.listen_port = port self.context = context self.sock = nil @shutting_down = false @output_dir = nil @tftproot = nil self.files = [] self.uploaded = [] self.transfers = [] end
Public Instance Methods
find_file(fname)
click to toggle source
Find
the hash entry for a file that may be offered
# File lib/rex/proto/tftp/server.rb, line 133 def find_file(fname) # Files served via register_file() take precedence. self.files.each do |f| if (fname == f[:name]) return f end end # Now, if we have a tftproot, see if it can serve from it if @tftproot return find_file_in_root(fname) end nil end
find_file_in_root(fname)
click to toggle source
Find
the file in the specified tftp root and add a temporary entry to the files hash.
# File lib/rex/proto/tftp/server.rb, line 154 def find_file_in_root(fname) fn = ::File.expand_path(::File.join(@tftproot, fname)) # Don't allow directory traversal return nil if fn.index(@tftproot) != 0 return nil if not ::File.file?(fn) or not ::File.readable?(fn) # Read the file contents, and register it as being served once data = data = ::File.open(fn, "rb") { |fd| fd.read(fd.stat.size) } register_file(fname, data) # Return the last file in the array return self.files[-1] end
register_file(fn, content, once = false)
click to toggle source
Register a filename and content for a client to request
# File lib/rex/proto/tftp/server.rb, line 82 def register_file(fn, content, once = false) self.files << { :name => fn, :data => content, :once => once } end
send_error(from, num)
click to toggle source
Send an error packet w/the specified code and string
# File lib/rex/proto/tftp/server.rb, line 110 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
send_packet(from, pkt)
click to toggle source
Send a single packet to the specified host
# File lib/rex/proto/tftp/server.rb, line 125 def send_packet(from, pkt) self.sock.sendto(pkt, from[0], from[1]) end
set_output_dir(outdir)
click to toggle source
Register a directory to write uploaded files to
# File lib/rex/proto/tftp/server.rb, line 102 def set_output_dir(outdir) @output_dir = outdir if ::File.directory?(outdir) end
set_tftproot(rootdir)
click to toggle source
Register an entire directory to serve files from
# File lib/rex/proto/tftp/server.rb, line 94 def set_tftproot(rootdir) @tftproot = rootdir if ::File.directory?(rootdir) end
start()
click to toggle source
Start the TFTP
server
# File lib/rex/proto/tftp/server.rb, line 46 def start self.sock = Rex::Socket::Udp.create( 'LocalHost' => listen_host, 'LocalPort' => listen_port, 'Context' => context ) self.thread = Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { monitor_socket } end
stop()
click to toggle source
Stop the TFTP
server
# File lib/rex/proto/tftp/server.rb, line 62 def stop @shutting_down = true # Wait a maximum of 30 seconds for all transfers to finish. start = ::Time.now while (self.transfers.length > 0) ::IO.select(nil, nil, nil, 0.5) dur = ::Time.now - start break if (dur > 30) end self.files.clear self.thread.kill self.sock.close rescue nil # might be closed already end
Protected Instance Methods
check_retransmission(tr)
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 205 def check_retransmission(tr) elapsed = ::Time.now - tr[:last_sent] if (elapsed >= tr[:timeout]) # max retries reached? if (tr[:retries] < 3) #if (tr[:type] == OpRead) # puts "[-] ack timed out, resending block" #else # puts "[-] block timed out, resending ack" #end tr[:last_sent] = nil tr[:retries] += 1 else #puts "[-] maximum tries reached, terminating transfer" self.transfers.delete(tr) end end end
dispatch_request(from, buf)
click to toggle source
Dispatch a packet that we received
# File lib/rex/proto/tftp/server.rb, line 312 def dispatch_request(from, buf) op = buf.unpack('n')[0] buf.slice!(0,2) #XXX: todo - create call backs for status #start = "[*] TFTP - %s:%u - %s" % [from[0], from[1], OPCODES[op]] case op when OpRead # Process RRQ packets fn = TFTP::get_string(buf) mode = TFTP::get_string(buf).downcase #puts "%s %s %s" % [start, fn, mode] if (not @shutting_down) and (file = self.find_file(fn)) if (file[:once] and file[:started]) send_error(from, ErrFileNotFound) else transfer = { :type => OpRead, :from => from, :file => file, :block => 1, :blksize => 512, :offset => 0, :timeout => 3, :last_sent => nil, :retries => 0 } process_options(from, buf, transfer) self.transfers << transfer end else #puts "[-] file not found!" send_error(from, ErrFileNotFound) end when OpWrite # Process WRQ packets fn = TFTP::get_string(buf) mode = TFTP::get_string(buf).downcase #puts "%s %s %s" % [start, fn, mode] if not @shutting_down transfer = { :type => OpWrite, :from => from, :file => { :name => fn, :data => '' }, :block => 0, # WRQ starts at 0 :blksize => 512, :timeout => 3, :last_sent => nil, :retries => 0 } process_options(from, buf, transfer) self.transfers << transfer else send_error(from, ErrIllegalOperation) end when OpAck # Process ACK packets block = buf.unpack('n')[0] #puts "%s %d" % [start, block] tr = find_transfer(OpRead, from, block) if not tr # NOTE: some clients, such as pxelinux, send an ack for block 0. # To deal with this, we simply ignore it as we start with block 1. return if block == 0 # If we didn't find it, send an error. send_error(from, ErrUnknownTransferId) else # acked! send the next block tr[:offset] += tr[:blksize] next_block(tr) # If the transfer is finished, delete it if (tr[:offset] > tr[:file][:data].length) #puts "[*] Transfer complete" self.transfers.delete(tr) # if the file is a one-serve, delete it from the files array if tr[:file][:once] #puts "[*] Removed one-serve file: #{tr[:file][:name]}" self.files.delete(tr[:file]) end end end when OpData # Process Data packets block = buf.unpack('n')[0] data = buf.slice(2, buf.length) #puts "%s %d %d bytes" % [start, block, data.length] tr = find_transfer(OpWrite, from, (block-1)) if not tr # If we didn't find it, send an error. send_error(from, ErrUnknownTransferId) else tr[:file][:data] << data tr[:last_size] = data.length next_block(tr) # Similar to RRQ transfers, we cannot detect that the # transfer finished here. We must do so after transmitting # the final ACK. end else # Other packets are unsupported #puts start send_error(from, ErrAccessViolation) end end
find_transfer(type, from, block)
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 179 def find_transfer(type, from, block) self.transfers.each do |tr| if (tr[:type] == type and tr[:from] == from and tr[:block] == block) return tr end end nil end
monitor_socket()
click to toggle source
See if there is anything to do.. If so, dispatch it.
# File lib/rex/proto/tftp/server.rb, line 228 def monitor_socket while true rds = [@sock] wds = [] self.transfers.each do |tr| if (not tr[:last_sent]) wds << @sock break end end eds = [@sock] r,w,e = ::IO.select(rds,wds,eds,1) if (r != nil and r[0] == self.sock) buf,host,port = self.sock.recvfrom(65535) # Lame compatabilitiy :-/ from = [host, port] dispatch_request(from, buf) end # # Check to see if transfers need maintenance # self.transfers.each do |tr| # We handle RRQ and WRQ separately # if (tr[:type] == OpRead) # Are we awaiting an ack? if (tr[:last_sent]) check_retransmission(tr) elsif (w != nil and w[0] == self.sock) # No ack waiting, send next block.. chunk = tr[:file][:data].slice(tr[:offset], tr[:blksize]) if (chunk and chunk.length >= 0) pkt = [OpData, tr[:block]].pack('nn') pkt << chunk send_packet(tr[:from], pkt) tr[:last_sent] = ::Time.now # If the file is a one-serve, mark it as started tr[:file][:started] = true if (tr[:file][:once]) else # No more chunks.. transfer is most likely done. # However, we can only delete it once the last chunk has been # acked. end end else # Are we awaiting data? if (tr[:last_sent]) check_retransmission(tr) elsif (w != nil and w[0] == self.sock) # Not waiting for data, send an ack.. #puts "[*] sending ack for block %d" % [tr[:block]] pkt = [OpAck, tr[:block]].pack('nn') send_packet(tr[:from], pkt) tr[:last_sent] = ::Time.now # If we had a 0-511 byte chunk, we're done. if (tr[:last_size] and tr[:last_size] < tr[:blksize]) #puts "[*] Transfer complete, saving output" save_output(tr) self.transfers.delete(tr) end end end end end end
next_block(tr)
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 302 def next_block(tr) tr[:block] += 1 tr[:last_sent] = nil tr[:retries] = 0 end
process_options(from, buf, tr)
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 440 def process_options(from, buf, tr) found = 0 to_ack = [] while buf.length >= 4 opt = TFTP::get_string(buf) break if not opt val = TFTP::get_string(buf) break if not val found += 1 # Is it one we support? opt.downcase! case opt when "blksize" val = val.to_i if val > 0 tr[:blksize] = val to_ack << [ opt, val.to_s ] end when "timeout" val = val.to_i if val >= 1 and val <= 255 tr[:timeout] = val to_ack << [ opt, val.to_s ] end when "tsize" if tr[:type] == OpRead len = tr[:file][:data].length else val = val.to_i len = val end to_ack << [ opt, len.to_s ] end end return if to_ack.length < 1 # if we have anything to ack, do it data = [OpOptAck].pack('n') to_ack.each { |el| data << el[0] << "\x00" << el[1] << "\x00" } send_packet(from, data) end
save_output(tr)
click to toggle source
# File lib/rex/proto/tftp/server.rb, line 188 def save_output(tr) self.uploaded << tr[:file] return incoming_file_hook.call(tr) if incoming_file_hook if @output_dir fn = tr[:file][:name].split(File::SEPARATOR)[-1] if fn fn = ::File.join(@output_dir, Rex::FileUtils.clean_path(fn)) ::File.open(fn, "wb") { |fd| fd.write(tr[:file][:data]) } end end end