metasploit-framework/lib/rex/proto/tftp/server.rb

498 lines
9.9 KiB
Ruby
Raw Normal View History

# $Id$
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)
# 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