diff --git a/nselib/tftp.lua b/nselib/tftp.lua new file mode 100644 index 000000000..42a75fecc --- /dev/null +++ b/nselib/tftp.lua @@ -0,0 +1,339 @@ +--- 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 +-- * Packet +-- ** The Packet classes contain one class for each TFTP operation. +-- * File +-- ** The File class holds a recieved file including the name and contents +-- * ConnHandler +-- ** The ConnHandler class handles and processes incoming connections. +-- +-- The following code snipplet starts the TFTP server and waits for the file incoming.txt +-- to be uploaded for 10 seconds: +-- +-- tftp.start() +-- local status, f = tftp.waitFile("incoming.txt", 10) +-- if ( status ) then return f:getContent() end +-- +-- +-- @author Patrik Karlsson +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- + +-- version 0.2 +-- +-- 2011-01-22 - re-wrote library to use coroutines instead of new_thread code. + +module(... or "tftp", package.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 bin.pack(">SS", 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 bin.pack(">SSz", 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 + + local n = table.getn(threads) + if ( n == 0 ) then break end + for i=1,n do + local status, res = coroutine.resume(threads[i]) + 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.print_debug("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 pos, op = bin.unpack(">S", 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.print_debug("Unsupported opcode") + socket:send( tostring(Packet.ERROR:new(0, "TFTP server has write-only support"))) + end + + local pos, filename, enctype = bin.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 havent succesfully 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 succesful read + lastread = os.time() + pos, op = bin.unpack(">S", pdata) + if ( OpCode.DATA ~= op ) then + stdnse.print_debug("Expected a data packet, terminating TFTP transfer") + end + + local block, data + pos, block, data = bin.unpack(">SA" .. #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 fith block check that we've received the preceeding 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 .. blocks[i] + end + stdnse.print_debug("Finnished receiving file \"%s\"", filename) + + -- Add anew file to the global infiles table + table.insert( infiles, File:new(filename, 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 finnish running. We know we are done once the state + -- shifts to STOPPED and we get a singla 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 finnished 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 \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 340d5c510..dc0220acf 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -126,6 +126,7 @@ Entry { filename = "realvnc-auth-bypass.nse", categories = { "auth", "default", Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } } Entry { filename = "rmi-dumpregistry.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "rpcinfo.nse", categories = { "discovery", "safe", } } +Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "skypev2-version.nse", categories = { "version", } } Entry { filename = "smb-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } } @@ -149,6 +150,7 @@ Entry { filename = "smtp-strangeport.nse", categories = { "malware", "safe", } } Entry { filename = "sniffer-detect.nse", categories = { "discovery", "intrusive", } } Entry { filename = "snmp-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "snmp-interfaces.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "snmp-ios-config.nse", categories = { "intrusive", } } Entry { filename = "snmp-netstat.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "snmp-processes.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "snmp-sysdescr.nse", categories = { "default", "discovery", "safe", } } diff --git a/scripts/snmp-ios-config.nse b/scripts/snmp-ios-config.nse new file mode 100644 index 000000000..f8a393722 --- /dev/null +++ b/scripts/snmp-ios-config.nse @@ -0,0 +1,204 @@ +description = [[ +Download IOS configuration using SNMP RW (v1) and displays the result or saves it to a file. +]] + +--- +-- @usage +-- nmap -sU -p 161 --script snmp-ios-config --script-args snmpcommunity= +-- +-- @output +-- | snmp-ios-config: +-- | ! +-- | version 12.3 +-- | service timestamps debug datetime msec +-- | service timestamps log datetime msec +-- | no service password-encryption +-- | ! +-- | hostname Router +-- | ! +-- | boot-start-marker +-- | boot-end-marker +-- +-- +-- @args snmp-ios-config.tftproot If set, specifies to what directory the downloaded config should be saved +-- +-- Version 0.2 +-- Created 01/03/2011 - v0.1 - created by Vikas Singhal +-- Revised 02/22/2011 - v0.2 - cleaned up and added support for built-in tftp, Patrik Karlsson + +author = "Vikas Singhal, Patrik Karlsson" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"intrusive"} + +dependencies = {"snmp-brute"} + +require "shortport" +require "snmp" +require "tftp" + +portrule = shortport.portnumber(161, "udp", {"open", "open|filtered"}) + +local function sendrequest(socket, oid, setparam) + local payload + local options = {} + options.reqId = 28428 -- unnecessary? + payload = snmp.encode(snmp.buildPacket(snmp.buildSetRequest(options, oid,setparam))) + + try(socket:send(payload)) + + -- read in any response we might get + local status, response = socket:receive() + if ( not(status) ) then return status, response end + + local result = snmp.fetchFirst(response) + return true +end + +--- +-- Sends SNMP packets to host and reads responses +action = function(host, port) + + local tftproot = stdnse.get_script_args("snmp-ios-config.tftproot") + + if ( tftproot and not( tftproot:match("[\\/]+$") ) ) then + return "ERROR: tftproot needs to end with slash" + end + + -- create the socket used for our connection + local socket = nmap.new_socket() + + -- set a reasonable timeout value + socket:set_timeout(5000) + + -- do some exception handling / cleanup + catch = function() socket:close() end + + try = nmap.new_try(catch) + + -- connect to the potential SNMP system + try(socket:connect(host.ip, port.number, "udp")) + + local status, tftpserver, _, _, _ = socket:get_info() + if( not(status) ) then + return "ERROR: Failed to determin local ip" + end + + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.2.9999 (ConfigCopyProtocol is set to TFTP [1] ) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.2.9999",1) + + -- Fail silently if the first request doesn't get a proper response + if ( not(request) ) then return end + + -- since we got something back, the port is definitely open + nmap.set_port_state(host, port, "open") + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.3 (SourceFileType is set to running-config [4] ) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.3.9999",4) + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.4 (DestinationFileType is set to networkfile [1] ) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.4.9999",1) + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.15 (ServerAddress is set to the IP address of the TFTP server ) + + local tbl = {} + tbl._snmp = '40' + for octet in tftpserver:gmatch("%d+") do + table.insert(tbl, octet) + end + + request = sendrequest(socket, nil, { { snmp.str2oid(".1.3.6.1.4.1.9.9.96.1.1.1.1.5.9999"), tbl } } ) + -- request = sendrequest(".1.3.6.1.4.1.9.9.96.1.1.1.1.5.9999",tftpserver) + + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.15 (ServerAddressType is set 1 for ipv4 ) + -- more options - 1:ipv4, 2:ipv6, 3:ipv4z, 4:ipv6z, 16:dns + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.15.9999",1) + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.16 (ServerAddress is set to the IP address of the TFTP server ) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.16.9999",tftpserver) + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.6 (CopyFilename is set to IP-config) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.6.9999",host.ip .. "-config") + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.14 (Start copying by setting CopyStatus to active [1]) + -- more options: 1:active, 2:notInService, 3:notReady, 4:createAndGo, 5:createAndWait, 6:destroy + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.14.9999",1) + + -- wait for sometime and print the status of filetransfer + tftp.start() + local status, infile = tftp.waitFile(host.ip .. "-config", 10) + + -- build a SNMP v1 packet + -- get value: .1.3.6.1.4.1.9.9.96.1.1.1.1.10 (Check the status of filetransfer) 1:waiting, 2:running, 3:successful, 4:failed + + local options = {} + options.reqId = 28428 + local payload = snmp.encode(snmp.buildPacket(snmp.buildGetRequest(options, ".1.3.6.1.4.1.9.9.96.1.1.1.1.10.9999"))) + + try(socket:send(payload)) + + local status + local response + -- read in any response we might get + status, response = socket:receive() + + if (not status) or (response == "TIMEOUT") then + return "\n ERROR: Failed to receive cisco configuration file" + end + + local result + result = snmp.fetchFirst(response) + + if result == 3 then + result = ( infile and infile:getContent() ) + + if ( tftproot ) then + local fname = tftproot .. host.ip .. "-config" + local file, err = io.open(fname, "w") + if ( file ) then + file:write(result) + file:close() + else + return "\n ERROR: " .. file + end + result = ("\n Configuration saved to (%s)"):format(fname) + end + else + result = "Not successful! error code: " .. result .. " (1:waiting, 2:running, 3:successful, 4:failed)" + end + + ------------------------------------------------- + -- build a SNMP v1 packet + -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.14 (Destroy settings by setting CopyStatus to destroy [6]) + + request = sendrequest(socket, ".1.3.6.1.4.1.9.9.96.1.1.1.1.14.9999",6) + + try(socket:close()) + + return result +end +