summaryrefslogtreecommitdiffstats
path: root/nselib/tftp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/tftp.lua')
-rw-r--r--nselib/tftp.lua347
1 files changed, 347 insertions, 0 deletions
diff --git a/nselib/tftp.lua b/nselib/tftp.lua
new file mode 100644
index 0000000..c34d2af
--- /dev/null
+++ b/nselib/tftp.lua
@@ -0,0 +1,347 @@
+--- Library implementing a minimal TFTP server
+--
+-- Currently only write-operations are supported so that script can trigger
+-- TFTP transfers and receive the files and return them as result.
+--
+-- The library contains the following classes
+-- * <code>Packet</code>
+-- ** The <code>Packet</code> classes contain one class for each TFTP operation.
+-- * <code>File</code>
+-- ** The <code>File</code> class holds a received file including the name and contents
+-- * <code>ConnHandler</code>
+-- ** The <code>ConnHandler</code> class handles and processes incoming connections.
+--
+-- The following code snippet starts the TFTP server and waits for the file incoming.txt
+-- to be uploaded for 10 seconds:
+-- <code>
+-- tftp.start()
+-- local status, f = tftp.waitFile("incoming.txt", 10)
+-- if ( status ) then return f:getContent() end
+-- </code>
+--
+-- @author Patrik Karlsson <patrik@cqure.net>
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+--
+
+-- version 0.2
+--
+-- 2011-01-22 - re-wrote library to use coroutines instead of new_thread code.
+
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+_ENV = stdnse.module("tftp", stdnse.seeall)
+
+threads, infiles, running = {}, {}, {}
+state = "STOPPED"
+srvthread = {}
+
+-- All opcodes supported by TFTP
+OpCode = {
+ RRQ = 1,
+ WRQ = 2,
+ DATA = 3,
+ ACK = 4,
+ ERROR = 5,
+}
+
+
+--- A minimal packet implementation
+--
+-- The current code only implements the ACK and ERROR packets
+-- As the server is write-only the other packet types are not needed
+Packet = {
+
+ -- Implements the ACK packet
+ ACK = {
+
+ new = function( self, block )
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.block = block
+ return o
+ end,
+
+ __tostring = function( self )
+ return string.pack(">I2I2", OpCode.ACK, self.block)
+ end,
+
+ },
+
+ -- Implements the error packet
+ ERROR = {
+
+ new = function( self, code, msg )
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.msg = msg
+ o.code = code
+ return o
+ end,
+
+ __tostring = function( self )
+ return string.pack(">I2I2z", OpCode.ERROR, self.code, self.msg)
+ end,
+ }
+
+}
+
+--- The File class holds files received by the TFTP server
+File = {
+
+ --- Creates a new file object
+ --
+ -- @param filename string containing the filename
+ -- @param content string containing the file content
+ -- @return o new class instance
+ new = function(self, filename, content, sender)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.name = filename
+ o.content = content
+ o.sender = sender
+ return o
+ end,
+
+ getContent = function(self) return self.content end,
+ setContent = function(self, content) self.content = content end,
+
+ getName = function(self) return self.name end,
+ setName = function(self, name) self.name = name end,
+
+ setSender = function(self, sender) self.sender = sender end,
+ getSender = function(self) return self.sender end,
+}
+
+
+-- The thread dispatcher is called by the start function once
+local function dispatcher()
+
+ local last = os.time()
+ local f_condvar = nmap.condvar(infiles)
+ local s_condvar = nmap.condvar(state)
+
+ while(true) do
+
+ -- check if other scripts are active
+ local counter = 0
+ for t in pairs(running) do
+ counter = counter + 1
+ end
+ if ( counter == 0 ) then
+ state = "STOPPING"
+ s_condvar "broadcast"
+ end
+
+ if #threads == 0 then break end
+ for i, thread in ipairs(threads) do
+ local status, res = coroutine.resume(thread)
+ if ( not(res) ) then -- thread finished its task?
+ table.remove(threads, i)
+ break
+ end
+ end
+
+ -- Make sure to process waitFile atleast every 2 seconds
+ -- in case no files have arrived
+ if ( os.time() - last >= 2 ) then
+ last = os.time()
+ f_condvar "broadcast"
+ end
+
+ end
+ state = "STOPPED"
+ s_condvar "broadcast"
+ stdnse.debug1("Exiting _dispatcher")
+end
+
+-- Processes a new incoming file transfer
+-- Currently only uploads are supported
+--
+-- @param host containing the hostname or ip of the initiating host
+-- @param port containing the port of the initiating host
+-- @param data string containing the initial data passed to the server
+local function processConnection( host, port, data )
+ local op, pos = string.unpack(">I2", data)
+ local socket = nmap.new_socket("udp")
+
+ socket:set_timeout(1000)
+ local status, err = socket:connect(host, port)
+ if ( not(status) ) then return status, err end
+
+ socket:set_timeout(10)
+
+ -- If we get anything else than a write request, abort the connection
+ if ( OpCode.WRQ ~= op ) then
+ stdnse.debug1("Unsupported opcode")
+ socket:send( tostring(Packet.ERROR:new(0, "TFTP server has write-only support")))
+ end
+
+ local filename, enctype, pos = string.unpack("zz", data, pos)
+ status, err = socket:send( tostring( Packet.ACK:new(0) ) )
+
+ local blocks = {}
+ local lastread = os.time()
+
+ while( true ) do
+ local status, pdata = socket:receive()
+ if ( not(status) ) then
+ -- if we're here and haven't successfully read a packet for 5 seconds, abort
+ if ( os.time() - lastread > 5 ) then
+ coroutine.yield(false)
+ else
+ coroutine.yield(true)
+ end
+ else
+ -- record last time we had a successful read
+ lastread = os.time()
+ op, pos = string.unpack(">I2", pdata)
+ if ( OpCode.DATA ~= op ) then
+ stdnse.debug1("Expected a data packet, terminating TFTP transfer")
+ end
+
+ local block, data
+ block, data, pos = string.unpack(">I2 c" .. #pdata - 4, pdata, pos )
+
+ blocks[block] = data
+
+ -- First block was not 1
+ if ( #blocks == 0 ) then
+ socket:send( tostring(Packet.ERROR:new(0, "Did not receive block 1")))
+ break
+ end
+
+ -- for every fifth block check that we've received the preceding four
+ if ( ( #blocks % 5 ) == 0 ) then
+ for b = #blocks - 4, #blocks do
+ if ( not(blocks[b]) ) then
+ socket:send( tostring(Packet.ERROR:new(0, "Did not receive block " .. b)))
+ end
+ end
+ end
+
+ -- Ack the data block
+ status, err = socket:send( tostring(Packet.ACK:new(block)) )
+
+ if ( ( #blocks % 20 ) == 0 ) then
+ -- yield every 5th iteration so other threads may work
+ coroutine.yield(true)
+ end
+
+ -- If the data length was less than 512, this was our last block
+ if ( #data < 512 ) then
+ socket:close()
+ break
+ end
+ end
+ end
+
+ local filecontent = {}
+
+ -- Make sure we received all the blocks needed to proceed
+ for i=1, #blocks do
+ if ( not(blocks[i]) ) then
+ return false, ("Block #%d was missing in transfer")
+ end
+ filecontent[#filecontent+1] = blocks[i]
+ end
+ stdnse.debug1("Finished receiving file \"%s\"", filename)
+
+ -- Add anew file to the global infiles table
+ table.insert( infiles, File:new(filename, table.concat(filecontent), host) )
+
+ local condvar = nmap.condvar(infiles)
+ condvar "broadcast"
+end
+
+-- Waits for a connection from a client
+local function waitForConnection()
+
+ local srvsock = nmap.new_socket("udp")
+ local status = srvsock:bind(nil, 69)
+ assert(status, "Failed to bind to TFTP server port")
+
+ srvsock:set_timeout(0)
+
+ while( state == "RUNNING" ) do
+ local status, data = srvsock:receive()
+ if ( not(status) ) then
+ coroutine.yield(true)
+ else
+ local status, _, _, rhost, rport = srvsock:get_info()
+ local x = coroutine.create( function() processConnection(rhost, rport, data) end )
+ table.insert( threads, x )
+ coroutine.yield(true)
+ end
+ end
+end
+
+
+--- Starts the TFTP server and creates a new thread handing over to the dispatcher
+function start()
+ local disp = nil
+ local mutex = nmap.mutex("srvsocket")
+
+ -- register a running script
+ running[coroutine.running()] = true
+
+ mutex "lock"
+ if ( state == "STOPPED" ) then
+ srvthread = coroutine.running()
+ table.insert( threads, coroutine.create( waitForConnection ) )
+ stdnse.new_thread( dispatcher )
+ state = "RUNNING"
+ end
+ mutex "done"
+
+end
+
+local function waitLast()
+ -- The thread that started the server needs to wait here until the rest
+ -- of the scripts finish running. We know we are done once the state
+ -- shifts to STOPPED and we get a signal from the condvar in the
+ -- dispatcher
+ local s_condvar = nmap.condvar(state)
+ while( srvthread == coroutine.running() and state ~= "STOPPED" ) do
+ s_condvar "wait"
+ end
+end
+
+--- Waits for a file with a specific filename for at least the number of
+-- seconds specified by the timeout parameter.
+--
+-- If this function is called from the thread that's running the server it will
+-- wait until all the other threads have finished executing before returning.
+--
+-- @param filename string containing the name of the file to receive
+-- @param timeout number containing the minimum number of seconds to wait
+-- for the file to be received
+-- @return status true on success false on failure
+-- @return File instance on success, nil on failure
+function waitFile( filename, timeout )
+ local condvar = nmap.condvar(infiles)
+ local t = os.time()
+ while(os.time() - t < timeout) do
+ for _, f in ipairs(infiles) do
+ if (f:getName() == filename) then
+ running[coroutine.running()] = nil
+ waitLast()
+ return true, f
+ end
+ end
+ condvar "wait"
+ end
+ -- de-register a running script
+ running[coroutine.running()] = nil
+ waitLast()
+
+ return false
+end
+
+return _ENV;