--- -- RPC Library supporting a very limited subset of operations. -- -- The library works over both the UDP and TCP protocols. A subset of nfs and -- mountd procedures are supported. The nfs and mountd programs support -- versions 1 through 3. Authentication is supported using the NULL RPC -- Authentication protocol -- -- The library contains the following classes: -- * 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. -- * Portmap -- ** Contains RPC constants. -- ** Handles communication with the portmap RPC program. -- * Mount -- ** Handles communication with the mount RPC program. -- * NFS -- ** Handles communication with the nfs RPC program. -- * Helper -- ** Provides easy access to common RPC functions. -- ** Implemented as a static class where most functions accept host and port parameters. -- * Util -- ** Mostly static conversion routines. -- -- The portmapper dynamically allocates TCP/UDP ports to RPC programs. So in -- in order to request a list of NFS shares from the server we need to: -- * Make sure that we can talk to the portmapper on port 111 TCP or UDP. -- * Query the portmapper for the ports allocated to the NFS program. -- * Query the NFS program for a list of shares on the ports returned by the portmap program. -- -- The Helper class contains functions that facilitate access to common -- RPC program procedures through static class methods. Most functions accept -- host and port parameters. As the Helper functions query the portmapper to -- get the correct RPC program port, the port supplied to these functions -- should be the rpcbind port 111/tcp or 111/udp. -- -- The following sample code illustrates how scripts can use the Helper class -- to interface the library: -- -- -- -- retrieve a list of NFS export -- status, mounts = rpc.Helper.ShowMounts( host, port ) -- -- -- iterate over every share -- for _, mount in ipairs( mounts ) do -- -- -- get the NFS attributes for the share -- status, attribs = rpc.Helper.GetAttributes( host, port, mount.name ) -- .... process NFS attributes here .... -- end -- -- -- RPC transaction IDs (XID) are not properly implemented as a random ID is -- generated for each client call. The library makes no attempt to verify -- whether the returned XID is valid or not. -- -- Therefore TCP is the preferred method of communication and the library -- always attempts to connect to the TCP port of the RPC program first. -- This behaviour can be overridden by setting the rpc.protocol argument. -- The portmap service is always queried over the protocol specified in the -- port information used to call the Helper function from the script. -- -- When multiple versions exists for a specific RPC program the library -- always attempts to connect using the highest available version. -- -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html -- -- @author "Patrik Karlsson " -- -- @args nfs.version number If set overrides the detected version of nfs -- @args mount.version number If set overrides the detected version of mountd -- @args rpc.protocol table If set overrides the preferred order in which -- protocols are tested. (ie. "tcp", "udp") local bin = require "bin" local bit = require "bit" local datafiles = require "datafiles" local math = require "math" local nmap = require "nmap" local os = require "os" local stdnse = require "stdnse" local string = require "string" local table = require "table" _ENV = stdnse.module("rpc", stdnse.seeall) -- Version 0.3 -- -- Created 01/24/2010 - v0.1 - created by Patrik Karlsson -- 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 -- Revised 06/02/2010 - v0.5 - added code to the Util class to check for file -- types and permissions. -- Revised 06/04/2010 - v0.6 - combined Portmap and RPC classes in the -- same Portmap class. -- -- 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_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 -- local mutex to synchronize I/O operations on nmap.registry[host.ip]['portmapper'] local mutex = nmap.mutex("rpc") -- Supported protocol versions RPC_version = { ["rpcbind"] = { min=2, max=2 }, ["nfs"] = { min=1, max=3 }, ["mountd"] = { min=1, max=3 }, } -- Low-level communication class Comm = { --- Creates 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.checkprogver = true o:SetVersion(version) return o 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 if ( port.protocol == "tcp" ) then if nmap.is_privileged() then -- Try to bind to a reserved port for i = 1, 10, 1 do local resvport = math.random(1, 1024) socket = nmap.new_socket() status, err = socket:bind(nil, resvport) if status then status, err = socket:connect(host, port) if status or err == "TIMEOUT" then break end socket:close() end end else socket = nmap.new_socket() status, err = socket:connect(host, port) end else if nmap.is_privileged() then -- Try to bind to a reserved port for i = 1, 10, 1 do local resvport = math.random(1, 1024) socket = nmap.new_socket("udp") status, err = socket:bind(nil, resvport) if status then status, err = socket:connect(host, port) if status or err == "TIMEOUT" then break end socket:close() end end else socket = nmap.new_socket("udp") status, err = socket:connect(host, port) end end if (not(status)) then return status, string.format("%s connect error: %s", self.program, err) else self.socket = socket self.host = host 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 not self.checkprogver then return true end 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 self.checkprogver then 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 else self.version = version end return true, nil end, --- Sets the verification of the specified program and version support -- before trying to connecting. -- @param check boolean to enable or disable checking of program and version support. SetCheckProgVer = function(self, check) self.checkprogver = check end, --- Sets the RPC program ID to use. -- @param progid number Program ID to set. SetProgID = function(self, progid) self.program_id = progid 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 -- -- @param data string containing the current buffer -- @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 or error message on failure GetAdditionalBytes = function( self, data, pos, needed ) local status, tmp if data:len() - pos + 1 < needed then local toread = needed - ( data:len() - pos + 1 ) status, tmp = self.socket:receive_bytes( toread ) if status then data = data .. tmp else return false, string.format("getAdditionalBytes() failed to read: %d bytes from the socket", needed - ( data:len() - pos ) ) end end return true, data end, --- Creates a RPC header -- -- @param xid number. If no xid was provided, a random one will be used. -- @param procedure number containing the procedure to call. Defaults to 0. -- @param auth table containing the authentication data to use. Defaults to NULL authentication. -- @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 -- Defaulting to NULL Authentication local auth = auth or {type = Portmap.AuthType.NULL} local xid = xid or math.random(1234567890) local procedure = procedure or 0 packet = bin.pack( ">IIIIII", xid, Portmap.MessageType.CALL, RPC_VERSION, self.program_id, self.version, procedure ) if auth.type == Portmap.AuthType.NULL then packet = packet .. bin.pack( "IIII", 0, 0, 0, 0 ) elseif auth.type == Portmap.AuthType.UNIX then packet = packet .. Util.marshall_int32(auth.type) local blob = Util.marshall_int32(nmap.clock()) --time blob = blob .. Util.marshall_vopaque(auth.hostname or 'localhost') blob = blob .. Util.marshall_int32(auth.uid or 0) blob = blob .. Util.marshall_int32(auth.gid or 0) if auth.gids then --len prefix gid list blob = blob .. Util.marshall_int32(#auth.gids) for _,gid in ipairs(auth.gids) do blob = blob .. Util.marshall_int32(gid) end else blob = blob .. Util.marshall_int32(0) end packet = packet .. Util.marshall_vopaque(blob) packet = packet .. bin.pack( "II", 0, 0 ) --AUTH_NULL verf else return false, "Comm.CreateHeader: invalid authentication type specified" end return true, packet end, --- Decodes the RPC header (without the leading 4 bytes as received over TCP) -- -- @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 -- @return header table containing xid, type, state, -- verifier and ( accept_state or denied_state ) DecodeHeader = function( self, data, pos ) local header = {} local status local HEADER_LEN = 20 header.verifier = {} if ( data:len() - pos < HEADER_LEN ) then local tmp status, tmp = self:GetAdditionalBytes( data, pos, HEADER_LEN - ( data:len() - pos ) ) if not status then stdnse.print_debug(4, string.format("Comm.DecodeHeader: 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 == Portmap.State.MSG_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(4, string.format("Comm.DecodeHeader: 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 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 -- to do appropriate checks return self.socket:receive_bytes(1) else local tmp, lastfragment, length local data, pos = "", 1 -- Maximum number of allowed attempts to parse the received bytes. This -- prevents the code from looping endlessly on invalid content. local retries = 400 repeat retries = retries - 1 lastfragment = false status, data = self:GetAdditionalBytes( data, pos, 4 ) if ( not(status) ) then return false, "Comm.ReceivePacket: failed to call GetAdditionalBytes" end pos, tmp = bin.unpack(">i", data, pos ) length = bit.band( tmp, 0x7FFFFFFF ) if ( bit.band( tmp, 0x80000000 ) == 0x80000000 ) then lastfragment = true end status, data = self:GetAdditionalBytes( data, pos, length ) if ( not(status) ) then return false, "Comm.ReceivePacket: failed to call GetAdditionalBytes" end -- -- When multiple packets are received they look like this -- H = Header data -- D = Data -- -- We don't want the Header -- -- HHHHDDDDDDDDDDDDDDHHHHDDDDDDDDDDD -- ^ ^ ^ ^ -- 1 5 18 22 -- -- eg. we want -- data:sub(5, 18) and data:sub(22) -- local bufcopy = data:sub(pos) if 1 ~= pos - 4 then bufcopy = data:sub(1, pos - 5) .. bufcopy pos = pos - 4 else pos = 1 end pos = pos + length data = bufcopy until (lastfragment == true) or (retries == 0) if retries == 0 then return false, "Aborted after too many retries" end return true, data end end, --- Encodes a RPC packet -- -- @param xid number containing the transaction ID -- @param proc 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, 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 -- set the high bit as this is our last fragment len = 0x80000000 + packet:len() return bin.pack(">I", len) .. packet end end, SendPacket = function( self, packet ) if ( self.host and self.port ) then return self.socket:sendto(self.host, self.port, packet) else return self.socket:send( packet ) end end, GetSocketInfo = function(self) return self.socket:get_info() end, } --- Portmap (rpcbind) class Portmap = { PROTOCOLS = { ['tcp'] = 6, ['udp'] = 17, }, -- TODO: add more Authentication Protocols AuthType = { NULL = 0, UNIX = 1, }, -- TODO: complete Authentication stats and error messages AuthState = { AUTH_OK = 0, AUTH_BADCRED = 1, AUTH_REJECTEDCRED = 2, AUTH_BADVERF = 3, AUTH_REJECTEDVERF = 4, AUTH_TOOWEAK = 5, AUTH_INVALIDRESP = 6, AUTH_FAILED = 7, }, AuthMsg = { [0] = "Success.", [1] = "bad credential (seal broken).", [2] = "client must begin new session.", [3] = "bad verifier (seal broken).", [4] = "verifier expired or replayed.", [5] = "rejected for security reasons.", [6] = "bogus response verifier.", [7] = "reason unknown.", }, MessageType = { CALL = 0, REPLY = 1 }, Procedure = { [2] = { GETPORT = 3, DUMP = 4, CALLIT = 5, }, }, State = { MSG_ACCEPTED = 0, MSG_DENIED = 1, }, AcceptState = { SUCCESS = 0, PROG_UNAVAIL = 1, PROG_MISMATCH = 2, PROC_UNAVAIL = 3, GARBAGE_ARGS = 4, SYSTEM_ERR = 5, }, AcceptMsg = { [0] = "RPC executed successfully.", [1] = "remote hasn't exported program.", [2] = "remote can't support version.", [3] = "program can't support procedure.", [4] = "procedure can't decode params.", [5] = "errors like memory allocation failure.", }, RejectState = { RPC_MISMATCH = 0, AUTH_ERROR = 1, }, RejectMsg = { [0] = "RPC version number != 2.", [1] = "remote can't authenticate caller.", }, new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o 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: -- -- -- table[program_id][protocol]["port"] = -- table[program_id][protocol]["version"] = -- -- -- Where -- o program_id is the number associated with the program -- o protocol is either "tcp" or "udp" -- Dump = function(self, comm) local status, data, packet, response, pos, header local program_table = setmetatable({}, { __mode = 'v' }) packet = comm:EncodePacket( nil, Portmap.Procedure[comm.version].DUMP, { type=Portmap.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" end pos, header = comm:DecodeHeader( data, 1 ) if ( not(header) ) then return false, "Portmap.Dump: Failed to decode RPC header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Portmap.Dump: Packet was not a reply" end if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Portmap.Dump: RPC call failed: %s", Portmap.RejectMsg[header.denied_state]) else return false, string.format("Portmap.Dump: RPC call failed: code %d", header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Portmap.Dump: RPC accepted state: %s", Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Portmap.Dump: RPC accepted state code %d", header.accept_state) end end while true do local vfollows local program, version, protocol, port 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 protocol = "udp" end program_table[program] = program_table[program] or {} program_table[program][protocol] = program_table[program][protocol] or {} program_table[program][protocol]["port"] = port program_table[program][protocol]["version"] = program_table[program][protocol]["version"] or {} table.insert( program_table[program][protocol]["version"], version ) -- parts of the code rely on versions being in order -- this way the highest version can be chosen by choosing the last element table.sort( program_table[program][protocol]["version"] ) end nmap.registry[comm.ip]['portmapper'] = program_table return true, nmap.registry[comm.ip]['portmapper'] end, --- Calls the portmap callit call and returns the raw response -- -- @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 status true on success, false on failure -- @return data string containing the raw response Callit = function( self, comm, program, protocol, version ) if ( not( Portmap.PROTOCOLS[protocol] ) ) then return false, ("Portmap.Callit: Protocol %s not supported"):format(protocol) end if ( Util.ProgNameToNumber(program) == nil ) then return false, ("Portmap.Callit: Unknown program name: %s"):format(program) end local data = bin.pack(">IIII", Util.ProgNameToNumber(program), version, 0, 0 ) local packet = comm:EncodePacket(nil, Portmap.Procedure[comm.version].CALLIT, { type=Portmap.AuthType.NULL }, data ) if (not(comm:SendPacket(packet))) then return false, "Portmap.Callit: Failed to send data" end data = "" local status, data = comm:ReceivePacket() if ( not(status) ) then return false, "Portmap.Callit: Failed to read data from socket" end local pos, header = comm:DecodeHeader( data, 1 ) if ( not(header) ) then return false, "Portmap.Callit: Failed to decode RPC header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Portmap.Callit: Packet was not a reply" end return true, data end, --- 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, comm, program, protocol, version ) local status, data, response, header, pos, packet local xid if ( not( Portmap.PROTOCOLS[protocol] ) ) then return false, ("Portmap.GetPort: Protocol %s not supported"):format(protocol) end if ( Util.ProgNameToNumber(program) == nil ) then return false, ("Portmap.GetPort: Unknown program name: %s"):format(program) end data = bin.pack(">I>I>I>I", Util.ProgNameToNumber(program), version, Portmap.PROTOCOLS[protocol], 0 ) packet = comm:EncodePacket(xid, Portmap.Procedure[comm.version].GETPORT, { type=Portmap.AuthType.NULL }, data ) 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, "Portmap.GetPort: Failed to read data from socket" end pos, header = comm:DecodeHeader( data, 1 ) if ( not(header) ) then return false, "Portmap.GetPort: Failed to decode RPC header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Portmap.GetPort: Packet was not a reply" end if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Portmap.GetPort: RPC call failed: %s", Portmap.RejectMsg[header.denied_state]) else return false, string.format("Portmap.GetPort: RPC call failed: code %d", header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Portmap.GetPort: RPC accepted state: %s", Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Portmap.GetPort: RPC accepted state code %d", header.accept_state) end end 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, } --- Mount class handling communication with the mountd program -- -- Currently supports versions 1 through 3 -- Can be called either directly or through the static Helper class -- 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, DUMP = 2, UMNT = 3, UMNTALL = 4, EXPORT = 5, }, new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o 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, comm) local msg_type = 0 local packet local pos = 1 local header = {} local entries = {} local data = "" local status 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=Portmap.AuthType.UNIX }, nil ) if (not(comm:SendPacket( packet ))) then return false, "Mount.Export: Failed to send data" end status, data = comm:ReceivePacket() if ( not(status) ) then return false, "Mount.Export: Failed to read data from socket" end -- make sure we have at least 24 bytes to unpack the header 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, "Mount.Export: Failed to decode header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Mount.Export: packet was not a reply" end if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Mount.Export: RPC call failed: %s", Portmap.RejectMsg[header.denied_state]) else return false, string.format("Mount.Export: RPC call failed: code %d", header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Mount.Export: RPC accepted state: %s", Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Mount.Export: RPC accepted state code %d", header.accept_state) end end --- -- Decode directory entries -- -- [entry] -- 4 bytes - value follows (1 if more data, 0 if not) -- [Directory] -- 4 bytes - value len -- len bytes - directory name -- ? bytes - fill bytes (@see calcFillByte) -- [Groups] -- 4 bytes - value follows (1 if more data, 0 if not) -- [Group] (1 or more) -- 4 bytes - group len -- len bytes - group value -- ? bytes - fill bytes (@see calcFillByte) --- while true do -- make sure we have atleast 4 more bytes to check for value follows 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 = Util.unmarshall_uint32(data, pos) if data_follows ~= 1 then break end --- Export list entry starts here local entry = {} local len -- make sure we have atleast 4 more bytes to get the length status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then return false, "Mount.Export: Failed to call GetAdditionalBytes" end pos, len = Util.unmarshall_uint32(data, pos) status, data = comm:GetAdditionalBytes( data, pos, len ) if (not(status)) then return false, "Mount.Export: Failed to call GetAdditionalBytes" end pos, entry.name = Util.unmarshall_vopaque(len, data, pos) -- decode groups while true do local group status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then return false, "Mount.Export: Failed to call GetAdditionalBytes" end pos, data_follows = Util.unmarshall_uint32(data, pos) if data_follows ~= 1 then break end status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then return false, "Mount.Export: Failed to call GetAdditionalBytes" end pos, len = Util.unmarshall_uint32(data, pos) status, data = comm:GetAdditionalBytes( data, pos, len ) if (not(status)) then return false, "Mount.Export: Failed to call GetAdditionalBytes" end pos, group = Util.unmarshall_vopaque(len, data, pos) table.insert( entry, group ) end table.insert(entries, entry) end 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, comm, path) local packet, mount_status local _, pos, data, header, fhandle = "", 1, "", "", {} local status, len data = Util.marshall_vopaque(path) packet = comm:EncodePacket( nil, Mount.Procedure.MOUNT, { type=Portmap.AuthType.UNIX }, data ) if (not(comm:SendPacket(packet))) then return false, "Mount: Failed to send data" end 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, "Mount: Failed to decode header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Mount: Packet was not a reply" end if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Mount: RPC call failed: %s", Portmap.RejectMsg[header.denied_state]) else return false, string.format("Mount: RPC call failed: code %d", header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Mount (%s): RPC accepted state: %s", path, Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Mount (%s): RPC accepted state code %d", path, header.accept_state) end end status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then return false, "Mount: Failed to call GetAdditionalBytes" end pos, mount_status = Util.unmarshall_uint32(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 ) 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 ( 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" end return true, fhandle end, --- 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, comm, path) local packet, status local _, pos, data, header, fhandle = "", 1, "", "", {} data = Util.marshall_vopaque(path) packet = comm:EncodePacket( nil, Mount.Procedure.UMNT, { type=Portmap.AuthType.UNIX }, data ) if (not(comm:SendPacket(packet))) then return false, "Unmount: Failed to send data" end 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, "Unmount: Failed to decode header" end if header.type ~= Portmap.MessageType.REPLY then return false, "Unmount: Packet was not a reply" end if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Unmount: RPC call failed: %s", Portmap.RejectMsg[header.denied_state]) else return false, string.format("Unmount: RPC call failed: code %d", header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Unmount (%s): RPC accepted state: %s", path, Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Unmount (%s): RPC accepted state code %d", path, header.accept_state) end end return true, "" end, } --- NFS class handling communication with the nfsd program -- -- Currently supports versions 1 through 3 -- Can be called either directly or through the static Helper class -- 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 = { -- NFS Version 1 [1] = { GETATTR = 1, ROOT = 3, LOOKUP = 4, EXPORT = 5, READDIR = 16, STATFS = 17, }, -- NFS Version 2 [2] = { GETATTR = 1, ROOT = 3, LOOKUP = 4, EXPORT = 5, READDIR = 16, STATFS = 17, }, -- NFS Version 3 [3] = { GETATTR = 1, SETATTR = 2, LOOKUP = 3, ACCESS = 4, EXPORT = 5, READDIR = 16, READDIRPLUS = 17, FSSTAT = 18, FSINFO = 19, PATHCONF = 20, COMMIT = 21, }, }, -- ACCESS values used to check the bit mask. AccessBits = { [3] = { ACCESS_READ = 0x0001, ACCESS_LOOKUP = 0x0002, ACCESS_MODIFY = 0x0004, ACCESS_EXTEND = 0x0008, ACCESS_DELETE = 0x0010, ACCESS_EXECUTE = 0x0020, }, }, FSinfoBits = { [3] = { FSF_LINK = 0x0001, FSF_SYMLINK = 0x0002, FSF_HOMOGENEOUS = 0x0008, FSF_CANSETTIME = 0x0010, }, }, new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, CheckStat = function (self, procedurename, version, status) if (status ~= NFS.StatCode[version].NFS_OK) then if (NFS.StatMsg[status]) then stdnse.print_debug(4, string.format("%s failed: %s", procedurename, NFS.StatMsg[status])) else stdnse.print_debug(4, string.format("%s failed: code %d", procedurename, status)) end return false end return true end, AccessRead = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_READ) end, AccessLookup = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_LOOKUP) end, AccessModify = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_MODIFY) end, AccessExtend = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_EXTEND) end, AccessDelete = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_DELETE) end, AccessExecute = function (self, mask, version) return bit.band(mask, NFS.AccessBits[version].ACCESS_EXECUTE) end, FSinfoLink = function(self, mask, version) return bit.band(mask, NFS.FSinfoBits[version].FSF_LINK) end, FSinfoSymlink = function(self, mask, version) return bit.band(mask, NFS.FSinfoBits[version].FSF_SYMLINK) end, FSinfoHomogeneous = function(self, mask, version) return bit.band(mask, NFS.FSinfoBits[version].FSF_HOMOGENEOUS) end, FSinfoCansettime = function(self, mask, version) return bit.band(mask, NFS.FSinfoBits[version].FSF_CANSETTIME) end, --- 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 -- @return entries table containing two table entries attributes -- and entries. The attributes entry is only present when -- using NFS version 3. The entries field contain one -- table for each file/directory entry. It has the following fields -- file_id, name and cookie -- ReadDirDecode = function( self, comm, data, pos ) local response = {} local value_follows local status, _ status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("READDIR", comm.version, status)) then return -1, nil end if ( 3 == comm.version ) then local attrib = {} response.attributes = {} status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, value_follows = Util.unmarshall_uint32(data, pos) if value_follows == 0 then return -1, nil end status, data = comm:GetAdditionalBytes( data, pos, 84 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, attrib = Util.unmarshall_nfsattr(data, pos, comm.version) table.insert(response.attributes, attrib) -- opaque data status, data = comm:GetAdditionalBytes( data, pos, 8 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, _ = bin.unpack(">L", data, pos) end response.entries = {} while true do local entry = {} status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, value_follows = Util.unmarshall_uint32(data, pos) if ( value_follows == 0 ) then break end if ( 3 == comm.version ) then status, data = comm:GetAdditionalBytes( data, pos, 8 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.fileid = Util.unmarshall_uint64(data, pos ) else status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.fileid = Util.unmarshall_uint32(data, pos) end status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.length = Util.unmarshall_uint32(data, pos) status, data = comm:GetAdditionalBytes( data, pos, entry.length ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.name = Util.unmarshall_vopaque(entry.length, data, pos) if ( 3 == comm.version ) then status, data = comm:GetAdditionalBytes( data, pos, 8 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.cookie = Util.unmarshall_uint64(data, pos) else status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "NFS.ReadDirDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.cookie = Util.unmarshall_uint32(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, comm, file_handle ) local status, packet local cookie, count = 0, 8192 local pos, data, _ = 1, "", "" local header, response = {}, {} if ( not(file_handle) ) then return false, "ReadDir: No filehandle received" end 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 = comm:EncodePacket( nil, NFS.Procedure[comm.version].READDIR, { type=Portmap.AuthType.UNIX }, data ) if(not(comm:SendPacket( packet ))) then return false, "ReadDir: Failed to send data" end 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 return true, response end, LookUpDecode = function(self, comm, data, pos) local lookup, status, len, value_follows, _ = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("LOOKUP", comm.version, status)) then return -1, nil end if (comm.version == 3) then status, data = comm:GetAdditionalBytes( data, pos, 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end _, len = Util.unmarshall_uint32(data, pos) status, data = comm:GetAdditionalBytes( data, pos, len + 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, lookup.fhandle = bin.unpack( "A" .. len + 4, data, pos) status, data = comm:GetAdditionalBytes( data, pos, 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end lookup.attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, lookup.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") end status, data = comm:GetAdditionalBytes( data, pos, 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end lookup.dir_attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, lookup.dir_attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") end elseif (comm.version < 3) then status, data = comm:GetAdditionalBytes( data, pos, 32) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, lookup.fhandle = bin.unpack("A32", data, pos) status, data = comm:GetAdditionalBytes( data, pos, 64 ) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, lookup.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.pritn_debug("NFS.LookUpDecode: NFS unsupported version %d", comm.version) return -1, nil end return pos, lookup end, LookUp = function(self, comm, dir_handle, file) local status, packet local pos, data = 1, "" local header, response = {}, {} if (not(dir_handle)) then return false, "LookUp: No dirhandle received" end data = Util.marshall_opaque(dir_handle) .. Util.marshall_vopaque(file) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].LOOKUP, {type=Portmap.AuthType.UNIX}, data) if(not(comm:SendPacket(packet))) then return false, "LookUp: Failed to send data" end status, data = comm:ReceivePacket() if ( not(status) ) then return false, "LookUp: Failed to read data from socket" end pos, header = comm:DecodeHeader(data, pos) if not header then return false, "LookUp: Failed to decode header" end pos, response = self:LookUpDecode(comm, data, pos) if (not(response)) then return false, "LookUp: Failed to decode the LOOKUP section" end return true, response end, ReadDirPlusDecode = function(self, comm, data, pos) local response, status, value_follows, _ = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("READDIRPLUS", comm.version, status)) then return -1, nil end status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, value_follows = bin.unpack(">I", data, pos) if value_follows == 0 then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Attributes follow failed") return -1, nil end status, data = comm:GetAdditionalBytes( data, pos, 84 ) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end response.attributes = {} pos, response.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) status, data = comm:GetAdditionalBytes(data, pos, 8) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, _ = bin.unpack(">L", data, pos) response.entries = {} while true do local entry, len = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, value_follows = bin.unpack(">I", data, pos) if (value_follows == 0) then break end status, data = comm:GetAdditionalBytes(data, pos, 8) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.fileid = bin.unpack(">L", data, pos) status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.length = bin.unpack(">I", data, pos) status, data = comm:GetAdditionalBytes( data, pos, entry.length ) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.name = Util.unmarshall_vopaque(entry.length, data, pos) status, data = comm:GetAdditionalBytes(data, pos, 8) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.cookie = bin.unpack(">L", data, pos) status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end entry.attributes = {} pos, value_follows = bin.unpack(">I", data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.ReadDirPlusDecode: %s Attributes follow failed", entry.name) end status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end entry.fhandle = "" pos, value_follows = bin.unpack(">I", data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end _, len = bin.unpack(">I", data, pos) status, data = comm:GetAdditionalBytes(data, pos, len + 4) if not status then stdnse.print_debug(4, "NFS.ReadDirPlusDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, entry.fhandle = bin.unpack( "A" .. len + 4, data, pos ) else stdnse.print_debug(4, "NFS.ReadDirPlusDecode: %s handle follow failed", entry.name) end table.insert(response.entries, entry) end return pos, response end, ReadDirPlus = function(self, comm, file_handle) local status, packet local cookie, opaque_data, dircount, maxcount = 0, 0, 512, 8192 local pos, data = 1, "" local header, response = {}, {} if (comm.version < 3) then return false, string.format("NFS version: %d does not support ReadDirPlus", comm.version) end if not file_handle then return false, "ReadDirPlus: No filehandle received" end data = bin.pack("A>L>L>I>I", file_handle, cookie, opaque_data, dircount, maxcount) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].READDIRPLUS, {type = Portmap.AuthType.UNIX }, data) if (not(comm:SendPacket(packet))) then return false, "ReadDirPlus: Failed to send data" end status, data = comm:ReceivePacket() if not status then return false, "ReadDirPlus: Failed to read data from socket" end pos, header = comm:DecodeHeader( data, pos ) if not header then return false, "ReadDirPlus: Failed to decode header" end pos, response = self:ReadDirPlusDecode( comm, data, pos ) if not response then return false, "ReadDirPlus: Failed to decode the READDIR section" end return true, response end, FsStatDecode = function(self, comm, data, pos) local fsstat, status, value_follows = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.FsStatDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("FSSTAT", comm.version, status)) then return -1, nil end fsstat.attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if not status then stdnse.print_debug(4, "NFS.FsStatDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, fsstat.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.FsStatDecode: Attributes follow failed") end status, data = comm:GetAdditionalBytes( data, pos, 52) if not status then stdnse.print_debug(4, "NFS.FsStatDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, fsstat.tbytes, fsstat.fbytes, fsstat.abytes, fsstat.tfiles, fsstat.ffiles, fsstat.afiles = Util.unmarshall_nfssize3(data, pos, 6) pos, fsstat.invarsec = Util.unmarshall_uint32(data, pos) return pos, fsstat end, FsStat = function(self, comm, file_handle) local status, packet local pos, data = 1, "" local header, response = {}, {} if (comm.version < 3) then return false, string.format("NFS version: %d does not support FSSTAT", comm.version) end if not file_handle then return false, "FsStat: No filehandle received" end data = bin.pack("A", file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].FSSTAT, {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "FsStat: Failed to send data" end status, data = comm:ReceivePacket() if not status then return false, "FsStat: Failed to read data from socket" end pos, header = comm:DecodeHeader(data, pos) if not header then return false, "FsStat: Failed to decode header" end pos, response = self:FsStatDecode(comm, data, pos) if not response then return false, "FsStat: Failed to decode the FSSTAT section" end return true, response end, FsInfoDecode = function(self, comm, data, pos) local fsinfo, status, value_follows = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.FsInfoDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("FSINFO", comm.version, status)) then return -1, nil end fsinfo.attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if not status then stdnse.print_debug(4, "NFS.FsInfoDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, fsinfo.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.FsInfoDecode: Attributes follow failed") end status, data = comm:GetAdditionalBytes(data, pos, 48) if not status then stdnse.print_debug(4, "NFS.FsStatDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, fsinfo.rtmax, fsinfo.rtpref, fsinfo.rtmult, fsinfo.wtmax, fsinfo.wtpref, fsinfo.wtmult, fsinfo.dtpref = Util.unmarshall_uint32(data, pos, 7) pos, fsinfo.maxfilesize = Util.unmarshall_nfssize3(data, pos) pos, fsinfo.time_delta = Util.unmarshall_nfstime(data, pos) pos, fsinfo.properties = Util.unmarshall_uint32(data, pos) return pos, fsinfo end, FsInfo = function(self, comm, file_handle) local status, packet local pos, data = 1, "" local header, response = {} if (comm.version < 3) then return false, string.format("NFS version: %d does not support FSINFO", comm.version) end if not file_handle then return false, "FsInfo: No filehandle received" end data = Util.marshall_opaque(file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].FSINFO, {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "FsInfo: Failed to send data" end status, data = comm:ReceivePacket() if not status then return false, "FsInfo: Failed to read data from socket" end pos, header = comm:DecodeHeader(data, pos) if not header then return false, "FsInfo: Failed to decode header" end pos, response = self:FsInfoDecode(comm, data, pos) if not response then return false, "FsInfo: Failed to decode the FSINFO section" end return true, response end, PathConfDecode = function(self, comm, data, pos) local pconf, status, value_follows = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.PathConfDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("PATHCONF", comm.version, status)) then return -1, nil end pconf.attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if not status then stdnse.print_debug(4, "NFS.PathConfDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, pconf.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.PathConfDecode: Attributes follow failed") end status, data = comm:GetAdditionalBytes(data, pos, 24) if not status then stdnse.print_debug(4, "NFS.PathConfDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, pconf.linkmax, pconf.name_max, pconf.no_trunc, pconf.chown_restricted, pconf.case_insensitive, pconf.case_preserving = Util.unmarshall_uint32(data, pos, 6) return pos, pconf end, PathConf = function(self, comm, file_handle) local status, packet local pos, data = 1, "" local header, response = {} if (comm.version < 3) then return false, string.format("NFS version: %d does not support PATHCONF", comm.version) end if not file_handle then return false, "PathConf: No filehandle received" end data = Util.marshall_opaque(file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].PATHCONF, {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "PathConf: Failed to send data" end status, data = comm:ReceivePacket() if not status then return false, "PathConf: Failed to read data from socket" end pos, header = comm:DecodeHeader(data, pos) if not header then return false, "PathConf: Failed to decode header" end pos, response = self:PathConfDecode(comm, data, pos) if not response then return false, "PathConf: Failed to decode the PATHCONF section" end return true, response end, AccessDecode = function(self, comm, data, pos) local access, status, value_follows = {} status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.AccessDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("ACCESS", comm.version, status)) then return -1, nil end access.attributes = {} pos, value_follows = Util.unmarshall_uint32(data, pos) if (value_follows ~= 0) then status, data = comm:GetAdditionalBytes(data, pos, 84) if not status then stdnse.print_debug(4, "NFS.AccessDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, access.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.AccessDecode: Attributes follow failed") end status, data = comm:GetAdditionalBytes(data, pos, 4) if not status then stdnse.print_debug(4, "NFS.AccessDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, access.mask = Util.unmarshall_uint32(data, pos) return pos, access end, Access = function(self, comm, file_handle, access) local status, packet local pos, data = 1, "" local header, response = {}, {} if (comm.version < 3) then return false, string.format("NFS version: %d does not support ACCESS", comm.version) end if not file_handle then return false, "Access: No filehandle received" end data = Util.marshall_opaque(file_handle) .. Util.marshall_uint32(access) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].ACCESS, {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "Access: Failed to send data" end status, data = comm:ReceivePacket() if not status then return false, "Access: Failed to read data from socket" end pos, header = comm:DecodeHeader(data, pos) if not header then return false, "Access: Failed to decode header" end pos, response = self:AccessDecode(comm, data, pos) if not response then return false, "Access: Failed to decode the FSSTAT section" end 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, comm, file_handle ) local status, packet local pos, data, _ = 1, "", "" local header, statfs = {}, {} 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, "StatFs: Incorrect filehandle received" end data = Util.marshall_opaque(file_handle) packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].STATFS, { type=Portmap.AuthType.UNIX }, data ) if (not(comm:SendPacket( packet ))) then return false, "StatFS: Failed to send data" end 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, "StatFs: Failed to decode header" end pos, statfs = self:StatFsDecode( comm, data, pos ) if not statfs then 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 -- @return statfs table with the following fields: type, mode, -- nlink, uid, gid, size, -- blocksize, rdev, blocks, fsid, -- fileid, atime, mtime and ctime -- GetAttrDecode = function( self, comm, data, pos ) local status status, data = comm:GetAdditionalBytes( data, pos, 4 ) if (not(status)) then stdnse.print_debug(4, "GetAttrDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("GETATTR", comm.version, status)) then return -1, nil end if ( comm.version < 3 ) then status, data = comm:GetAdditionalBytes( data, pos, 64 ) elseif (comm.version == 3) then status, data = comm:GetAdditionalBytes( data, pos, 84 ) else stdnse.print_debug(4, "GetAttrDecode: Unsupported version") return -1, nil end if ( not(status) ) then stdnse.print_debug(4, "GetAttrDecode: Failed to call GetAdditionalBytes") return -1, nil end return Util.unmarshall_nfsattr(data, pos, comm.version) 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 = Util.marshall_opaque(file_handle) packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].GETATTR, { type=Portmap.AuthType.UNIX }, 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(4, "StatFsDecode: Failed to call GetAdditionalBytes") return -1, nil end pos, status = Util.unmarshall_uint32(data, pos) if (not self:CheckStat("STATFS", comm.version, status)) then return -1, nil end status, data = comm:GetAdditionalBytes( data, pos, 20 ) if (not(status)) then stdnse.print_debug(4, "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 = Util.unmarshall_uint32(data, pos, 5) return pos, statfs end, } Helper = { --- Lists the NFS exports on the remote host -- This function abstracts the RPC communication with the portmapper from the user -- -- @param host table -- @param port table -- @return status true on success, false on failure -- @return result table of string entries or error message on failure ShowMounts = function( host, port ) local status, result, mounts local mountd, mnt_comm local mnt = Mount:new() local portmap = Portmap:new() status, mountd = Helper.GetProgramInfo( host, port, "mountd") if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.ShowMounts: GetProgramInfo failed") return status, "rpc.Helper.ShowMounts: GetProgramInfo failed" end mnt_comm = Comm:new('mountd', mountd.version) status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.ShowMounts: %s", result) return false, result end status, mounts = mnt:Export(mnt_comm) mnt_comm:Disconnect() if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.ShowMounts: %s", mounts) end return status, mounts end, --- Mounts a remote NFS export and returns the file handle -- -- This is a high level function to be used by NSE scripts -- To close the mounted NFS export use UnmountPath() function -- -- @param host table -- @param port table -- @param path string containing the path to mount -- @return on success a Comm object which can be -- used later as a parameter by low level Mount -- functions, on failure returns nil. -- @return on success the filehandle of the NFS export as -- a string, on failure returns the error message. MountPath = function(host, port, path) local fhandle, status, err local mountd, mnt_comm local mnt = Mount:new() status, mountd = Helper.GetProgramInfo( host, port, "mountd") if not status then stdnse.print_debug(4, "rpc.Helper.MountPath: GetProgramInfo failed") return nil, "rpc.Helper.MountPath: GetProgramInfo failed" end mnt_comm = Comm:new("mountd", mountd.version) status, err = mnt_comm:Connect(host, mountd.port) if not status then stdnse.print_debug(4, "rpc.Helper.MountPath: %s", err) return nil, err end status, fhandle = mnt:Mount(mnt_comm, path) if not status then mnt_comm:Disconnect() stdnse.print_debug(4, "rpc.Helper.MountPath: %s", fhandle) return nil, fhandle end return mnt_comm, fhandle end, --- Unmounts a remote mounted NFS export -- -- This is a high level function to be used by NSE scripts -- This function must be used to unmount a NFS point -- mounted by MountPath() -- -- @param mnt_comm object returned from a previous call to -- MountPath() -- @param path string containing the path to unmount -- @return true on success or nil on failure -- @return error message on failure UnmountPath = function(mnt_comm, path) local mnt = Mount:new() local status, ret = mnt:Unmount(mnt_comm, path) mnt_comm:Disconnect() if not status then stdnse.print_debug(4, "rpc.Helper.UnmountPath: %s", ret) return nil, ret end return status, nil end, --- Connects to a remote NFS server -- -- This is a high level function to open NFS connections -- To close the NFS connection use NfsClose() function -- -- @param host table -- @param port table -- @return on success a Comm object which can be -- used later as a parameter by low level NFS -- functions, on failure returns nil. -- @return error message on failure. NfsOpen = function(host, port) local nfs_comm, nfsd, status, err status, nfsd = Helper.GetProgramInfo(host, port, "nfs") if not status then stdnse.print_debug(4, "rpc.Helper.NfsOpen: GetProgramInfo failed") return nil, "rpc.Helper.NfsOpen: GetProgramInfo failed" end nfs_comm = Comm:new('nfs', nfsd.version) status, err = nfs_comm:Connect(host, nfsd.port) if not status then stdnse.print_debug(4, "rpc.Helper.NfsProc: %s", err) return nil, err end return nfs_comm, nil end, --- Closes the NFS connection -- -- This is a high level function to close NFS connections -- This function must be used to close the NFS connection -- opened by the NfsOpen() call -- -- @param nfs_comm object returned by NfsOpen() -- @return true on success or nil on failure -- @return error message on failure NfsClose = function(nfs_comm) local status, ret = nfs_comm:Disconnect() if not status then stdnse.print_debug(4, "rpc.Helper.NfsClose: %s", ret) return nil, ret end return status, nil end, --- Retrieves NFS storage statistics -- -- @param host table -- @param port table -- @param path string containing the nfs export path -- @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 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 stdnse.print_debug(4, "rpc.Helper.ExportStats: GetProgramInfo failed") return status, "rpc.Helper.ExportStats: GetProgramInfo failed" end status, nfsd = Helper.GetProgramInfo( host, port, "nfs", 2) if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.ExportStats: GetProgramInfo failed") return status, "rpc.Helper.ExportStats: GetProgramInfo failed" 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(4,"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(4, "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(4, "rpc.Helper.ExportStats: %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(4, "rpc.Helper.ExportStats: %s", fhandle) return status, fhandle end status, stats = nfs:StatFs(nfs_comm, fhandle) if ( not(status) ) then mnt_comm:Disconnect() nfs_comm:Disconnect() stdnse.print_debug(4, "rpc.Helper.ExportStats: %s", stats) return status, stats end status, fhandle = mnt:Unmount(mnt_comm, path) mnt_comm:Disconnect() nfs_comm:Disconnect() if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.ExportStats: %s", fhandle) return status, fhandle end return true, stats end, --- Retrieves a list of files from the NFS export -- -- @param host table -- @param port table -- @param path string containing the nfs export path -- @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 stdnse.print_debug(4, "rpc.Helper.Dir: GetProgramInfo failed") return status, "rpc.Helper.Dir: GetProgramInfo failed" end status, nfsd = Helper.GetProgramInfo( host, port, "nfs") if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.Dir: GetProgramInfo failed") return status, "rpc.Helper.Dir: GetProgramInfo failed" 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(4, "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 stdnse.print_debug(4, "rpc.Helper.Dir: %s", result) return status, result end status, result = nfs_comm:Connect(host, nfsd.port) if ( not(status) ) then mnt_comm:Disconnect() stdnse.print_debug(4, "rpc.Helper.Dir: %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(4, "rpc.Helper.Dir: %s", fhandle) return status, fhandle end status, dirs = nfs:ReadDir(nfs_comm, fhandle ) if ( not(status) ) then mnt_comm:Disconnect() nfs_comm:Disconnect() stdnse.print_debug(4, "rpc.Helper.Dir: %s", dirs) return status, dirs end status, fhandle = mnt:Unmount(mnt_comm, path) mnt_comm:Disconnect() nfs_comm:Disconnect() if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.Dir: %s", fhandle) return status, fhandle end return true, dirs end, --- Retrieves NFS Attributes -- -- @param host table -- @param port table -- @param path string containing the nfs export path -- @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 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 stdnse.print_debug(4, "rpc.Helper.GetAttributes: GetProgramInfo failed") return status, "rpc.Helper.GetAttributes: GetProgramInfo failed" end status, nfsd = Helper.GetProgramInfo( host, port, "nfs") if ( not(status) ) then stdnse.print_debug(4, "rpc.Helper.GetAttributes: GetProgramInfo failed") return status, "rpc.Helper.GetAttributes: GetProgramInfo failed" end 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(4, "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 stdnse.print_debug(4, "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(4, "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(4, "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(4, "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(4, "rpc.Helper.GetAttributes: %s", fhandle) return status, fhandle end return true, attribs end, --- Queries the portmapper for a list of programs -- -- @param host table -- @param port table -- @return status true on success, false on failure -- @return table containing the portmapper information as returned by -- Portmap.Dump RpcInfo = function( host, port ) local status, result local portmap = Portmap:new() local comm = Comm:new('rpcbind', 2) mutex "lock" if nmap.registry[host.ip] == nil then nmap.registry[host.ip] = {} end if nmap.registry[host.ip]['portmapper'] == nil then nmap.registry[host.ip]['portmapper'] = {} elseif next(nmap.registry[host.ip]['portmapper']) ~= nil then mutex "done" return true, nmap.registry[host.ip]['portmapper'] end status, result = comm:Connect(host, port) if (not(status)) then mutex "done" stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) return status, result end status, result = portmap:Dump(comm) comm:Disconnect() mutex "done" if (not(status)) then stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) end return status, result end, --- Queries the portmapper for a port for the specified RPC program -- -- @param host table -- @param port table -- @param program string containing the RPC program name -- @param protocol string containing either "tcp" or "udp" -- @return status true on success, false on failure -- @return table containing the portmapper information as returned by -- Portmap.Dump GetPortForProgram = function( host, port, program, protocol ) local status, result local portmap = Portmap:new() local comm = Comm:new('rpcbind', 2) status, result = comm:Connect(host, port) if (not(status)) then stdnse.print_debug(4, "rpc.Helper.GetPortForProgram: %s", result) return status, result end status, result = portmap:GetPort(comm, program, protocol, 1 ) comm:Disconnect() if (not(status)) then stdnse.print_debug(4, "rpc.Helper.GetPortForProgram: %s", result) end return status, result end, --- Get RPC program information -- -- @param host table -- @param port table -- @param program string containing the RPC program name -- @param max_version (optional) number containing highest version to retrieve -- @return status true on success, false on failure -- @return info table containing port, port.number -- port.protocol and version GetProgramInfo = function( host, port, program, max_version ) local status, portmap_table = Helper.RpcInfo(host, port) if ( not(status) ) then return status, portmap_table end local info = {} -- assume failure status = false for _, p in ipairs( RPC_PROTOCOLS ) do local tmp = portmap_table[Util.ProgNameToNumber(program)] if ( tmp and tmp[p] ) then info = {} info.port = {} info.port.number = tmp[p].port info.port.protocol = p -- choose the highest version available 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 ( RPC_version[program].max >= tmp[p].version[i] ) then if ( not(max_version) ) then info.version = tmp[p].version[i] status = true break else if ( max_version >= tmp[p].version[i] ) then info.version = tmp[p].version[i] status = true break end end end end end break end end return status, info end, } --- Static class containing mostly conversion functions -- and File type codes and permissions emulation Util = { -- Symbolic letters for file permission codes Fperm = { owner = { -- S_IRUSR [0x00000100] = { idx = 1, char = "r" }, -- S_IWUSR [0x00000080] = { idx = 2, char = "w" }, -- S_IXUSR [0x00000040] = { idx = 3, char = "x" }, -- S_ISUID [0x00000800] = { idx = 3, char = "S" }, }, group = { -- S_IRGRP [0x00000020] = { idx = 4, char = "r" }, -- S_IWGRP [0x00000010] = { idx = 5, char = "w" }, -- S_IXGRP [0x00000008] = { idx = 6, char = "x" }, -- S_ISGID [0x00000400] = { idx = 6, char = "S" }, }, other = { -- S_IROTH [0x00000004] = { idx = 7, char = "r" }, -- S_IWOTH [0x00000002] = { idx = 8, char = "w" }, -- S_IXOTH [0x00000001] = { idx = 9, char = "x" }, -- S_ISVTX [0x00000200] = { idx = 9, char = "t" }, }, }, -- bit mask used to extract the file type code from a mode -- S_IFMT = 00170000 (octal) S_IFMT = 0xF000, FileType = { -- S_IFSOCK [0x0000C000] = { char = "s", str = "socket" }, -- S_IFLNK [0x0000A000] = { char = "l", str = "symbolic link" }, -- S_IFREG [0x00008000] = { char = "-", str = "file" }, -- S_IFBLK [0x00006000] = { char = "b", str = "block device" }, -- S_IFDIR [0x00004000] = { char = "d", str = "directory" }, -- S_IFCHR [0x00002000] = { char = "c", str = "char device" }, -- S_IFIFO [0x00001000] = { char = "p", str = "named pipe" }, }, --- Converts a numeric ACL mode to a file type char -- -- @param mode number containing the ACL mode -- @return char containing the file type FtypeToChar = function(mode) local code = bit.band(mode, Util.S_IFMT) if Util.FileType[code] then return Util.FileType[code].char else stdnse.print_debug(1,"FtypeToChar: Unknown file type, mode: %o", mode) return "" end end, --- Converts a numeric ACL mode to a file type string -- -- @param mode number containing the ACL mode -- @return string containing the file type name FtypeToString = function(mode) local code = bit.band(mode, Util.S_IFMT) if Util.FileType[code] then return Util.FileType[code].str else stdnse.print_debug(1,"FtypeToString: Unknown file type, mode: %o", mode) return "" end end, --- Converts a numeric ACL mode to a string in an octal -- number format. -- -- @param mode number containing the ACL mode -- @return string containing the octal ACL mode FmodeToOctalString = function(mode) local code = bit.band(mode, Util.S_IFMT) if Util.FileType[code] then code = bit.bxor(mode, code) else code = mode stdnse.print_debug(1,"FmodeToOctalString: Unknown file type, mode: %o", mode) end return stdnse.tooctal(code) end, --- Converts a numeric ACL to it's character equivalent eg. (rwxr-xr-x) -- -- @param mode number containing the ACL mode -- @return string containing the ACL characters FpermToString = function(mode) local tmpacl, acl = {}, "" for i = 1, 9 do tmpacl[i] = "-" end for user,_ in pairs(Util.Fperm) do local t = Util.Fperm[user] for i in pairs(t) do local code = bit.band(mode, i) if t[code] then -- save set-ID and sticky bits if tmpacl[t[code].idx] == "x" then if t[code].char == "S" then tmpacl[t[code].idx] = "s" else tmpacl[t[code].idx] = t[code].char end elseif tmpacl[t[code].idx] == "S" then if t[code].char == "x" then tmpacl[t[code].idx] = "s" end else tmpacl[t[code].idx] = t[code].char end end end end for i = 1,#tmpacl do acl = acl .. tmpacl[i] end return acl end, --- Converts the NFS file attributes to a string. -- -- An optional second argument is the mactime to use -- -- @param attr table returned by NFS GETATTR or ACCESS -- @param mactime to use, the default value is mtime -- Possible values: mtime, atime, ctime -- @return string containing the file attributes format_nfsfattr = function(attr, mactime) local time = "mtime" if mactime then time = mactime end return string.format("%s%s uid: %5d gid: %5d %6s %s", Util.FtypeToChar(attr.mode), Util.FpermToString(attr.mode), attr.uid, attr.gid, Util.SizeToHuman(attr.size), Util.TimeToString(attr[time].seconds)) end, marshall_int32 = function(int32, count) if count then return bin.pack(">i" .. count, int32) end return bin.pack(">i", int32) end, unmarshall_int32 = function(data, pos, count) if count then return bin.unpack(">i" .. count, data, pos) end return bin.unpack(">i", data, pos) end, marshall_uint32 = function(uint32, count) if count then return bin.pack(">I" .. count, uint32) end return bin.pack(">I", uint32) end, unmarshall_uint32 = function(data, pos, count) if count then return bin.unpack(">I" .. count, data, pos) end return bin.unpack(">I", data, pos) end, marshall_int64 = function(int64, count) if count then return bin.pack(">l" .. count, int64) end return bin.pack(">l", int64) end, unmarshall_int64 = function(data, pos, count) if count then return bin.unpack(">l" .. count, data, pos) end return bin.unpack(">l", data, pos) end, marshall_uint64 = function(uint64, count) if count then return bin.pack(">L" .. count, uint64) end return bin.pack(">L", uint64) end, unmarshall_uint64 = function(data, pos, count) if count then return bin.unpack(">L" .. count, data, pos) end return bin.unpack(">L", data, pos) end, marshall_opaque = function(data) local opaque = bin.pack(">A", data) for i = 1, Util.CalcFillBytes(data:len()) do opaque = opaque .. string.char(0x00) end return opaque end, unmarshall_opaque = function(len, data, pos) return bin.unpack(">A" .. len, data, pos) end, marshall_vopaque = function(data) local opaque, l l = data:len() opaque = Util.marshall_uint32(l) .. bin.pack(">A", data) for i = 1, Util.CalcFillBytes(l) do opaque = opaque .. string.char(0x00) end return opaque end, unmarshall_vopaque = function(len, data, pos) local opaque, pad pad = Util.CalcFillBytes(len) pos, opaque = bin.unpack(">A" .. len, data, pos) return pos + pad, opaque end, unmarshall_nfsftype = function(data, pos, count) return Util.unmarshall_uint32(data, pos, count) end, unmarshall_nfsfmode = function(data, pos, count) return Util.unmarshall_uint32(data, pos, count) end, unmarshall_nfssize3 = function(data, pos, count) return Util.unmarshall_uint64(data, pos, count) end, unmarshall_nfsspecdata3 = function(data, pos) local specdata3 = {} pos, specdata3.specdata1, specdata3.specdata2 = Util.unmarshall_uint32(data, pos, 2) return pos, specdata3 end, --- Unmarshall NFSv3 fileid field of the NFS attributes -- -- @param data The data being processed. -- @param pos The position within data -- @return pos The new position -- @return uint64 The decoded fileid unmarshall_nfsfileid3 = function(data, pos) return Util.unmarshall_uint64(data, pos) end, --- Unmarshall NFS time -- -- @param data The data being processed. -- @param pos The position within data -- @return pos The new position -- @return table The decoded NFS time table. unmarshall_nfstime = function(data, pos) local nfstime = {} pos, nfstime.seconds, nfstime.nseconds = Util.unmarshall_uint32(data, pos, 2) return pos, nfstime end, --- Unmarshall NFS file attributes -- -- @param data The data being processed. -- @param pos The position within data -- @param number The NFS version. -- @return pos The new position -- @return table The decoded file attributes table. unmarshall_nfsattr = function(data, pos, nfsversion) local attr = {} pos, attr.type = Util.unmarshall_nfsftype(data, pos) pos, attr.mode = Util.unmarshall_nfsfmode(data, pos) pos, attr.nlink, attr.uid, attr.gid = Util.unmarshall_uint32(data, pos, 3) if (nfsversion < 3) then pos, attr.size, attr.blocksize, attr.rdev, attr.blocks, attr.fsid, attr.fileid = Util.unmarshall_uint32(data, pos, 6) elseif (nfsversion == 3) then pos, attr.size = Util.unmarshall_nfssize3(data, pos) pos, attr.used = Util.unmarshall_nfssize3(data, pos) pos, attr.rdev = Util.unmarshall_nfsspecdata3(data, pos) pos, attr.fsid = Util.unmarshall_uint64(data, pos) pos, attr.fileid = Util.unmarshall_nfsfileid3(data, pos) else stdnse.print_debug(4, "unmarshall_nfsattr: unsupported NFS version %d", nfsversion) return -1, nil end pos, attr.atime = Util.unmarshall_nfstime(data, pos) pos, attr.mtime = Util.unmarshall_nfstime(data, pos) pos, attr.ctime = Util.unmarshall_nfstime(data, pos) return pos, attr end, --- Returns a string containing date and time -- -- @param number of seconds since some given start time -- (the "epoch") -- @return string that represents time. TimeToString = function(time) return os.date("!%F %H:%M", time) end, --- Converts the size in bytes to a human readable format -- -- An optional second argument is the size of a block -- @usage -- size_tohuman(1024) --> 1024.0B -- size_tohuman(926548776) --> 883.6M -- size_tohuman(246548, 1024) --> 240.8K -- size_tohuman(246548, 1000) --> 246.5K -- -- @param size in bytes -- @param blocksize represents the number of bytes per block -- Possible values are: 1024 or 1000 -- Default value is: 1024 -- @return string containing the size in the human readable -- format SizeToHuman = function(size, blocksize) local bs, idx = 1024, 1 local unit = { "B", "K", "M", "G" , "T"} if blocksize and blocksize == 1000 then bs = blocksize end for i=1, #unit do if (size > bs and idx < #unit) then size = size / bs idx = idx + 1 end end return string.format("%.1f%s", size, unit[idx]) end, format_access = function(mask, version) local ret, nfsobj = "", NFS:new() if nfsobj:AccessRead(mask, version) ~= 0 then ret = "Read " else ret = "NoRead " end if nfsobj:AccessLookup(mask, version) ~= 0 then ret = ret .. "Lookup " else ret = ret .. "NoLookup " end if nfsobj:AccessModify(mask, version) ~= 0 then ret = ret .. "Modify " else ret = ret .. "NoModify " end if nfsobj:AccessExtend(mask, version) ~= 0 then ret = ret .. "Extend " else ret = ret .. "NoExtend " end if nfsobj:AccessDelete(mask, version) ~= 0 then ret = ret .. "Delete " else ret = ret .. "NoDelete " end if nfsobj:AccessExecute(mask, version) ~= 0 then ret = ret .. "Execute" else ret = ret .. "NoExecute" end return ret end, --- Return the pathconf filesystem table -- -- @param pconf table returned by the NFSv3 PATHCONF call -- @param nfsversion the version of the remote NFS server -- @return fs table that contains the remote filesystem -- pathconf information. calc_pathconf_table = function(pconf, nfsversion) local fs = {} if nfsversion ~= 3 then return nil, "ERROR: unsupported NFS version." end fs.linkmax = pconf.linkmax fs.name_max = pconf.name_max if pconf.chown_restricted then fs.chown_restricted = "True" else fs.chown_restricted = "False" end return fs, nil end, --- Calculate and return the fsinfo filesystem table -- -- @param fsinfo table returned by the NFSv3 FSINFO call -- @param nfsversion the version of the remote NFS server -- @param human if set show the size in the human -- readable format. -- @return fs table that contains the remote filesystem -- information. calc_fsinfo_table = function(fsinfo, nfsversion, human) local fs = {} local nfsobj = NFS:new() if nfsversion ~= 3 then return nil, "ERROR: unsupported NFS version." end fs.maxfilesize = Util.SizeToHuman(fsinfo.maxfilesize) if nfsobj:FSinfoLink(fsinfo.properties, nfsversion) ~= 0 then fs.link = "True" else fs.link = "False" end if nfsobj:FSinfoSymlink(fsinfo.properties, nfsversion) ~= 0 then fs.symlink = "True" else fs.symlink = "False" end return fs, nil end, --- Calculate and return the fsstat filesystem table -- -- @param stats table returned by the NFSv3 FSSTAT or -- NFSv2 STATFS calls -- @param nfsversion the version of the remote NFS server -- @param human if set show the size in the human -- readable format. -- @return df table that contains the remote filesystem -- attributes. calc_fsstat_table = function(stats, nfsversion, human) local df, base = {}, 1024 local size, free, total, avail, used, use if (nfsversion == 3) then free = stats.fbytes size = stats.tbytes avail = stats.abytes elseif (nfsversion == 2) then df.bsize = stats.block_size free = stats.free_blocks * df.bsize size = stats.total_blocks * df.bsize avail = stats.available_blocks * df.bsize else return nil, "ERROR: unsupported NFS version." end if (human) then if (df.bsize) then df.bsize = Util.SizeToHuman(df.bsize) end df.size = Util.SizeToHuman(size) df.available = Util.SizeToHuman(avail) used = size - free avail = avail df.used = Util.SizeToHuman(used) total = used + avail else free = free / base df.size = size / base df.available = avail / base used = df.size - free df.used = used total = df.used + df.available end use = math.ceil(used * 100 / total) df.use = string.format("%.0f%%", use) return df, nil end, --- Converts a RPC program name to it's equivalent number -- -- @param prog_name string containing the name of the RPC program -- @return num number containing the program ID ProgNameToNumber = function(prog_name) local status if not( RPC_PROGRAMS ) then status, RPC_PROGRAMS = datafiles.parse_rpc() if ( not(status) ) then return end end for num, name in pairs(RPC_PROGRAMS) do if ( prog_name == name ) then return num end end return end, --- Converts the RPC program number to it's equivalent name -- -- @param num number containing the RPC program identifier -- @return string containing the RPC program name ProgNumberToName = function( num ) local status if not( RPC_PROGRAMS ) then status, RPC_PROGRAMS = datafiles.parse_rpc() if ( not(status) ) then return end end return RPC_PROGRAMS[num] end, -- -- Calculates the number of fill bytes needed -- @param length contains the length of the string -- @return the amount of pad needed to be dividable by 4 CalcFillBytes = function(length) -- calculate fill bytes if math.fmod( length, 4 ) ~= 0 then return (4 - math.fmod( length, 4)) else return 0 end end } return _ENV;