class Rex::Proto::TFTP::Server

TFTP Server class

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