# $Id: server.rb 11636 2011-01-25 02:24:37Z hdm $ require 'rex/socket' require 'rex/proto/tftp' module Rex module Proto module TFTP # # Little util function # def self.get_string(data) idx = data.index("\x00") return nil if not idx ret = data.slice!(0, idx) # Slice off the nul byte. data.slice!(0,1) ret end ## # # TFTP Server class # ## class Server 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 # # Start the TFTP server # 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 the TFTP server # 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 # # Register a filename and content for a client to request # def register_file(fn, content, once = false) self.files << { :name => fn, :data => content, :once => once } end # # Register an entire directory to serve files from # def set_tftproot(rootdir) @tftproot = rootdir if ::File.directory?(rootdir) end # # Register a directory to write uploaded files to # def set_output_dir(outdir) @output_dir = outdir if ::File.directory?(outdir) end # # Send an error packet w/the specified code and string # 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 a single packet to the specified host # def send_packet(from, pkt) self.sock.sendto(pkt, from[0], from[1]) end # # Find the hash entry for a file that may be offered # 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 the file in the specified tftp root and add a temporary # entry to the files hash. # 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, true) # Return the last file in the array return self.files[-1] end attr_accessor :listen_host, :listen_port, :context attr_accessor :sock, :files, :transfers, :uploaded attr_accessor :thread attr_accessor :incoming_file_hook protected 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 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 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 # # See if there is anything to do.. If so, dispatch it. # 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 def next_block(tr) tr[:block] += 1 tr[:last_sent] = nil tr[:retries] = 0 end # # Dispatch a packet that we received # 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 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 end end end end