diff --git a/CHANGELOG b/CHANGELOG index d69068bd7..23f8a2b0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Improved error handling and reporting and re-designed communication + class in RPC library with patch from Djalal Harouni. [Patrik] + o Upgraded the included libpcap to version 1.1.1. [David] o [NSE] Add some special use IPv4 addresses to isPrivate which are described in diff --git a/nselib/rpc.lua b/nselib/rpc.lua index 8f20a203c..942fd43b0 100644 --- a/nselib/rpc.lua +++ b/nselib/rpc.lua @@ -12,7 +12,9 @@ -- -------- -- The library contains the following classes: -- o Comm +-- - Handles network connections -- - Handles low-level packet sending, recieving, decoding and encoding +-- - Stores rpc programs info: socket, protocol, program name, id and version -- - Used by Mount, NFS, RPC and Portmap -- o Mount -- - Handles communication with the mount RPC program @@ -93,18 +95,32 @@ require("datafiles") -- Revised 02/22/2010 - v0.2 - cleanup, revised the way TCP/UDP are handled fo -- encoding an decoding -- Revised 03/13/2010 - v0.3 - re-worked library to be OO +-- Revised 04/18/2010 - v0.4 - Applied patch from Djalal Harouni with improved +-- error checking and re-designed Comm class. see: +-- http://seclists.org/nmap-dev/2010/q2/232 -- +-- RPC args using the nmap.registry.args +RPC_args = { + ["rpcbind"] = { proto = 'rpc.protocol' }, + ["nfs"] = { ver = 'nfs.version' }, + ["mountd"] = { ver = 'mount.version' }, +} + + -- Defines the order in which to try to connect to the RPC programs -- TCP appears to be more stable than UDP in most cases, so try it first -local RPC_PROTOCOLS = ( nmap.registry.args and nmap.registry.args['rpc.protocol'] and type(nmap.registry.args['rpc.protocol']) == 'table') and nmap.registry.args['rpc.protocol'] or { "tcp", "udp" } +local RPC_PROTOCOLS = (nmap.registry.args and nmap.registry.args[RPC_args['rpcbind'].proto] and + type(nmap.registry.args[RPC_args['rpcbind'].proto]) == 'table') and + nmap.registry.args[RPC_args['rpcbind'].proto] or { "tcp", "udp" } -- used to cache the contents of the rpc datafile local RPC_PROGRAMS -- Supported protocol versions -Version = { +RPC_version = { + ["rpcbind"] = { min=2, max=2 }, ["nfs"] = { min=1, max=3 }, ["mountd"] = { min=1, max=3 }, } @@ -114,13 +130,98 @@ math.randomseed( os.time() ) -- Low-level communication class Comm = { - new = function(self,o) - o = o or {} - setmetatable(o, self) + --- Creats a new rpc Comm object + -- + -- @param program name string + -- @param version number containing the program version to use + -- @return a new Comm object + new = function(self, program, version) + local o = {} + setmetatable(o, self) self.__index = self + o.program = program + o.program_id = Util.ProgNameToNumber(program) + o:SetVersion(version) return o - end, - + end, + + --- Connects to the remote program + -- + -- @param host table + -- @param port table + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + Connect = function(self, host, port) + local status, err, socket + status, err = self:ChkProgram() + if (not(status)) then + return status, err + end + status, err = self:ChkVersion() + if (not(status)) then + return status, err + end + socket = nmap.new_socket() + status, err = socket:connect(host.ip, port.number, port.protocol) + if (not(status)) then + return status, string.format("%s connect error: %s", self.program, err) + else + self.socket = socket + self.ip = host.ip + self.port = port.number + self.proto = port.protocol + return status, nil + end + end, + + --- Disconnects from the remote program + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + Disconnect = function(self) + local status, err = self.socket:close() + if (not(status)) then + return status, string.format("%s disconnect error: %s", self.program, err) + end + self.socket=nil + return status, nil + end, + + --- Checks if the rpc program is supported + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + ChkProgram = function(self) + if (not(RPC_version[self.program])) then + return false, string.format("RPC library does not support: %s protocol", self.program) + end + return true, nil + end, + + --- Checks if the rpc program version is supported + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + ChkVersion = function(self) + if ( self.version > RPC_version[self.program].max or self.version < RPC_version[self.program].min ) then + return false, string.format("RPC library does not support: %s version %d",self.program,self.version) + end + return true, nil + end, + + --- Sets the rpc program version + -- + -- @return status boolean true + SetVersion = function(self, version) + if (RPC_version[self.program] and RPC_args[self.program] and + nmap.registry.args and nmap.registry.args[RPC_args[self.program].ver]) then + self.version = tonumber(nmap.registry.args[RPC_args[self.program].ver]) + elseif (not(self.version) and version) then + self.version = version + end + return true, nil + end, + --- Checks if data contains enough bytes to read the needed amount -- If it doesn't it attempts to read the remaining amount of bytes from the socket -- @@ -128,11 +229,10 @@ Comm = { -- @param pos number containing the current offset into the buffer -- @param needed number containing the number of bytes needed to be available -- @return status success or failure - -- @return data string containing the data passed to the function and the additional data appended to it + -- @return data string containing the data passed to the function and the additional data appended to it or error message on failure GetAdditionalBytes = function( self, data, pos, needed ) - local status = true - local tmp + local status, tmp if data:len() - pos + 1 < needed then local toread = needed - ( data:len() - pos + 1 ) @@ -143,18 +243,17 @@ Comm = { return false, string.format("getAdditionalBytes() failed to read: %d bytes from the socket", needed - ( data:len() - pos ) ) end end - return status, data + return true, data end, --- Creates a RPC header -- -- @param xid number - -- @param program_id number containing the program_id to connect to - -- @param program_version number containing the version to query -- @param procedure number containing the procedure to call -- @param auth table containing the authentication data to use - -- @return string of bytes - CreateHeader = function( self, xid, program_id, program_version, procedure, auth ) + -- @return status boolean true on success, false on failure + -- @return string of bytes on success, error message on failure + CreateHeader = function( self, xid, procedure, auth ) local RPC_VERSION = 2 local packet @@ -162,10 +261,10 @@ Comm = { xid = math.random(1234567890) end if not auth or auth.type ~= RPC.AuthType.Null then - return false, "No or invalid authentication type specified" + return false, "Comm.CreateHeader: No or invalid authentication type specified" end - packet = bin.pack( ">IIIIII", xid, RPC.MessageType.Call, RPC_VERSION, program_id, program_version, procedure ) + packet = bin.pack( ">IIIIII", xid, RPC.MessageType.Call, RPC_VERSION, self.program_id, self.version, procedure ) if auth.type == RPC.AuthType.Null then packet = packet .. bin.pack( "IIII", 0, 0, 0, 0 ) end @@ -178,7 +277,7 @@ Comm = { -- @param pos number containing the current offset into data -- @return pos number containing the offset after the decoding -- @return header table containing xid, type, state, - -- verifier and accept_state + -- verifier and ( accept_state or denied_state ) DecodeHeader = function( self, data, pos ) local header = {} local status @@ -191,32 +290,43 @@ Comm = { local tmp status, tmp = self:GetAdditionalBytes( data, pos, HEADER_LEN - ( data:len() - pos ) ) if not status then + stdnse.print_debug(string.format("Comm.ReceivePacket: failed to call GetAdditionalBytes")) return -1, nil end data = data .. tmp end pos, header.xid, header.type, header.state = bin.unpack(">III", data, pos) + + if ( header.state == RPC.State.Denied ) then + pos, header.denied_state = bin.unpack(">I", data, pos ) + return pos, header + end + pos, header.verifier.flavor = bin.unpack(">I", data, pos) pos, header.verifier.length = bin.unpack(">I", data, pos) if header.verifier.length - 8 > 0 then status, data = self:GetAdditionalBytes( data, pos, header.verifier.length - 8 ) if not status then + stdnse.print_debug(string.format("Comm.ReceivePacket: failed to call GetAdditionalBytes")) return -1, nil end pos, header.verifier.data = bin.unpack("A" .. header.verifier.length - 8, data, pos ) end pos, header.accept_state = bin.unpack(">I", data, pos ) + + return pos, header end, --- Reads the response from the socket -- - -- @return data string containing the raw response + -- @return status true on success, false on failure + -- @return data string containing the raw response or error message on failure ReceivePacket = function( self ) local status - + if ( self.proto == "udp" ) then -- There's not much we can do in here to check if we received all data -- as the packet contains no length field. It's up to each decoding function @@ -230,9 +340,9 @@ Comm = { lastfragment = false status, data = self:GetAdditionalBytes( data, pos, 4 ) if ( not(status) ) then - return false, "rpc.Comm.ReceivePacket: failed to call GetAdditionalBytes" + return false, "Comm.ReceivePacket: failed to call GetAdditionalBytes" end - + pos, tmp = bin.unpack(">i", data, pos ) length = bit.band( tmp, 0x7FFFFFFF ) @@ -242,7 +352,7 @@ Comm = { status, data = self:GetAdditionalBytes( data, pos, length ) if ( not(status) ) then - return false, "rpc.Comm.ReceivePacket: failed to call GetAdditionalBytes" + return false, "Comm.ReceivePacket: failed to call GetAdditionalBytes" end -- @@ -279,20 +389,18 @@ Comm = { --- Encodes a RPC packet -- -- @param xid number containing the transaction ID - -- @param prog number containing the program id + -- @param procedure number containing the procedure to call -- @param auth table containing authentication information -- @param data string containing the packet data -- @return packet string containing the encoded packet data - EncodePacket = function( self, xid, prog, auth, data ) - local status, packet = self:CreateHeader( xid, prog.id, prog.version, prog.proc, auth ) + EncodePacket = function( self, xid, proc, auth, data ) + local status, packet = self:CreateHeader( xid, proc, auth ) local len - if ( not(status) ) then return end packet = packet .. ( data or "" ) - if ( self.proto == "udp") then return packet else @@ -305,8 +413,9 @@ Comm = { SendPacket = function( self, packet ) return self.socket:send( packet ) end, - + } + --- Mount class handling communication with the mountd program -- @@ -315,6 +424,31 @@ Comm = { -- Mount = { + StatMsg = { + [1] = "Not owner.", + [2] = "No such file or directory.", + [5] = "I/O error.", + [13] = "Permission denied.", + [20] = "Not a directory.", + [22] = "Invalid argument.", + [63] = "Filename too long.", + [10004] = "Operation not supported.", + [10006] = "A failure on the server.", + }, + + StatCode = { + MNT_OK = 0, + MNTERR_PERM = 1, + MNTERR_NOENT = 2, + MNTERR_IO = 5, + MNTERR_ACCES = 13, + MNTERR_NOTDIR = 20, + MNTERR_INVAL = 22, + MNTERR_NAMETOOLONG = 63, + MNTERR_NOTSUPP = 10004, + MNTERR_SERVERFAULT = 10006, + }, + Procedure = { MOUNT = 1, @@ -329,57 +463,17 @@ Mount = { setmetatable(o, self) self.__index = self return o - end, - - --- Connects to the mountd program - -- - -- @param host table - -- @param port table - -- @param version number containing the program version to use - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Connect = function( self, host, port, version ) - local socket = nmap.new_socket() - local status, result = socket:connect(host.ip, port.number, port.protocol) - - if ( status ) then - self.socket = socket - self.proto = port.protocol - self.comm = Comm:new( { socket = socket, proto=port.protocol} ) - self.version = ( nmap.registry.args and nmap.registry.args['mount.version'] ) and tonumber(nmap.registry.args['mount.version']) or version - - if ( self.version > Version["mountd"].max or self.version < Version["mountd"].min ) then - return false, "Library does not support mountd version: " .. self.version - end - end - - return status, result - end, - - --- Disconnects from the mountd program - -- - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Disconnect = function( self ) - local status, result = self.socket:close() - if ( status ) then - self.proto = nil - self.socket = nil - self.comm = nil - end - return status, result end, --- Requests a list of NFS export from the remote server -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @return status success or failure -- @return entries table containing a list of share names (strings) - Export = function( self ) + Export = function(self, comm) - local catch = function() self.socket:close() end - local try = nmap.new_try(catch) local msg_type = 0 - local prg_mount = Util.ProgNameToNumber("mountd") local packet local pos = 1 local header = {} @@ -387,37 +481,43 @@ Mount = { local data = "" local status - local REPLY_ACCEPTED, SUCCESS, PROC_EXPORT = 0, 0, 5 - - if self.proto ~= "tcp" and self.proto ~= "udp" then - return false, "Protocol should be either udp or tcp" + if comm.proto ~= "tcp" and comm.proto ~= "udp" then + return false, "Mount.Export: Protocol should be either udp or tcp" + end + packet = comm:EncodePacket(nil, Mount.Procedure.EXPORT, { type=RPC.AuthType.Null }, nil ) + if (not(comm:SendPacket( packet ))) then + return false, "Mount.Export: Failed to send data" end - packet = self.comm:EncodePacket( nil, { id=prg_mount, version=self.version, proc=Mount.Procedure.EXPORT }, { type=RPC.AuthType.Null }, nil ) - try( self.comm:SendPacket( packet ) ) - status, data = self.comm:ReceivePacket() + status, data = comm:ReceivePacket() if ( not(status) ) then - return false, "mountExportCall: Failed to read data from socket" + return false, "Mount.Export: Failed to read data from socket" end -- make sure we have atleast 24 bytes to unpack the header - data = try( self.comm:GetAdditionalBytes( data, pos, 24 ) ) - pos, header = self.comm:DecodeHeader( data, pos ) - + status, data = comm:GetAdditionalBytes( data, pos, 24 ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end + pos, header = comm:DecodeHeader( data, pos ) if not header then - return false, "Failed to decode header" + return false, "Mount.Export: Failed to decode header" end if header.type ~= RPC.MessageType.Reply then - return false, string.format("Packet was not a reply") + return false, "Mount.Export: packet was not a reply" end - if header.state ~= REPLY_ACCEPTED then - return false, string.format("Reply state was not Accepted(0) as expected") + if header.state ~= RPC.State.Accepted then + if ( header.denied_state == RPC.RejectState.AUTH_ERROR ) then + return false, "Mount.Export: RPC Authentication Failed" + else + return false, "Mount.Export: Reply state was not Accepted(0) as expected" + end end - if header.accept_state ~= SUCCESS then - return false, string.format("Accept State was not Successful") + if header.accept_state ~= RPC.AcceptState.SUCCESS then + return false, "Mount.Export: Accept State was not Successful" end --- @@ -438,7 +538,10 @@ Mount = { --- while true do -- make sure we have atleast 4 more bytes to check for value follows - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end local data_follows pos, data_follows = bin.unpack( ">I", data, pos ) @@ -452,10 +555,16 @@ Mount = { local len -- make sure we have atleast 4 more bytes to get the length - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end pos, len = bin.unpack(">I", data, pos ) - data = try( self.comm:GetAdditionalBytes( data, pos, len ) ) + status, data = comm:GetAdditionalBytes( data, pos, len ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end pos, entry.name = bin.unpack("A" .. len, data, pos ) pos = pos + Util.CalcFillBytes( len ) @@ -463,16 +572,25 @@ Mount = { while true do local group - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end pos, data_follows = bin.unpack( ">I", data, pos ) if data_follows ~= 1 then break end - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end pos, len = bin.unpack( ">I", data, pos ) - data = try( self.comm:GetAdditionalBytes( data, pos, len ) ) + status, data = comm:GetAdditionalBytes( data, pos, len ) + if (not(status)) then + return false, "Mount.Export: Failed to call GetAdditionalBytes" + end pos, group = bin.unpack( "A" .. len, data, pos ) table.insert( entry, group ) @@ -483,73 +601,85 @@ Mount = { return true, entries end, - --- Attempts to mount a remote export in order to get the filehandle -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param path string containing the path to mount -- @return status success or failure -- @return fhandle string containing the filehandle of the remote export - Mount = function( self, path ) - - local catch = function() self.socket:close() end - local try = nmap.new_try(catch) - local packet, data - local prog_id = Util.ProgNameToNumber("mountd") + Mount = function(self, comm, path) + local packet, mount_status local _, pos, data, header, fhandle = "", 1, "", "", {} local status, len - local REPLY_ACCEPTED, SUCCESS, MOUNT_OK = 0, 0, 0 - data = bin.pack(">IA", path:len(), path) for i=1, Util.CalcFillBytes( path:len() ) do data = data .. string.char( 0x00 ) end - packet = self.comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=Mount.Procedure.MOUNT }, { type=RPC.AuthType.Null }, data ) - try( self.comm:SendPacket( packet ) ) - - status, data = self.comm:ReceivePacket() - if ( not(status) ) then - return false, "mountCall: Failed to read data from socket" + packet = comm:EncodePacket( nil, Mount.Procedure.MOUNT, { type=RPC.AuthType.Null }, data ) + if (not(comm:SendPacket(packet))) then + return false, "Mount: Failed to send data" end - pos, header = self.comm:DecodeHeader( data, pos ) + status, data = comm:ReceivePacket() + if ( not(status) ) then + return false, "Mount: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, pos ) if not header then - return false, "Failed to decode header" + return false, "Mount: Failed to decode header" end if header.type ~= RPC.MessageType.Reply then - return false, string.format("Packet was not a reply") + return false, "Mount: Packet was not a reply" end - if header.state ~= REPLY_ACCEPTED then - return false, string.format("Reply state was not Accepted(0) as expected") - end - - if header.accept_state ~= SUCCESS then - return false, string.format(3, "mountCall: Accept State was not Successful", path) - end - - local mount_status - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) - pos, mount_status = bin.unpack(">I", data, pos ) - - if mount_status ~= MOUNT_OK then - if ( mount_status == 13 ) then - return false, "Access Denied" + if header.state ~= RPC.State.Accepted then + if ( header.denied_state == RPC.RejectState.AUTH_ERROR ) then + return false, "Mount: RPC Authentication Failed" else - return false, string.format("Mount failed: %d", mount_status) + return false, "Mount: Reply state was not Accepted(0) as expected" end end - if ( self.version == 3 ) then - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + if header.accept_state ~= RPC.AcceptState.SUCCESS then + return false, string.format(3, "Mount: Accept State was not Successful", path) + end + + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount: Failed to call GetAdditionalBytes" + end + pos, mount_status = bin.unpack(">I", data, pos ) + + if (mount_status ~= Mount.StatCode.MNT_OK) then + if (Mount.StatMsg[mount_status]) then + return false, string.format("Mount failed: %s",Mount.StatMsg[mount_status]) + else + return false, string.format("Mount failed: code %d", mount_status) + end + end + + if ( comm.version == 3 ) then + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + return false, "Mount: Failed to call GetAdditionalBytes" + end _, len = bin.unpack(">I", data, pos ) - data = try( self.comm:GetAdditionalBytes( data, pos, len + 4 ) ) + status, data = comm:GetAdditionalBytes( data, pos, len + 4 ) + if (not(status)) then + return false, "Mount: Failed to call GetAdditionalBytes" + end pos, fhandle = bin.unpack( "A" .. len + 4, data, pos ) - elseif ( self.version < 3 ) then - data = try( self.comm:GetAdditionalBytes( data, pos, 32 ) ) + elseif ( comm.version < 3 ) then + status, data = comm:GetAdditionalBytes( data, pos, 32 ) + if (not(status)) then + return false, "Mount: Failed to call GetAdditionalBytes" + end pos, fhandle = bin.unpack( "A32", data, pos ) else return false, "Mount failed" @@ -560,19 +690,14 @@ Mount = { --- Attempts to unmount a remote export in order to get the filehandle -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param path string containing the path to mount -- @return status success or failure -- @return error string containing error if status is false - Unmount = function( self, path ) - - local catch = function() self.socket:close() end - local try = nmap.new_try(catch) - local packet, data - local prog_id = Util.ProgNameToNumber("mountd") + Unmount = function(self, comm, path) + local packet, status local _, pos, data, header, fhandle = "", 1, "", "", {} - local status - - local REPLY_ACCEPTED, SUCCESS, MOUNT_OK = 0, 0, 0 data = bin.pack(">IA", path:len(), path) @@ -580,29 +705,31 @@ Mount = { data = data .. string.char( 0x00 ) end - packet = self.comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=Mount.Procedure.UMNT }, { type=RPC.AuthType.Null }, data ) - try( self.comm:SendPacket( packet ) ) - - status, data = self.comm:ReceivePacket( ) - if ( not(status) ) then - return false, "mountCall: Failed to read data from socket" + packet = comm:EncodePacket( nil, Mount.Procedure.UMNT, { type=RPC.AuthType.Null }, data ) + if (not(comm:SendPacket(packet))) then + return false, "Unmount: Failed to send data" end - pos, header = self.comm:DecodeHeader( data, pos ) + status, data = comm:ReceivePacket( ) + if ( not(status) ) then + return false, "Unmount: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, pos ) if not header then - return false, "Failed to decode header" + return false, "Unmount: Failed to decode header" end if header.type ~= RPC.MessageType.Reply then - return false, string.format("Packet was not a reply") + return false, "Unmount: Packet was not a reply" end - if header.state ~= REPLY_ACCEPTED then - return false, string.format("Reply state was not Accepted(0) as expected") + if header.state ~= RPC.State.Accepted then + return false, "Unmount: Reply state was not Accepted(0) as expected" end - if header.accept_state ~= SUCCESS then - return false, string.format(3, "mountCall: Accept State was not Successful", path) + if header.accept_state ~= RPC.AcceptState.SUCCESS then + return false, string.format(3, "Unmount: Accept State was not Successful", path) end return true, "" @@ -617,6 +744,118 @@ Mount = { -- NFS = { + -- NFS error msg v2 and v3 + StatMsg = { + [1] = "Not owner.", + [2] = "No such file or directory.", + [5] = "I/O error.", + [6] = "I/O error. No such device or address.", + [13] = "Permission denied.", + [17] = "File exists.", + [18] = "Attempt to do a cross-device hard link.", + [19] = "No such device.", + [20] = "Not a directory.", + [21] = "Is a directory.", + [22] = "Invalid argument or unsupported argument for an operation.", + [27] = "File too large.", + [28] = "No space left on device.", + [30] = "Read-only file system.", + [31] = "Too many hard links.", + [63] = "The filename in an operation was too long.", + [66] = "An attempt was made to remove a directory that was not empty.", + [69] = "Resource (quota) hard limit exceeded.", + [70] = "Invalid file handle.", + [71] = "Too many levels of remote in path.", + [99] = "The server's write cache used in the \"WRITECACHE\" call got flushed to disk.", + [10001] = "Illegal NFS file handle.", + [10002] = "Update synchronization mismatch was detected during a SETATTR operation.", + [10003] = "READDIR or READDIRPLUS cookie is stale.", + [10004] = "Operation is not supported.", + [10005] = "Buffer or request is too small.", + [10006] = "An error occurred on the server which does not map to any of the legal NFS version 3 protocol error values.", + [10007] = "An attempt was made to create an object of a type not supported by the server.", + [10008] = "The server initiated the request, but was not able to complete it in a timely fashion.", + }, + + StatCode = { + -- NFS Version 1 + [1] = { + NFS_OK = 0, + NFSERR_PERM = 1, + NFSERR_NOENT = 2, + NFSERR_IO = 5, + NFSERR_NXIO = 6, + NFSERR_ACCES = 13, + NFSERR_EXIST = 17, + NFSERR_NODEV = 19, + NFSERR_NOTDIR = 20, + NFSERR_ISDIR = 21, + NFSERR_FBIG = 27, + NFSERR_NOSPC = 28, + NFSERR_ROFS = 30, + NFSERR_NAMETOOLONG = 63, + NFSERR_NOTEMPTY = 66, + NFSERR_DQUOT = 69, + NFSERR_STALE = 70, + NFSERR_WFLUSH = 99, + }, + + -- NFS Version 2 + [2] = { + NFS_OK = 0, + NFSERR_PERM = 1, + NFSERR_NOENT = 2, + NFSERR_IO = 5, + NFSERR_NXIO = 6, + NFSERR_ACCES = 13, + NFSERR_EXIST = 17, + NFSERR_NODEV = 19, + NFSERR_NOTDIR = 20, + NFSERR_ISDIR = 21, + NFSERR_FBIG = 27, + NFSERR_NOSPC = 28, + NFSERR_ROFS = 30, + NFSERR_NAMETOOLONG = 63, + NFSERR_NOTEMPTY = 66, + NFSERR_DQUOT = 69, + NFSERR_STALE = 70, + NFSERR_WFLUSH = 99, + }, + + -- NFS Version 3 + [3] = { + NFS_OK = 0, + NFSERR_PERM = 1, + NFSERR_NOENT = 2, + NFSERR_IO = 5, + NFSERR_NXIO = 6, + NFSERR_ACCES = 13, + NFSERR_EXIST = 17, + NFSERR_XDEV = 18, + NFSERR_NODEV = 19, + NFSERR_NOTDIR = 20, + NFSERR_ISDIR = 21, + NFSERR_INVAL = 22, + NFSERR_FBIG = 27, + NFSERR_NOSPC = 28, + NFSERR_ROFS = 30, + NFSERR_MLINK = 31, + NFSERR_NAMETOOLONG = 63, + NFSERR_NOTEMPTY = 66, + NFSERR_DQUOT = 69, + NFSERR_STALE = 70, + NFSERR_REMOTE = 71, + NFSERR_BADHANDLE = 10001, + NFSERR_NOT_SYNC = 10002, + NFSERR_BAD_COOKIE = 10003, + NFSERR_NOTSUPP = 10004, + NFSERR_TOOSMALL = 10005, + NFSERR_SERVERFAULT = 10006, + NFSERR_BADTYPE = 10007, + NFSERR_JUKEBOX = 10008, + }, + }, + -- Unfortunately the NFS procedure numbers differ in between versions Procedure = { @@ -664,50 +903,12 @@ NFS = { setmetatable(o, self) self.__index = self return o - end, + end, - --- Connects to the nfsd program - -- - -- @param host table - -- @param port table - -- @param version number containing the program version to use - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Connect = function( self, host, port, version ) - local socket = nmap.new_socket() - local status, result = socket:connect(host.ip, port.number, port.protocol) - - if ( status ) then - self.socket = socket - self.proto = port.protocol - self.version = ( nmap.registry.args and nmap.registry.args['nfs.version'] ) and tonumber(nmap.registry.args['nfs.version']) or version - - if ( self.version > Version["nfs"].max or self.version < Version["nfs"].min ) then - return false, "Library does not support nfsd version: " .. self.version - end - - self.comm = Comm:new( { socket = socket, proto=port.protocol} ) - end - - return status, result - end, - - --- Disconnects from the nfsd program - -- - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Disconnect = function( self ) - local status, result = self.socket:close() - if ( status ) then - self.proto = nil - self.socket = nil - self.comm = nil - end - return status, result - end, - - --- Decodes the READDIR section of a NFS ReadDir response + --- Decodes the READDIR section of a NFS ReadDir response -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param data string containing the buffer of bytes read so far -- @param pos number containing the current offset into data -- @return pos number containing the offset after the decoding @@ -717,47 +918,54 @@ NFS = { -- table for each file/directory entry. It has the following fields -- file_id, name and cookie -- - ReadDirDecode = function( self, data, pos ) - + ReadDirDecode = function( self, comm, data, pos ) local entry, response = {}, {} local value_follows local status, _ - local NFS_OK = 0 - - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, status = bin.unpack(">I", data, pos) - if status ~= NFS_OK then + if (status ~= NFS.StatCode[comm.version].NFS_OK) then + if (NFS.StatMsg[status]) then + stdnse.print_debug(string.format("READDIR query failed: %s", NFS.StatMsg[status])) + else + stdnse.print_debug(string.format("READDIR query failed: code %d", status)) + end return -1, nil end - if ( 3 == self.version ) then + if ( 3 == comm.version ) then local attrib = {} response.attributes = {} - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if( not(status) ) then - return false, "NFS.ReadDirDecode failed to get additional bytes from socket" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end + pos, value_follows = bin.unpack(">I", data, pos) if value_follows == 0 then return -1, nil end - status, data = self.comm:GetAdditionalBytes( data, pos, 84 ) - if( not(status) ) then - return false, "NFS.ReadDirDecode failed to get additional bytes from socket" + status, data = comm:GetAdditionalBytes( data, pos, 84 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, attrib.gid, attrib.size, attrib.used, attrib.rdev, attrib.fsid, attrib.fileid, attrib.atime, attrib.mtime, attrib.ctime = bin.unpack(">IIIIILLLLLLLL", data, pos) table.insert(response.attributes, attrib) -- opaque data - status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 8 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, _ = bin.unpack(">L", data, pos) end @@ -765,9 +973,10 @@ NFS = { response.entries = {} while true do entry = {} - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, value_follows = bin.unpack(">I", data, pos) @@ -776,61 +985,66 @@ NFS = { break end - if ( 3 == self.version ) then - status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + if ( 3 == comm.version ) then + status, data = comm:GetAdditionalBytes( data, pos, 8 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, entry.fileid = bin.unpack(">L", data, pos ) else - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, entry.fileid = bin.unpack(">I", data, pos ) end - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, entry.length = bin.unpack(">I", data, pos) - status, data = self.comm:GetAdditionalBytes( data, pos, entry.length ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, entry.length ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, entry.name = bin.unpack("A" .. entry.length, data, pos) pos = pos + Util.CalcFillBytes( entry.length ) - if ( 3 == self.version ) then - status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + if ( 3 == comm.version ) then + status, data = comm:GetAdditionalBytes( data, pos, 8 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end - pos, entry.cookie = bin.unpack(">L", data, pos) else - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "ReadDirDecode failed" + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("NFS.ReadDirDecode: Failed to call GetAdditionalBytes") + return -1, nil end - pos, entry.cookie = bin.unpack(">I", data, pos) end table.insert( response.entries, entry ) end return pos, response end, - - + --- Reads the contents inside a NFS directory -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param file_handle string containing the filehandle to query -- @return status true on success, false on failure -- @return table of file table entries as described in decodeReadDir - ReadDir = function( self, file_handle ) + ReadDir = function( self, comm, file_handle ) local status, packet local cookie, count = 0, 8192 @@ -838,83 +1052,88 @@ NFS = { local header, response = {}, {} if ( not(file_handle) ) then - return false, "No filehandle received" + return false, "ReadDir: No filehandle received" end - if ( self.version == 3 ) then + if ( comm.version == 3 ) then local opaque_data = 0 data = bin.pack("A>L>L>I", file_handle, cookie, opaque_data, count) else data = bin.pack("A>I>I", file_handle, cookie, count) end - packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].READDIR }, { type=RPC.AuthType.Null }, data ) - status = self.comm:SendPacket( packet ) - if ( not(status) ) then - return false, "nfsReadDir: Failed to write to socket" - end - - status, data = self.comm:ReceivePacket() - if ( not(status) ) then - return false, "nfsReadDir: Failed to read data from socket" + packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].READDIR, { type=RPC.AuthType.Null }, data ) + if(not(comm:SendPacket( packet ))) then + return false, "ReadDir: Failed to send data" end - pos, header = self.comm:DecodeHeader( data, pos ) - if not header then - return false, "Failed to decode header" + status, data = comm:ReceivePacket() + if ( not(status) ) then + return false, "ReadDir: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, pos ) + if not header then + return false, "ReadDir: Failed to decode header" + end + pos, response = self:ReadDirDecode( comm, data, pos ) + if (not(response)) then + return false, "ReadDir: Failed to decode the READDIR section" end - pos, response = self:ReadDirDecode( data, pos ) return true, response end, --- Gets filesystem stats (Total Blocks, Free Blocks and Available block) on a remote NFS share -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param file_handle string containing the filehandle to query -- @return status true on success, false on failure -- @return statfs table with the fields transfer_size, block_size, -- total_blocks, free_blocks and available_blocks -- @return errormsg if status is false - StatFs = function( self, file_handle ) + StatFs = function( self, comm, file_handle ) local status, packet local pos, data, _ = 1, "", "" local header, statfs = {}, {} - if ( self.version > 2 ) then - return false, ("Version %d not supported"):format(self.version) + if ( comm.version > 2 ) then + return false, ("StatFs: Version %d not supported"):format(comm.version) end if ( not(file_handle) or file_handle:len() ~= 32 ) then - return false, "Incorrect filehandle received" + return false, "StatFs: Incorrect filehandle received" end data = bin.pack("A", file_handle ) - packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].STATFS }, { type=RPC.AuthType.Null }, data ) - status = self.comm:SendPacket( packet ) - if ( not(status) ) then - return false, "nfsStatFs: Failed to write to socket" - end - - status, data = self.comm:ReceivePacket( ) - if ( not(status) ) then - return false, "nfsStatFs: Failed to read data from socket" + packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].STATFS, { type=RPC.AuthType.Null }, data ) + if (not(comm:SendPacket( packet ))) then + return false, "StatFS: Failed to send data" end - pos, header = self.comm:DecodeHeader( data, pos ) + status, data = comm:ReceivePacket( ) + if ( not(status) ) then + return false, "StatFs: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, pos ) if not header then - return false, "Failed to decode header" + return false, "StatFs: Failed to decode header" end - pos, statfs = self:StatFsDecode( data, pos ) + pos, statfs = self:StatFsDecode( comm, data, pos ) if not statfs then - return false, "Failed to decode statfs structure" + return false, "StatFs: Failed to decode statfs structure" end return true, statfs end, --- Attempts to decode the attributes section of the reply -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param data string containing the full statfs reply -- @param pos number pointing to the statfs section of the reply -- @return pos number containing the offset after decoding @@ -923,117 +1142,134 @@ NFS = { -- blocksize, rdev, blocks, fsid, -- fileid, atime, mtime and ctime -- - GetAttrDecode = function( self, data, pos ) + GetAttrDecode = function( self, comm, data, pos ) local attrib = {} - local catch = function() self.socket:close() end - local try = nmap.new_try(catch) - local NFS_OK = 0 local status - status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) - if ( not(status) ) then - return false, "GetAttrDecode: GetAdditionalBytes failed" - end - - pos, attrib.status = bin.unpack(">I", data, pos) - - if attrib.status ~= NFS_OK then + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("GetAttrDecode: Failed to call GetAdditionalBytes") return -1, nil end - if ( self.version < 3 ) then - status, data = self.comm:GetAdditionalBytes( data, pos, 64 ) + pos, attrib.status = bin.unpack(">I", data, pos) + + if (attrib.status ~= NFS.StatCode[comm.version].NFS_OK) then + if (NFS.StatMsg[attrib.status]) then + stdnse.print_debug(string.format("GETATTR query failed: %s", NFS.StatMsg[attrib.status])) + else + stdnse.print_debug(string.format("GETATTR query failed: code %d", attrib.status)) + end + return -1, nil + end + + if ( comm.version < 3 ) then + status, data = comm:GetAdditionalBytes( data, pos, 64 ) if ( not(status) ) then - return false, "GetAttrDecode: GetAdditionalBytes failed" + stdnse.print_debug("GetAttrDecode: Failed to call GetAdditionalBytes") + return -1, nil end pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, attrib.gid, attrib.size, attrib.blocksize, attrib.rdev, attrib.blocks, attrib.fsid, attrib.fileid, attrib.atime, attrib.mtime, attrib.ctime = bin.unpack( ">IIIIIIIIIILLL", data, pos ) - elseif ( self.version == 3 ) then - status, data = self.comm:GetAdditionalBytes( data, pos, 84 ) - if ( not(status) ) then - return false, "GetAttrDecode: GetAdditionalBytes failed" - end - pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, - attrib.gid, attrib.size, attrib.used, attrib.rdev, - attrib.fsid, attrib.fileid, attrib.atime, attrib.mtime, - attrib.ctime = bin.unpack(">IIIIILLLLLLLL", data, pos) - else - return -1, "Unsupported version" - end - return pos, attrib - end, - --- Gets mount attributes (uid, gid, mode, etc ..) from a remote NFS share - -- - -- @param file_handle string containing the filehandle to query - -- @return status true on success, false on failure - -- @return attribs table with the fields type, mode, - -- nlink, uid, gid, size, - -- blocksize, rdev, blocks, fsid, - -- fileid, atime, mtime and ctime - -- @return errormsg if status is false - GetAttr = function( self, file_handle ) - local data, packet, status, attribs, pos, header - - data = bin.pack("A", file_handle) - packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].GETATTR }, { type=RPC.AuthType.Null }, data ) - status = self.comm:SendPacket(packet) - if ( not(status) ) then - return false, "nfsGetAttribs: Failed to send data to socket" - end - - status, data = self.comm:ReceivePacket() - if ( not(status) ) then - return false, "nfsGetAttribs: Failed to read data from socket" - end - - pos, header = self.comm:DecodeHeader( data, 1 ) - - if not header then - return false, "Failed to decode header" - end - - pos, attribs = self:GetAttrDecode( data, pos ) - - if not attribs then - return false, "Failed to decode attrib structure" - end - - return true, attribs - end, - - --- Attempts to decode the StatFS section of the reply - -- - -- @param data string containing the full statfs reply - -- @param pos number pointing to the statfs section of the reply - -- @return pos number containing the offset after decoding - -- @return statfs table with the following fields: transfer_size, block_size, - -- total_blocks, free_blocks and available_blocks - -- - StatFsDecode = function( self, data, pos ) - local catch = function() self.socket:close() end - local try = nmap.new_try(catch) - local statfs = {} - local NFS_OK, NSFERR_ACCESS = 0, 13 - - data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) - pos, statfs.status = bin.unpack(">I", data, pos) - - if statfs.status ~= NFS_OK then - if statfs.status == NSFERR_ACCESS then - stdnse.print_debug("STATFS query received NSFERR_ACCESS") - end + elseif ( comm.version == 3 ) then + status, data = comm:GetAdditionalBytes( data, pos, 84 ) + if (not(status)) then + stdnse.print_debug("GetAttrDecode: Failed to call GetAdditionalBytes") return -1, nil end + pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, + attrib.gid, attrib.size, attrib.used, attrib.rdev, + attrib.fsid, attrib.fileid, attrib.atime, attrib.mtime, + attrib.ctime = bin.unpack(">IIIIILLLLLLLL", data, pos) - data = try( self.comm:GetAdditionalBytes( data, pos, 20 ) ) - pos, statfs.transfer_size, statfs.block_size, - statfs.total_blocks, statfs.free_blocks, - statfs.available_blocks = bin.unpack(">IIIII", data, pos ) - return pos, statfs - end, + else + stdnse.print_debug("GetAttrDecode: Unsupported version") + return -1, nil + end + + return pos, attrib + end, + + --- Gets mount attributes (uid, gid, mode, etc ..) from a remote NFS share + -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation + -- @param file_handle string containing the filehandle to query + -- @return status true on success, false on failure + -- @return attribs table with the fields type, mode, + -- nlink, uid, gid, size, + -- blocksize, rdev, blocks, fsid, + -- fileid, atime, mtime and ctime + -- @return errormsg if status is false + GetAttr = function( self, comm, file_handle ) + local data, packet, status, attribs, pos, header + + data = bin.pack("A", file_handle) + packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].GETATTR, { type=RPC.AuthType.Null }, data ) + if(not(comm:SendPacket(packet))) then + return false, "GetAttr: Failed to send data" + end + + status, data = comm:ReceivePacket() + if ( not(status) ) then + return false, "GetAttr: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, 1 ) + if not header then + return false, "GetAttr: Failed to decode header" + end + + pos, attribs = self:GetAttrDecode(comm, data, pos ) + if not attribs then + return false, "GetAttr: Failed to decode attrib structure" + end + + return true, attribs + end, + + --- Attempts to decode the StatFS section of the reply + -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation + -- @param data string containing the full statfs reply + -- @param pos number pointing to the statfs section of the reply + -- @return pos number containing the offset after decoding + -- @return statfs table with the following fields: transfer_size, block_size, + -- total_blocks, free_blocks and available_blocks + StatFsDecode = function( self, comm, data, pos ) + local status + local statfs = {} + + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if (not(status)) then + stdnse.print_debug("StatFsDecode: Failed to call GetAdditionalBytes") + return -1, nil + end + pos, statfs.status = bin.unpack(">I", data, pos) + + if (statfs.status ~= NFS.StatCode[comm.version].NFS_OK) then + if (NFS.StatMsg[statfs.status]) then + stdnse.print_debug(string.format("STATFS query failed: %s", NFS.StatMsg[statfs.status])) + else + stdnse.print_debug(string.format("STATFS query failed: code %d", statfs.status)) + end + return -1, nil + end + + status, data = comm:GetAdditionalBytes( data, pos, 20 ) + if (not(status)) then + stdnse.print_debug("StatFsDecode: Failed to call GetAdditionalBytes") + return -1, nil + end + pos, statfs.transfer_size, statfs.block_size, + statfs.total_blocks, statfs.free_blocks, + statfs.available_blocks = bin.unpack(">IIIII", data, pos ) + return pos, statfs + end, } Helper = { @@ -1046,32 +1282,29 @@ Helper = { -- @return status true on success, false on failure -- @return result table of string entries or error message on failure ShowMounts = function( host, port ) - - local data, prog_tbl = {}, {} - local status, result, mounts, response - local socket = nmap.new_socket() - local mountd - local ver - local mnt = Mount:new() + + local status, result, mounts + local mountd, mnt_comm + local mnt = Mount:new() local portmap = Portmap:new() - local portmap_table, proginfo status, mountd = Helper.GetProgramInfo( host, port, "mountd") - if ( not(status) ) then - return false, "Failed to retrieve rpc information for mountd" + stdnse.print_debug("rpc.Helper.ShowMounts: GetProgramInfo failed") + return status, "rpc.Helper.ShowMounts: GetProgramInfo failed" end - - status, result = mnt:Connect( host, mountd.port, mountd.version ) + + mnt_comm = Comm:new('mountd', mountd.version) + status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then - stdnse.print_debug(3, result) + stdnse.print_debug("rpc.Helper.ShowMounts: %s", result) return false, result end - - status, mounts = mnt:Export() - - mnt:Disconnect() - + status, mounts = mnt:Export(mnt_comm) + mnt_comm:Disconnect() + if ( not(status) ) then + stdnse.print_debug("rpc.Helper.ShowMounts: %s", mounts) + end return status, mounts end, @@ -1084,53 +1317,66 @@ Helper = { -- @return statfs table with the fields transfer_size, block_size, -- total_blocks, free_blocks and available_blocks ExportStats = function( host, port, path ) - local fhandle local stats, status, result + local mnt_comm, nfs_comm local mountd, nfsd = {}, {} local mnt, nfs = Mount:new(), NFS:new() - + status, mountd = Helper.GetProgramInfo( host, port, "mountd", 2) if ( not(status) ) then - return false, "Failed to retrieve rpc information for mountd" + stdnse.print_debug("rpc.Helper.ExportStats: %s", mountd) + return status, mountd end status, nfsd = Helper.GetProgramInfo( host, port, "nfs", 2) if ( not(status) ) then - return false, "Failed to retrieve rpc information for nfsd" + stdnse.print_debug("rpc.Helper.ExportStats: %s", nfsd) + return status, nfsd + end + mnt_comm = Comm:new('mountd', mountd.version) + nfs_comm = Comm:new('nfs', nfsd.version) + + -- TODO: recheck the version mismatch when adding NFSv4 + if (nfs_comm.version <= 2 and mnt_comm.version > 2) then + stdnse.print_debug("rpc.Helper.ExportStats: versions mismatch, nfs v%d - mount v%d", + nfs_comm.version, mnt_comm.version) + return false, string.format("versions mismatch, nfs v%d - mount v%d", nfs_comm.version, mnt_comm.version) + end + status, result = mnt_comm:Connect(host, mountd.port) + if ( not(status) ) then + stdnse.print_debug("rpc.Helper.ExportStats: %s", result) + return status, result + end + status, result = nfs_comm:Connect(host, nfsd.port) + if ( not(status) ) then + mnt_comm:Disconnect() + stdnse.print_debug("rpc.Helper.ExportStats: %s", result) + return status, result end - status, result = mnt:Connect( host, mountd.port, mountd.version ) + status, fhandle = mnt:Mount(mnt_comm, path) if ( not(status) ) then - return false, "Failed to connect to mountd program" + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.ExportStats: %s", fhandle) + return status, fhandle end - - status, result = nfs:Connect( host, nfsd.port, nfsd.version ) + status, stats = nfs:StatFs(nfs_comm, fhandle) if ( not(status) ) then - mnt:Disconnect() - return false, "Failed to connect to nfsd program" - end - - status, fhandle = mnt:Mount( path ) - if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - stdnse.print_debug("rpc.Helper.ExportStats: mount failed") - return false, "Mount failed" - end - - status, stats = nfs:StatFs( fhandle ) - if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - return false, stats + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.ExportStats: %s", stats) + return status, stats end - status, fhandle = mnt:Unmount( path ) - - mnt:Disconnect() - nfs:Disconnect() - + status, fhandle = mnt:Unmount(mnt_comm, path) + mnt_comm:Disconnect() + nfs_comm:Disconnect() + if ( not(status) ) then + stdnse.print_debug("rpc.Helper.ExportStats: %s", fhandle) + return status, fhandle + end return true, stats end, @@ -1142,58 +1388,71 @@ Helper = { -- @return status true on success, false on failure -- @return table of file table entries as described in decodeReadDir Dir = function( host, port, path ) - local fhandle local dirs, status, result local mountd, nfsd = {}, {} + local mnt_comm, nfs_comm local mnt, nfs = Mount:new(), NFS:new() status, mountd = Helper.GetProgramInfo( host, port, "mountd") if ( not(status) ) then - return false, "Failed to retrieve rpc information for mountd" + stdnse.print_debug("rpc.Helper.Dir: %s", mountd) + return status, mountd end status, nfsd = Helper.GetProgramInfo( host, port, "nfs") if ( not(status) ) then - return false, "Failed to retrieve rpc information for nfsd" + stdnse.print_debug("rpc.Helper.Dir: %s", nfsd) + return status, nfsd end - status, result = mnt:Connect( host, mountd.port, mountd.version ) + mnt_comm = Comm:new('mountd', mountd.version) + nfs_comm = Comm:new('nfs', nfsd.version) + + -- TODO: recheck the version mismatch when adding NFSv4 + if (nfs_comm.version <= 2 and mnt_comm.version > 2) then + stdnse.print_debug("rpc.Helper.Dir: versions mismatch, nfs v%d - mount v%d", + nfs_comm.version, mnt_comm.version) + return false, string.format("versions mismatch, nfs v%d - mount v%d", + nfs_comm.version, mnt_comm.version) + end + status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then - return false, "Failed to connect to mountd program" + stdnse.print_debug("rpc.Helper.Dir: %s", result) + return status, result end - status, result = nfs:Connect( host, nfsd.port, nfsd.version ) + status, result = nfs_comm:Connect(host, nfsd.port) if ( not(status) ) then - mnt:Disconnect() - return false, "Failed to connect to nfsd program" + mnt_comm:Disconnect() + stdnse.print_debug("rpc.Helper.Dir: %s", result) + return status, result end - status, fhandle = mnt:Mount( path ) + status, fhandle = mnt:Mount(mnt_comm, path ) if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - return false, "rpc.Helper.Dir: mount failed" + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.Dir: %s", fhandle) + return status, fhandle end - status, dirs = nfs:ReadDir( fhandle ) + status, dirs = nfs:ReadDir(nfs_comm, fhandle ) if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - return false, "rpc.Helper.Dir: statfs failed" + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.Dir: %s", dirs) + return status, dirs end - status, fhandle = mnt:Unmount( path ) - - mnt:Disconnect() - nfs:Disconnect() - + status, fhandle = mnt:Unmount(mnt_comm, path) + mnt_comm:Disconnect() + nfs_comm:Disconnect() if ( not(status) ) then - return false, "rpc.Helper.Dir: mount failed" + stdnse.print_debug("rpc.Helper.Dir: %s", fhandle) + return status, fhandle end - return true, dirs - end, --- Retrieves NFS Attributes @@ -1207,51 +1466,69 @@ Helper = { GetAttributes = function( host, port, path ) local fhandle local attribs, status, result + local mnt_comm, nfs_comm local mountd, nfsd = {}, {} local mnt, nfs = Mount:new(), NFS:new() status, mountd = Helper.GetProgramInfo( host, port, "mountd") if ( not(status) ) then - return false, "Failed to retrieve rpc information for mountd" + stdnse.print_debug("rpc.Helper.GetAttributes: %s", mountd) + return status, mountd end status, nfsd = Helper.GetProgramInfo( host, port, "nfs") if ( not(status) ) then - return false, "Failed to retrieve rpc information for nfsd" - end - - status, result = mnt:Connect( host, mountd.port, mountd.version ) - if ( not(status) ) then - return false, "Failed to connect to mountd program" - end - - status, result = nfs:Connect( host, nfsd.port, nfsd.version ) - if ( not(status) ) then - mnt:Disconnect() - return false, "Failed to connect to nfsd program" - end - - status, fhandle = mnt:Mount( path ) - if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - return false, "rpc.Helper.GetAttributes: mount failed" - end - - status, attribs = nfs:GetAttr( fhandle ) - if ( not(status) ) then - mnt:Disconnect() - nfs:Disconnect() - return false, "rpc.Helper.GetAttributes: GetAttr failed" + stdnse.print_debug("rpc.Helper.GetAttributes: %s", nfsd) + return status, nfsd end - status, fhandle = mnt:Unmount( path ) - - mnt:Disconnect() - nfs:Disconnect() - + mnt_comm, result = Comm:new('mountd', mountd.version) + nfs_comm, result = Comm:new('nfs', nfsd.version) + + -- TODO: recheck the version mismatch when adding NFSv4 + if (nfs_comm.version <= 2 and mnt_comm.version > 2) then + stdnse.print_debug("rpc.Helper.GetAttributes: versions mismatch, nfs v%d - mount v%d", + nfs_comm.version, mnt_comm.version) + return false, string.format("versions mismatch, nfs v%d - mount v%d", + nfs_comm.version, mnt_comm.version) + end + + status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then - return false, "rpc.Helper.ExportStats: mount failed" + stdnse.print_debug("rpc.Helper.GetAttributes: %s", result) + return status, result + end + + status, result = nfs_comm:Connect(host, nfsd.port) + if ( not(status) ) then + mnt_comm:Disconnect() + stdnse.print_debug("rpc.Helper.GetAttributes: %s", result) + return status, result + end + + status, fhandle = mnt:Mount(mnt_comm, path) + if ( not(status) ) then + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.GetAttributes: %s", fhandle) + return status, fhandle + end + + status, attribs = nfs:GetAttr(nfs_comm, fhandle) + if ( not(status) ) then + mnt_comm:Disconnect() + nfs_comm:Disconnect() + stdnse.print_debug("rpc.Helper.GetAttributes: %s", attribs) + return status, attribs + end + + status, fhandle = mnt:Unmount(mnt_comm, path) + + mnt_comm:Disconnect() + nfs_comm:Disconnect() + if ( not(status) ) then + stdnse.print_debug("rpc.Helper.GetAttributes: %s", fhandle) + return status, fhandle end return true, attribs @@ -1265,20 +1542,24 @@ Helper = { -- @return table containing the portmapper information as returned by -- Portmap.Dump RpcInfo = function( host, port ) + local status, result local portmap = Portmap:new() - local status = Portmap:Connect(host, port) - local result + local comm = Comm:new('rpcbind', 2) - if ( not(status) ) then - return + status, result = comm:Connect(host, port) + if (not(status)) then + stdnse.print_debug("rpc.Helper.RpcInfo: %s", result) + return status, result + end + status, result = portmap:Dump(comm) + comm:Disconnect() + if (not(status)) then + stdnse.print_debug("rpc.Helper.RpcInfo: %s", result) end - - status, result = portmap:Dump() - portmap:Disconnect() return status, result end, - + --- Queries the portmapper for a port for the specified RPC program -- -- @param host table @@ -1289,17 +1570,22 @@ Helper = { -- @return table containing the portmapper information as returned by -- Portmap.Dump GetPortForProgram = function( host, port, program_id, protocol ) + local status, result local portmap = Portmap:new() - local status = Portmap:Connect(host, port) - local result - - if ( not(status) ) then - return + local comm = Comm:new('rpcbind', 2) + + status, result = comm:Connect(host, port) + if (not(status)) then + stdnse.print_debug("rpc.Helper.GetPortForProgram: %s", result) + return status, result end - status, result = portmap:GetPort( program_id, protocol, 1 ) - portmap:Disconnect() - + status, result = portmap:GetPort(comm, program_id, protocol, 1 ) + comm:Disconnect() + if (not(status)) then + stdnse.print_debug("rpc.Helper.GetPortForProgram: %s", result) + end + return status, result end, @@ -1313,23 +1599,11 @@ Helper = { -- @return info table containing port, port.number -- port.protocol and version GetProgramInfo = function( host, port, program, max_version ) - - local status, response - local portmap_table, info - local portmap = Portmap:new() + local info - status, response = portmap:Connect( host, port ) + local status, portmap_table = Helper.RpcInfo(host, port) if ( not(status) ) then - return false, "rpc.Helper.ShowMounts: Failed to connect to portmap" - end - status, portmap_table = portmap:Dump() - if ( not(status) ) then - portmap:Disconnect() - return false, "rpc.Helper.ShowMounts: Failed to GetProgramVersions" - end - status = portmap:Disconnect() - if ( not(status) ) then - return false, "rpc.Helper.ShowMounts: Failed to disconnect from portmap" + return status, portmap_table end -- assume failure @@ -1344,12 +1618,12 @@ Helper = { info.port.number = tmp[p].port info.port.protocol = p -- choose the highest version available - if ( not(Version[program]) ) then + if ( not(RPC_version[program]) ) then info.version = tmp[p].version[#tmp[p].version] status = true else for i=#tmp[p].version, 1, -1 do - if ( Version[program].max >= tmp[p].version[i] ) then + if ( RPC_version[program].max >= tmp[p].version[i] ) then if ( not(max_version) ) then info.version = tmp[p].version[i] status = true @@ -1397,6 +1671,27 @@ RPC = }, + State = + { + Accepted = 0, + Denied = 1, + }, + + AcceptState = + { + SUCCESS = 0, + PROG_UNAVAIL = 1, + PROG_MISMATCH = 2, + PROC_UNAVAIL = 3, + GARBAGE_ARGS = 4, + }, + + RejectState = + { + RPC_MISMATCH = 0, + AUTH_ERROR = 1, + } + } --- Portmap class @@ -1412,42 +1707,12 @@ Portmap = setmetatable(o, self) self.__index = self return o - end, - - --- Connects to the Portmapper - -- - -- @param host table - -- @param port table - -- @param version number containing the program version to use - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Connect = function( self, host, port, version ) - local socket = nmap.new_socket() - local status, result = socket:connect(host.ip, port.number, port.protocol) - - if ( status ) then - self.socket = socket - self.version = version or 2 - self.protocol = port.protocol - end - - return status, result end, - - --- Disconnects from the portmapper program - -- - -- @return status boolean true on success, false on failure - -- @return result string containing error message (if status is false) - Disconnect = function( self ) - local status, result = self.socket:close() - if ( status ) then - self.socket = nil - end - return status, result - end, - + --- Dumps a list of RCP programs from the portmapper -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @return status boolean true on success, false on failure -- @return result table containing RPC program information or error message -- on failure. The table has the following format: @@ -1461,20 +1726,16 @@ Portmap = -- o program_id is the number associated with the program -- o protocol is either "tcp" or "udp" -- - Dump = function( self ) + Dump = function(self, comm) local status, data, packet, response, pos, header - - local prog_id = Util.ProgNameToNumber("rpcbind") - local prog_proc = RPC.Procedure[self.version].DUMP - local comm - if ( self.program_table ) then return true, self.program_table end - comm = Comm:new( { socket=self.socket, proto=self.protocol } ) - packet = comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=prog_proc }, { type=RPC.AuthType.Null }, data ) - status, response = comm:SendPacket( packet ) + packet = comm:EncodePacket( nil, RPC.Procedure[comm.version].DUMP, { type=RPC.AuthType.Null }, data ) + if (not(comm:SendPacket(packet))) then + return false, "Portmap.Dump: Failed to send data" + end status, data = comm:ReceivePacket() if ( not(status) ) then return false, "Portmap.Dump: Failed to read data from socket" @@ -1482,10 +1743,10 @@ Portmap = pos, header = comm:DecodeHeader( data, 1 ) if ( not(header) ) then - return false, "Failed to decode RPC header" + return false, "Portmap.Dump: Failed to decode RPC header" end if header.accept_state ~= 0 then - return false, string.format("RPC Accept State was not Successful") + return false, "Portmap.Dump: RPC Accept State was not Successful" end self.program_table = {} @@ -1494,15 +1755,16 @@ Portmap = local vfollows local program, version, protocol, port - status, data = comm:GetAdditionalBytes( data, pos, 4 ) - pos, vfollows = bin.unpack( ">I", data, pos ) - + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "Portmap.Dump: Failed to call GetAdditionalBytes" + end + pos, vfollows = bin.unpack( ">I", data, pos ) if ( vfollows == 0 ) then break end pos, program, version, protocol, port = bin.unpack(">IIII", data, pos) - if ( protocol == Portmap.PROTOCOLS.tcp ) then protocol = "tcp" elseif ( protocol == Portmap.PROTOCOLS.udp ) then @@ -1526,51 +1788,53 @@ Portmap = --- Queries the portmapper for the port of the selected program, -- protocol and version -- + -- @param Comm object handles rpc program information and + -- low-level packet manipulation -- @param program string name of the program -- @param protocol string containing either "tcp" or "udp" -- @param version number containing the version of the queried program -- @return number containing the port number - GetPort = function( self, program, protocol, version ) + GetPort = function( self, comm, program, protocol, version ) local status, data, response, header, pos, packet local xid - local prog_id = Util.ProgNameToNumber("rpcbind") -- RPC Portmap - local prog_proc = RPC.Procedure[self.version].GETPORT - local comm if ( not( Portmap.PROTOCOLS[protocol] ) ) then - return false, ("Protocol %s not supported"):format(protocol) + return false, ("Portmap.GetPort: Protocol %s not supported"):format(protocol) end - if ( Util.ProgNameToNumber( program ) == nil ) then - return false, ("Unknown program name: %s"):format(program) + if ( Util.ProgNameToNumber(program) == nil ) then + return false, ("Portmap.GetPort: Unknown program name: %s"):format(program) end - comm = Comm:new( { socket=self.socket, proto=self.protocol } ) data = bin.pack( ">I>I>I>I", Util.ProgNameToNumber(program), version, Portmap.PROTOCOLS[protocol], 0 ) - packet = comm:EncodePacket( xid, { id=prog_id, version=self.version, proc=prog_proc }, { type=RPC.AuthType.Null }, data ) + packet = comm:EncodePacket( xid, RPC.Procedure[comm.version].GETPORT, { type=RPC.AuthType.Null }, data ) - status = comm:SendPacket(packet) + if (not(comm:SendPacket(packet))) then + return false, "Portmap.GetPort: Failed to send data" + end + data = "" - status, data = comm:ReceivePacket() if ( not(status) ) then - return false, "GetPort: Failed to read data from socket" + return false, "Portmap.GetPort: Failed to read data from socket" end pos, header = comm:DecodeHeader( data, 1 ) if ( not(header) ) then - return false, "Failed to decode RPC header" + return false, "Portmap.GetPort: Failed to decode RPC header" end if header.accept_state ~= 0 then - return false, string.format("RPC Accept State was not Successful") + return false, "Portmap.GetPort: RPC Accept State was not Successful" end - status, data = comm:GetAdditionalBytes( data, pos, 4 ) - return true, select(2, bin.unpack(">I", data, pos ) ) - + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "Portmap.GetPort: Failed to call GetAdditionalBytes" + end + return true, select(2, bin.unpack(">I", data, pos ) ) end, - + } --- Static class containing mostly conversion functions diff --git a/scripts/nfs-acls.nse b/scripts/nfs-acls.nse index be8cd1938..486544bc5 100644 --- a/scripts/nfs-acls.nse +++ b/scripts/nfs-acls.nse @@ -42,7 +42,7 @@ action = function(host, port) status, mounts = rpc.Helper.ShowMounts( host, port ) if ( not(status) or mounts == nil ) then - return " \n\n Failed to list mount points" + return stdnse.format_output(false, mounts) end for _, mount in ipairs( mounts ) do @@ -54,7 +54,7 @@ action = function(host, port) if ( status ) then table.insert(item, ("uid: %d; gid: %d; mode: %s (%d)"):format(attribs.uid, attribs.gid, rpc.Util.ToAclText( attribs.mode ), rpc.Util.ToAclMode( attribs.mode )) ) else - table.insert(item, "ERROR: Mount failed") + table.insert(item, string.format("ERROR: %s", attribs)) end table.insert(result, item) diff --git a/scripts/nfs-dirlist.nse b/scripts/nfs-dirlist.nse index 6575c1409..ee5577537 100644 --- a/scripts/nfs-dirlist.nse +++ b/scripts/nfs-dirlist.nse @@ -47,7 +47,7 @@ action = function(host, port) status, mounts = rpc.Helper.ShowMounts( host, port ) if ( not(status) ) then - return " \n\n Failed to list mount points" + return stdnse.format_output(false, mounts) end for _, v in ipairs( mounts ) do @@ -72,7 +72,7 @@ action = function(host, port) table.sort(files) if hasmore then - files.name = v.name .. string.format(" (Output limited to %d files)", max_files ) + files.name = v.name .. string.format(" (Output limited to %d files, see nfs-dirlist.maxfiles)", max_files ) else files.name = v.name end @@ -80,7 +80,7 @@ action = function(host, port) table.insert( result, files ) else files.name = v.name - table.insert(files, "ERROR: Mount failed") + table.insert(files, string.format("ERROR: %s",dirlist)) table.insert( result, files ) end diff --git a/scripts/nfs-showmount.nse b/scripts/nfs-showmount.nse index 666c8ede7..8f1956ef0 100644 --- a/scripts/nfs-showmount.nse +++ b/scripts/nfs-showmount.nse @@ -39,7 +39,7 @@ action = function(host, port) status, mounts = rpc.Helper.ShowMounts( host, port ) if not status or mounts == nil then - return " \n\n Failed to list mount points" + return stdnse.format_output(false, mounts) end for _, v in ipairs( mounts ) do diff --git a/scripts/nfs-statfs.nse b/scripts/nfs-statfs.nse index 1ac03520c..73bd9d247 100644 --- a/scripts/nfs-statfs.nse +++ b/scripts/nfs-statfs.nse @@ -40,26 +40,22 @@ action = function(host, port) local status, mounts = rpc.Helper.ShowMounts( host, port ) if ( not(status) ) then - return " \n\n Failed to list mount points" + return stdnse.format_output(false, mounts) end for _, v in ipairs( mounts ) do local entry = {} local status, stats = rpc.Helper.ExportStats(host, port, v.name) - if ( not(status) and stats:match("Version %d not supported") ) then - return " \n\n " .. stats - end - entry.name = v.name - - if status and stats then + + if (not(status)) then + table.insert(entry, string.format("ERROR: %s", stats)) + else table.insert( entry, string.format("Block size: %d", stats.block_size) ) table.insert( entry, string.format("Total blocks: %d", stats.total_blocks) ) table.insert( entry, string.format("Free blocks: %d", stats.free_blocks) ) table.insert( entry, string.format("Available blocks: %d", stats.available_blocks) ) - else - table.insert( entry, "ERROR: Mount failed") end table.insert( result, entry ) end diff --git a/scripts/rpcinfo.nse b/scripts/rpcinfo.nse index 3441be042..335802e12 100644 --- a/scripts/rpcinfo.nse +++ b/scripts/rpcinfo.nse @@ -31,7 +31,7 @@ action = function(host, port) local status, rpcinfo = rpc.Helper.RpcInfo( host, port ) if ( not(status) ) then - return + return stdnse.format_output(false, rpcinfo) end for progid, v in pairs(rpcinfo) do