class Rex::Proto::TFTP::Client

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 transfer actions are 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 :status 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.

Attributes

action[RW]
block_size[RW]
client_sock[RW]
complete[RW]
context[RW]
local_file[RW]
local_host[RW]
local_port[RW]
mode[RW]
peer_host[RW]
peer_port[RW]
recv_tempfile[RW]
remote_file[RW]
server_sock[RW]
status[RW]
threads[RW]

Public Class Methods

new(params) click to toggle source
# File lib/rex/proto/tftp/client.rb, line 49
def initialize(params)
  self.threads = []
  self.local_host = params["LocalHost"] || "0.0.0.0"
  self.local_port = params["LocalPort"] || (1025 + rand(0xffff-1025))
  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"]
  self.remote_file = params["RemoteFile"] || (::File.split(self.local_file).last if self.local_file)
  self.mode = params["Mode"] || "octet"
  self.action = params["Action"] || (raise ArgumentError, "Need an action.")
  self.block_size = params["BlockSize"] || 512
end

Public Instance Methods

ack_packet(blocknum=0) click to toggle source
# File lib/rex/proto/tftp/client.rb, line 147
def ack_packet(blocknum=0)
  req = [OpAck, blocknum].pack("nn")
end
blockify_file_or_data() click to toggle source

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. If it's missing completely, then just quit.

# File lib/rex/proto/tftp/client.rb, line 243
def blockify_file_or_data
  if self.local_file =~ /^DATA:(.*)/m
    data = $1
  elsif ::File.file?(self.local_file) and ::File.readable?(self.local_file)
    data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} rescue []
  else
    return []
  end
  data_blocks = data.scan(/.{1,#{block_size}}/m)
  # Drop any trailing empty blocks
  if data_blocks.size > 1 and data_blocks.last.empty?
    data_blocks.pop
  end
  return data_blocks
end
monitor_client_sock() { |"Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 114
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, data = parse_tftp_response(res[0])
    yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given?
    self.status = {:error => [code, type, data]}
    stop
  end
end
monitor_server_sock() { |"Listening for incoming ACKs"| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 85
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_response(res[0])
    if code == OpAck and self.action == :upload
      if block_given?
        yield "WRQ accepted, sending the file." if type == 0
        send_data(res[1], res[2]) {|msg| yield msg}
      else
        send_data(res[1], res[2])
      end
    elsif code == OpData and 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
    elsif code == OpError
      yield("Aborting, got error type:%d, message:'%s'" % [type, data]) if block_given?
      self.status = {:error => [code, type, data]}
    else
      yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given?
      self.status = {:error => [code, type, data]}
    end
  end
  stop
end
parse_tftp_response(str) click to toggle source

Returns an array of [code, type, msg]. Data packets specifically will /not/ unpack, since that would drop any trailing spaces or nulls.

# File lib/rex/proto/tftp/client.rb, line 42
def parse_tftp_response(str)
  return nil unless str.length >= 4
  ret = str.unpack("nnA*")
  ret[2] = str[4,str.size] if ret[0] == OpData
  return ret
end
recv_data(host, port, first_block) { |"Source file: #{remote_file}, destination file: #{local_file}"| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 179
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
  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_response(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?
        stop
      end
    end
  end
  if block_given?
    yield("Transferred #{self.recv_tempfile.size} bytes in #{recvd_blocks} blocks, download complete!")
  end
  self.status = {:success => [
    self.local_file,
    self.remote_file,
    self.recv_tempfile.size,
    recvd_blocks.size]
  }
  self.recv_tempfile.close
  stop
end
rrq_packet() click to toggle source

Methods for download

# File lib/rex/proto/tftp/client.rb, line 141
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
send_data(host,port) { |"Closing down since there is no data to send."| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 287
def send_data(host,port)
  self.status = {:write_allowed => true}
  data_blocks = blockify_file_or_data()
  if data_blocks.empty?
    yield "Closing down since there is no data to send." if block_given?
    self.status = {:success => [self.local_file, self.local_file, 0, 0]}
    return nil
  end
  sent_data = 0
  sent_blocks = 0
  send_retries = 0
  expected_blocks = data_blocks.size
  expected_size = data_blocks.join.size
  if block_given?
    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|
    loop do
      req = [OpData, (idx + 1), data_block].pack("nnA*")
      if self.server_sock.sendto(req, host, port) <= 0
        send_retries += 1
        if send_retries > 100
          break
        else
          next
        end
      end
      send_retries = 0
      res = self.server_sock.recvfrom(65535)
      if res
        code, type, msg = parse_tftp_response(res[0])
        if code == 4
          if type == idx + 1
            sent_blocks += 1
            sent_data += data_block.size
            yield "Sent #{data_block.size} bytes in block #{idx+1}" if block_given?
            break
          else
            next
          end
        else
          if block_given?
            yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg]
          end
          break
        end
      end
    end
  end

  if send_retries > 100
    yield "Too many send retries, aborted"
  end

  if block_given?
    if(sent_data == expected_size)
      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.status = {:success => [
      self.local_file,
      self.remote_file,
      sent_data,
      sent_blocks
    ] }
  end
end
send_read_request() { |msg| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 151
def send_read_request(&block)
  self.status = nil
  self.complete = false
  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) {
    if block_given?
      monitor_client_sock {|msg| yield msg}
    else
      monitor_client_sock
    end
  }
  until self.complete
    return self.status
  end
end
send_write_request() { |msg| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 259
def send_write_request(&block)
  self.status = nil
  self.complete = false
  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) {
    if block_given?
      monitor_client_sock {|msg| yield msg}
    else
      monitor_client_sock
    end
  }
  until self.complete
    return self.status
  end
end
start_server_socket() { |"Started TFTP client listener on #{local_host}:#{local_port}"| ... } click to toggle source

Methods for both upload and download

# File lib/rex/proto/tftp/client.rb, line 67
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) {
    if block_given?
      monitor_server_sock {|msg| yield msg}
    else
      monitor_server_sock
    end
  }
end
stop() click to toggle source
# File lib/rex/proto/tftp/client.rb, line 124
def stop
  self.complete = true
  begin
    self.server_sock.close
    self.client_sock.close
    self.server_sock = nil
    self.client_sock = nil
    self.threads.each {|t| t.kill}
  rescue
    nil
  end
end
write_and_ack_data(data,blocknum,host,port) { |"Received and acknowledged #{size} in block #{blocknum}"| ... } click to toggle source
# File lib/rex/proto/tftp/client.rb, line 222
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}" if block_given?
end
wrq_packet() click to toggle source

Methods for upload

# File lib/rex/proto/tftp/client.rb, line 234
def wrq_packet
  req = [OpWrite, self.remote_file, self.mode]
  packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}"
  req.pack(packstr)
end