diff --git a/CHANGELOG b/CHANGELOG index 72e8d72ca..5639878e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,13 @@ [NOT YET RELEASED] +o [NSE] Added RPC library and three new NFS scripts. Modified the rpcinfo and + nfs-showmount scripts to use the new library. The new scripts are: + - nfs-acls shows the owner and directory mode of NFS exports + - nfs-dirlist lists the contents of NFS exports + - nfs-statfs shows file system statistics for NFS exports + [Patrik] + o Fixed the Idle Scan (-sI) so that scanning multiple hosts doesn't retest the zombie proxy and reinitialize all of the associated data at the beginning of each run. [Kris] diff --git a/nselib/rpc.lua b/nselib/rpc.lua new file mode 100644 index 000000000..ed783ede9 --- /dev/null +++ b/nselib/rpc.lua @@ -0,0 +1,1681 @@ +--- RPC Library supporting a very limited subset of operations +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- @author = "Patrik Karlsson " +-- +-- Summary +-- ------- +-- o The library works over both the UDP and TCP protocols +-- o A subset of nfs and mountd procedures are supported +-- o The versions 1 through 3 are supported for the nfs and mountd program +-- o Authentication is supported using the NULL RPC Authentication protocol +-- +-- Overview +-- -------- +-- The library contains the following classes: +-- o Comm +-- - Handles low-level packet sending, recieving, decoding and encoding +-- - Used by Mount, NFS, RPC and Portmap +-- o Mount +-- - Handles communication with the mount RPC program +-- o NFS +-- - Handles communication with the nfs RPC program +-- o Helper +-- - Provides easy access to common RPC functions +-- - Implemented as a static class where most functions accept host +-- and port parameters +-- o RPC +-- - Static container for constants +-- o Portmap +-- - Handles communication with the portmap RPC program +-- o 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: +-- o Make sure that we can talk to the portmapper on port 111 tcp or udp +-- o Query the portmapper for the ports allocated to the NFS program +-- o 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. +-- +-- Example +-- ------- +-- 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 +-- +-- +-- Additional information +-- ---------------------- +-- RPC transaction ID's (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 overrided 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. +-- +-- @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") + +-- +-- 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 +-- + +module(... or "rpc", package.seeall) +require("datafiles") + + +-- Defines the order in which to try to connect to the RPC programs +-- TCP appears to be more stable than UDP in most cases, so try it first +local RPC_PROTOCOLS = ( nmap.registry.args and nmap.registry.args['rpc.protocol'] and type(nmap.registry.args['rpc.protocol']) == 'table') and nmap.registry.args['rpc.protocol'] or { "tcp", "udp" } + +-- used to cache the contents of the rpc datafile +local RPC_PROGRAMS + +-- Supported protocol versions +Version = { + ["nfs"] = { min=1, max=3 }, + ["mountd"] = { min=1, max=3 }, +} + +math.randomseed( os.time() ) + +-- Low-level communication class +Comm = { + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + 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 + GetAdditionalBytes = function( self, data, pos, needed ) + + local status = true + local 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 status, data + end, + + --- Creates a RPC header + -- + -- @param xid number + -- @param program_id number containing the program_id to connect to + -- @param program_version number containing the version to query + -- @param procedure number containing the procedure to call + -- @param auth table containing the authentication data to use + -- @return string of bytes + CreateHeader = function( self, xid, program_id, program_version, procedure, auth ) + local RPC_VERSION = 2 + local packet + + if not(xid) then + xid = math.random(1234567890) + end + if not auth or auth.type ~= RPC.AuthType.Null then + return false, "No or invalid authentication type specified" + end + + packet = bin.pack( ">IIIIII", xid, RPC.MessageType.Call, RPC_VERSION, program_id, program_version, procedure ) + if auth.type == RPC.AuthType.Null then + packet = packet .. bin.pack( "IIII", 0, 0, 0, 0 ) + 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 + 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 + return -1, nil + end + data = data .. tmp + end + + pos, header.xid, header.type, header.state = bin.unpack(">III", data, pos) + 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 + return -1, nil + end + pos, header.verifier.data = bin.unpack("A" .. header.verifier.length - 8, data, pos ) + end + pos, header.accept_state = bin.unpack(">I", data, pos ) + return pos, header + end, + + --- Reads the response from the socket + -- + -- @return data string containing the raw response + 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 + + repeat + lastfragment = false + status, data = self:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "rpc.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, "rpc.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 + return true, data + end + end, + + --- Encodes a RPC packet + -- + -- @param xid number containing the transaction ID + -- @param prog number containing the program id + -- @param auth table containing authentication information + -- @param data string containing the packet data + -- @return packet string containing the encoded packet data + EncodePacket = function( self, xid, prog, auth, data ) + local status, packet = self:CreateHeader( xid, prog.id, prog.version, prog.proc, auth ) + 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 ) + return self.socket:send( packet ) + 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 = { + + 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, + + --- Connects to the mountd program + -- + -- @param host table + -- @param port table + -- @param version number containing the program version to use + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Connect = function( self, host, port, version ) + local socket = nmap.new_socket() + local status, result = socket:connect(host.ip, port.number, port.protocol) + + if ( status ) then + self.socket = socket + self.proto = port.protocol + self.comm = Comm:new( { socket = socket, proto=port.protocol} ) + self.version = ( nmap.registry.args and nmap.registry.args['mount.version'] ) and tonumber(nmap.registry.args['mount.version']) or version + + if ( self.version > Version["mountd"].max or self.version < Version["mountd"].min ) then + return false, "Library does not support mountd version: " .. self.version + end + end + + return status, result + end, + + --- Disconnects from the mountd program + -- + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Disconnect = function( self ) + local status, result = self.socket:close() + if ( status ) then + self.proto = nil + self.socket = nil + self.comm = nil + end + return status, result + end, + + --- Requests a list of NFS export from the remote server + -- + -- @return status success or failure + -- @return entries table containing a list of share names (strings) + Export = function( self ) + + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local msg_type = 0 + local prg_mount = Util.ProgNameToNumber("mountd") + local packet + local pos = 1 + local header = {} + local entries = {} + local data = "" + local status + + local REPLY_ACCEPTED, SUCCESS, PROC_EXPORT = 0, 0, 5 + + if self.proto ~= "tcp" and self.proto ~= "udp" then + return false, "Protocol should be either udp or tcp" + end + packet = self.comm:EncodePacket( nil, { id=prg_mount, version=self.version, proc=Mount.Procedure.EXPORT }, { type=RPC.AuthType.Null }, nil ) + try( self.comm:SendPacket( packet ) ) + + status, data = self.comm:ReceivePacket() + if ( not(status) ) then + return false, "mountExportCall: Failed to read data from socket" + end + + -- make sure we have atleast 24 bytes to unpack the header + data = try( self.comm:GetAdditionalBytes( data, pos, 24 ) ) + pos, header = self.comm:DecodeHeader( data, pos ) + + if not header then + return false, "Failed to decode header" + end + + if header.type ~= RPC.MessageType.Reply then + return false, string.format("Packet was not a reply") + end + + if header.state ~= REPLY_ACCEPTED then + return false, string.format("Reply state was not Accepted(0) as expected") + end + + if header.accept_state ~= SUCCESS then + return false, string.format("Accept State was not Successful") + 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 + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + + local data_follows + pos, data_follows = bin.unpack( ">I", 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 + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + pos, len = bin.unpack(">I", data, pos ) + + data = try( self.comm:GetAdditionalBytes( data, pos, len ) ) + pos, entry.name = bin.unpack("A" .. len, data, pos ) + pos = pos + Util.CalcFillBytes( len ) + + -- decode groups + while true do + local group + + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + pos, data_follows = bin.unpack( ">I", data, pos ) + + if data_follows ~= 1 then + break + end + + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + pos, len = bin.unpack( ">I", data, pos ) + data = try( self.comm:GetAdditionalBytes( data, pos, len ) ) + pos, group = bin.unpack( "A" .. len, data, pos ) + + table.insert( entry, group ) + pos = pos + Util.CalcFillBytes( len ) + end + table.insert(entries, entry) + end + return true, entries + end, + + + --- Attempts to mount a remote export in order to get the filehandle + -- + -- @param path string containing the path to mount + -- @return status success or failure + -- @return fhandle string containing the filehandle of the remote export + Mount = function( self, path ) + + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local packet, data + local prog_id = Util.ProgNameToNumber("mountd") + local _, pos, data, header, fhandle = "", 1, "", "", {} + local status, len + + local REPLY_ACCEPTED, SUCCESS, MOUNT_OK = 0, 0, 0 + + data = bin.pack(">IA", path:len(), path) + + for i=1, Util.CalcFillBytes( path:len() ) do + data = data .. string.char( 0x00 ) + end + + packet = self.comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=Mount.Procedure.MOUNT }, { type=RPC.AuthType.Null }, data ) + try( self.comm:SendPacket( packet ) ) + + status, data = self.comm:ReceivePacket() + if ( not(status) ) then + return false, "mountCall: Failed to read data from socket" + end + + pos, header = self.comm:DecodeHeader( data, pos ) + if not header then + return false, "Failed to decode header" + end + + if header.type ~= RPC.MessageType.Reply then + return false, string.format("Packet was not a reply") + end + + if header.state ~= REPLY_ACCEPTED then + return false, string.format("Reply state was not Accepted(0) as expected") + end + + if header.accept_state ~= SUCCESS then + return false, string.format(3, "mountCall: Accept State was not Successful", path) + end + + local mount_status + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + pos, mount_status = bin.unpack(">I", data, pos ) + + if mount_status ~= MOUNT_OK then + if ( mount_status == 13 ) then + return false, "Access Denied" + else + return false, string.format("Mount failed: %d", mount_status) + end + end + + if ( self.version == 3 ) then + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + _, len = bin.unpack(">I", data, pos ) + data = try( self.comm:GetAdditionalBytes( data, pos, len + 4 ) ) + pos, fhandle = bin.unpack( "A" .. len + 4, data, pos ) + elseif ( self.version < 3 ) then + data = try( self.comm:GetAdditionalBytes( data, pos, 32 ) ) + 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 path string containing the path to mount + -- @return status success or failure + -- @return error string containing error if status is false + Unmount = function( self, path ) + + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local packet, data + local prog_id = Util.ProgNameToNumber("mountd") + local _, pos, data, header, fhandle = "", 1, "", "", {} + local status + + local REPLY_ACCEPTED, SUCCESS, MOUNT_OK = 0, 0, 0 + + data = bin.pack(">IA", path:len(), path) + + for i=1, Util.CalcFillBytes( path:len() ) do + data = data .. string.char( 0x00 ) + end + + packet = self.comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=Mount.Procedure.UMNT }, { type=RPC.AuthType.Null }, data ) + try( self.comm:SendPacket( packet ) ) + + status, data = self.comm:ReceivePacket( ) + if ( not(status) ) then + return false, "mountCall: Failed to read data from socket" + end + + pos, header = self.comm:DecodeHeader( data, pos ) + if not header then + return false, "Failed to decode header" + end + + if header.type ~= RPC.MessageType.Reply then + return false, string.format("Packet was not a reply") + end + + if header.state ~= REPLY_ACCEPTED then + return false, string.format("Reply state was not Accepted(0) as expected") + end + + if header.accept_state ~= SUCCESS then + return false, string.format(3, "mountCall: Accept State was not Successful", path) + end + + 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 = { + + -- 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, + }, + }, + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Connects to the nfsd program + -- + -- @param host table + -- @param port table + -- @param version number containing the program version to use + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Connect = function( self, host, port, version ) + local socket = nmap.new_socket() + local status, result = socket:connect(host.ip, port.number, port.protocol) + + if ( status ) then + self.socket = socket + self.proto = port.protocol + self.version = ( nmap.registry.args and nmap.registry.args['nfs.version'] ) and tonumber(nmap.registry.args['nfs.version']) or version + + if ( self.version > Version["nfs"].max or self.version < Version["nfs"].min ) then + return false, "Library does not support nfsd version: " .. self.version + end + + self.comm = Comm:new( { socket = socket, proto=port.protocol} ) + end + + return status, result + end, + + --- Disconnects from the nfsd program + -- + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Disconnect = function( self ) + local status, result = self.socket:close() + if ( status ) then + self.proto = nil + self.socket = nil + self.comm = nil + end + return status, result + end, + + --- Decodes the READDIR section of a NFS ReadDir response + -- + -- @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, data, pos ) + + local entry, response = {}, {} + local value_follows + local status, _ + + local NFS_OK = 0 + + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, status = bin.unpack(">I", data, pos) + if status ~= NFS_OK then + return -1, nil + end + + if ( 3 == self.version ) then + local attrib = {} + response.attributes = {} + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if( not(status) ) then + return false, "NFS.ReadDirDecode failed to get additional bytes from socket" + end + pos, value_follows = bin.unpack(">I", data, pos) + if value_follows == 0 then + return -1, nil + end + status, data = self.comm:GetAdditionalBytes( data, pos, 84 ) + if( not(status) ) then + return false, "NFS.ReadDirDecode failed to get additional bytes from socket" + end + pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, attrib.gid, + attrib.size, attrib.used, attrib.rdev, attrib.fsid, attrib.fileid, + attrib.atime, attrib.mtime, attrib.ctime = bin.unpack(">IIIIILLLLLLLL", data, pos) + table.insert(response.attributes, attrib) + -- opaque data + status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + pos, _ = bin.unpack(">L", data, pos) + end + + response.entries = {} + while true do + entry = {} + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, value_follows = bin.unpack(">I", data, pos) + + if ( value_follows == 0 ) then + break + end + + if ( 3 == self.version ) then + status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + pos, entry.fileid = bin.unpack(">L", data, pos ) + else + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + pos, entry.fileid = bin.unpack(">I", data, pos ) + end + + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, entry.length = bin.unpack(">I", data, pos) + status, data = self.comm:GetAdditionalBytes( data, pos, entry.length ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, entry.name = bin.unpack("A" .. entry.length, data, pos) + pos = pos + Util.CalcFillBytes( entry.length ) + + if ( 3 == self.version ) then + status, data = self.comm:GetAdditionalBytes( data, pos, 8 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, entry.cookie = bin.unpack(">L", data, pos) + else + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "ReadDirDecode failed" + end + + pos, entry.cookie = bin.unpack(">I", data, pos) + end + table.insert( response.entries, entry ) + end + return pos, response + end, + + + --- Reads the contents inside a NFS directory + -- + -- @param file_handle string containing the filehandle to query + -- @return status true on success, false on failure + -- @return table of file table entries as described in decodeReadDir + ReadDir = function( self, file_handle ) + + local status, packet + local cookie, count = 0, 8192 + local pos, data, _ = 1, "", "" + local header, response = {}, {} + + if not file_handle or file_handle:len() ~= 32 then + return false, "Incorrect filehandle received" + end + + if ( self.version == 3 ) then + local opaque_data = 0 + data = bin.pack("A>L>L>I", file_handle, cookie, opaque_data, count) + else + data = bin.pack("A>I>I", file_handle, cookie, count) + end + packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].READDIR }, { type=RPC.AuthType.Null }, data ) + status = self.comm:SendPacket( packet ) + if ( not(status) ) then + return false, "nfsReadDir: Failed to write to socket" + end + + status, data = self.comm:ReceivePacket() + if ( not(status) ) then + return false, "nfsReadDir: Failed to read data from socket" + end + pos, header = self.comm:DecodeHeader( data, pos ) + + if not header then + return false, "Failed to decode header" + end + pos, response = self:ReadDirDecode( data, pos ) + return true, response + end, + + --- Gets filesystem stats (Total Blocks, Free Blocks and Available block) on a remote NFS share + -- + -- @param 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 + -- @returns errormsg if status is false + StatFs = function( self, file_handle ) + + local status, packet + local pos, data, _ = 1, "", "" + local header, statfs = {}, {} + + if ( self.version > 2 ) then + return false, ("Version %d not supported"):format(self.version) + end + + if not file_handle or file_handle:len() ~= 32 then + return false, "Incorrect filehandle received" + end + + data = bin.pack("A", file_handle ) + packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].STATFS }, { type=RPC.AuthType.Null }, data ) + status = self.comm:SendPacket( packet ) + if ( not(status) ) then + return false, "nfsStatFs: Failed to write to socket" + end + + status, data = self.comm:ReceivePacket( ) + if ( not(status) ) then + return false, "nfsStatFs: Failed to read data from socket" + end + + pos, header = self.comm:DecodeHeader( data, pos ) + + if not header then + return false, "Failed to decode header" + end + + pos, statfs = self:StatFsDecode( data, pos ) + + if not statfs then + return false, "Failed to decode statfs structure" + end + return true, statfs + end, + + --- Attempts to decode the attributes section of the reply + -- + -- @param data string containing the full statfs reply + -- @param pos number pointing to the statfs section of the reply + -- @return pos number containing the offset after decoding + -- @return statfs table with the following fields: type, mode, + -- nlink, uid, gid, size, + -- blocksize, rdev, blocks, fsid, + -- fileid, atime, mtime and ctime + -- + GetAttrDecode = function( self, data, pos ) + local attrib = {} + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local NFS_OK = 0 + local status + + status, data = self.comm:GetAdditionalBytes( data, pos, 4 ) + if ( not(status) ) then + return false, "GetAttrDecode: GetAdditionalBytes failed" + end + + pos, attrib.status = bin.unpack(">I", data, pos) + + if attrib.status ~= NFS_OK then + return -1, nil + end + if ( self.version < 3 ) then + status, data = self.comm:GetAdditionalBytes( data, pos, 64 ) + if ( not(status) ) then + return false, "GetAttrDecode: GetAdditionalBytes failed" + end + + pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, + attrib.gid, attrib.size, attrib.blocksize, attrib.rdev, + attrib.blocks, attrib.fsid, attrib.fileid, attrib.atime, + attrib.mtime, attrib.ctime = bin.unpack( ">IIIIIIIIIILLL", data, pos ) + elseif ( self.version == 3 ) then + status, data = self.comm:GetAdditionalBytes( data, pos, 84 ) + if ( not(status) ) then + return false, "GetAttrDecode: GetAdditionalBytes failed" + end + pos, attrib.type, attrib.mode, attrib.nlink, attrib.uid, + attrib.gid, attrib.size, attrib.used, attrib.rdev, + attrib.fsid, attrib.fileid, attrib.atime, attrib.mtime, + attrib.ctime = bin.unpack(">IIIIILLLLLLLL", data, pos) + else + return -1, "Unsupported version" + end + return pos, attrib + end, + + --- Gets mount attributes (uid, gid, mode, etc ..) from a remote NFS share + -- + -- @param file_handle string containing the filehandle to query + -- @return status true on success, false on failure + -- @return attribs table with the fields type, mode, + -- nlink, uid, gid, size, + -- blocksize, rdev, blocks, fsid, + -- fileid, atime, mtime and ctime + -- @return errormsg if status is false + GetAttr = function( self, file_handle ) + local data, packet, status, attribs, pos, header + + data = bin.pack("A", file_handle) + packet = self.comm:EncodePacket( nil, { id=Util.ProgNameToNumber("nfs"), version=self.version, proc=NFS.Procedure[self.version].GETATTR }, { type=RPC.AuthType.Null }, data ) + status = self.comm:SendPacket(packet) + if ( not(status) ) then + return false, "nfsGetAttribs: Failed to send data to socket" + end + + status, data = self.comm:ReceivePacket() + if ( not(status) ) then + return false, "nfsGetAttribs: Failed to read data from socket" + end + + pos, header = self.comm:DecodeHeader( data, 1 ) + + if not header then + return false, "Failed to decode header" + end + + pos, attribs = self:GetAttrDecode( data, pos ) + + if not attribs then + return false, "Failed to decode attrib structure" + end + + return true, attribs + end, + + --- Attempts to decode the StatFS section of the reply + -- + -- @param data string containing the full statfs reply + -- @param pos number pointing to the statfs section of the reply + -- @return pos number containing the offset after decoding + -- @return statfs table with the following fields: transfer_size, block_size, + -- total_blocks, free_blocks and available_blocks + -- + StatFsDecode = function( self, data, pos ) + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local statfs = {} + local NFS_OK, NSFERR_ACCESS = 0, 13 + + data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) ) + pos, statfs.status = bin.unpack(">I", data, pos) + + if statfs.status ~= NFS_OK then + if statfs.status == NSFERR_ACCESS then + stdnse.print_debug("STATFS query received NSFERR_ACCESS") + end + return -1, nil + end + + data = try( self.comm:GetAdditionalBytes( data, pos, 20 ) ) + pos, statfs.transfer_size, statfs.block_size, + statfs.total_blocks, statfs.free_blocks, + statfs.available_blocks = bin.unpack(">IIIII", data, pos ) + return pos, statfs + end, +} + +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 data, prog_tbl = {}, {} + local status, result, mounts, response + local socket = nmap.new_socket() + local mountd + local ver + local mnt = Mount:new() + local portmap = Portmap:new() + local portmap_table, proginfo + + status, mountd = Helper.GetProgramInfo( host, port, "mountd") + + if ( not(status) ) then + return false, "Failed to retrieve rpc information for mountd" + end + + status, result = mnt:Connect( host, mountd.port, mountd.version ) + if ( not(status) ) then + stdnse.print_debug(3, result) + return false, result + end + + status, mounts = mnt:Export() + + mnt:Disconnect() + + return status, mounts + 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 mountd, nfsd = {}, {} + local mnt, nfs = Mount:new(), NFS:new() + + status, mountd = Helper.GetProgramInfo( host, port, "mountd", 2) + if ( not(status) ) then + return false, "Failed to retrieve rpc information for mountd" + end + + status, nfsd = Helper.GetProgramInfo( host, port, "nfs", 2) + if ( not(status) ) then + return false, "Failed to retrieve rpc information for nfsd" + end + + status, result = mnt:Connect( host, mountd.port, mountd.version ) + if ( not(status) ) then + return false, "Failed to connect to mountd program" + end + + status, result = nfs:Connect( host, nfsd.port, nfsd.version ) + if ( not(status) ) then + mnt:Disconnect() + return false, "Failed to connect to nfsd program" + end + + status, fhandle = mnt:Mount( path ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + stdnse.print_debug("rpc.Helper.ExportStats: mount failed") + return false, "Mount failed" + end + + status, stats = nfs:StatFs( fhandle ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + return false, stats + end + + status, fhandle = mnt:Unmount( path ) + + mnt:Disconnect() + nfs:Disconnect() + + 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, nfs = Mount:new(), NFS:new() + + status, mountd = Helper.GetProgramInfo( host, port, "mountd") + if ( not(status) ) then + return false, "Failed to retrieve rpc information for mountd" + end + + status, nfsd = Helper.GetProgramInfo( host, port, "nfs") + if ( not(status) ) then + return false, "Failed to retrieve rpc information for nfsd" + end + + status, result = mnt:Connect( host, mountd.port, mountd.version ) + if ( not(status) ) then + return false, "Failed to connect to mountd program" + end + + status, result = nfs:Connect( host, nfsd.port, nfsd.version ) + if ( not(status) ) then + mnt:Disconnect() + return false, "Failed to connect to nfsd program" + end + + status, fhandle = mnt:Mount( path ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + return false, "rpc.Helper.Dir: mount failed" + end + + status, dirs = nfs:ReadDir( fhandle ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + return false, "rpc.Helper.Dir: statfs failed" + end + + status, fhandle = mnt:Unmount( path ) + + mnt:Disconnect() + nfs:Disconnect() + + if ( not(status) ) then + return false, "rpc.Helper.Dir: mount failed" + 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 mountd, nfsd = {}, {} + local mnt, nfs = Mount:new(), NFS:new() + + status, mountd = Helper.GetProgramInfo( host, port, "mountd") + if ( not(status) ) then + return false, "Failed to retrieve rpc information for mountd" + end + + status, nfsd = Helper.GetProgramInfo( host, port, "nfs") + if ( not(status) ) then + return false, "Failed to retrieve rpc information for nfsd" + end + + status, result = mnt:Connect( host, mountd.port, mountd.version ) + if ( not(status) ) then + return false, "Failed to connect to mountd program" + end + + status, result = nfs:Connect( host, nfsd.port, nfsd.version ) + if ( not(status) ) then + mnt:Disconnect() + return false, "Failed to connect to nfsd program" + end + + status, fhandle = mnt:Mount( path ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + return false, "rpc.Helper.GetAttributes: mount failed" + end + + status, attribs = nfs:GetAttr( fhandle ) + if ( not(status) ) then + mnt:Disconnect() + nfs:Disconnect() + return false, "rpc.Helper.GetAttributes: GetAttr failed" + end + + status, fhandle = mnt:Unmount( path ) + + mnt:Disconnect() + nfs:Disconnect() + + if ( not(status) ) then + return false, "rpc.Helper.ExportStats: mount failed" + 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 portmap = Portmap:new() + local status = Portmap:Connect(host, port) + local result + + if ( not(status) ) then + return + end + + status, result = portmap:Dump() + portmap:Disconnect() + + return status, result + end, + + --- Queries the portmapper for a port for the specified RPC program + -- + -- @param host table + -- @param port table + -- @param program_id number containing the RPC program ID + -- @param protcol 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_id, protocol ) + local portmap = Portmap:new() + local status = Portmap:Connect(host, port) + local result + + if ( not(status) ) then + return + end + + status, result = portmap:GetPort( program_id, protocol, 1 ) + portmap:Disconnect() + + 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, response + local portmap_table, info + local portmap = Portmap:new() + + status, response = portmap:Connect( host, port ) + if ( not(status) ) then + return false, "rpc.Helper.ShowMounts: Failed to connect to portmap" + end + status, portmap_table = portmap:Dump() + if ( not(status) ) then + portmap:Disconnect() + return false, "rpc.Helper.ShowMounts: Failed to GetProgramVersions" + end + status = portmap:Disconnect() + if ( not(status) ) then + return false, "rpc.Helper.ShowMounts: Failed to disconnect from portmap" + end + + -- 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(Version[program]) ) then + info.version = tmp[p].version[#tmp[p].version] + status = true + else + for i=#tmp[p].version, 1, -1 do + if ( Version[program].max >= tmp[p].version[i] ) then + if ( 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, + +} + +--- Container class for RPC constants +RPC = +{ + AuthType = + { + Null = 0 + }, + + MessageType = + { + Call = 0, + Reply = 1 + }, + + Procedure = + { + [2] = + { + GETPORT = 3, + DUMP = 4, + }, + + }, + +} + +--- Portmap class +Portmap = +{ + PROTOCOLS = { + ['tcp'] = 6, + ['udp'] = 17, + }, + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Connects to the Portmapper + -- + -- @param host table + -- @param port table + -- @param version number containing the program version to use + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Connect = function( self, host, port, version ) + local socket = nmap.new_socket() + local status, result = socket:connect(host.ip, port.number, port.protocol) + + if ( status ) then + self.socket = socket + self.version = version or 2 + self.protocol = port.protocol + end + + return status, result + end, + + --- Disconnects from the portmapper program + -- + -- @return status boolean true on success, false on failure + -- @return result string containing error message (if status is false) + Disconnect = function( self ) + local status, result = self.socket:close() + if ( status ) then + self.socket = nil + end + return status, result + end, + + --- Dumps a list of RCP programs from the portmapper + -- + -- @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 ) + local status, data, packet, response, pos, header + + local prog_id = Util.ProgNameToNumber("rpcbind") + local prog_proc = RPC.Procedure[self.version].DUMP + local comm + + if ( self.program_table ) then + return true, self.program_table + end + + comm = Comm:new( { socket=self.socket, proto=self.protocol } ) + packet = comm:EncodePacket( nil, { id=prog_id, version=self.version, proc=prog_proc }, { type=RPC.AuthType.Null }, data ) + status, response = comm:SendPacket( packet ) + 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, "Failed to decode RPC header" + end + if header.accept_state ~= 0 then + return false, string.format("RPC Accept State was not Successful") + end + + self.program_table = {} + + while true do + local vfollows + local program, version, protocol, port + + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + 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 + + self.program_table[program] = self.program_table[program] or {} + self.program_table[program][protocol] = self.program_table[program][protocol] or {} + self.program_table[program][protocol]["port"] = port + self.program_table[program][protocol]["version"] = self.program_table[program][protocol]["version"] or {} + table.insert( self.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( self.program_table[program][protocol]["version"] ) + end + + return true, self.program_table + + end, + + --- Queries the portmapper for the port of the selected program, + -- protocol and version + -- + -- @param program string name of the program + -- @param protocol string containing either "tcp" or "udp" + -- @param version number containing the version of the queried program + -- @return number containing the port number + GetPort = function( self, program, protocol, version ) + local status, data, response, header, pos, packet + local xid + local prog_id = Util.ProgNameToNumber("rpcbind") -- RPC Portmap + local prog_proc = RPC.Procedure[self.version].GETPORT + local comm + + if ( not( Portmap.PROTOCOLS[protocol] ) ) then + return false, ("Protocol %s not supported"):format(protocol) + end + + if ( Util.ProgNameToNumber( program ) == nil ) then + return false, ("Unknown program name: %s"):format(program) + end + + comm = Comm:new( { socket=self.socket, proto=self.protocol } ) + data = bin.pack( ">I>I>I>I", Util.ProgNameToNumber(program), version, Portmap.PROTOCOLS[protocol], 0 ) + packet = comm:EncodePacket( xid, { id=prog_id, version=self.version, proc=prog_proc }, { type=RPC.AuthType.Null }, data ) + + status = comm:SendPacket(packet) + data = "" + + status, data = comm:ReceivePacket() + if ( not(status) ) then + return false, "GetPort: Failed to read data from socket" + end + + pos, header = comm:DecodeHeader( data, 1 ) + + if ( not(header) ) then + return false, "Failed to decode RPC header" + end + + if header.accept_state ~= 0 then + return false, string.format("RPC Accept State was not Successful") + end + status, data = comm:GetAdditionalBytes( data, pos, 4 ) + return true, select(2, bin.unpack(">I", data, pos ) ) + + end, + +} + +--- Static class containing mostly conversion functions +Util = +{ + --- 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, + + --- Converts a numeric ACL mode as returned from mnt.GetAttr + -- to octal + -- + -- @param num number containing the ACL mode + -- @return num containing the octal ACL mode + ToAclMode = function( num ) + return ( ("%o"):format(bit.bxor(num, 0x4000)) ) + end, + + --- Converts a numeric ACL to it's character equivalent eg. (rwxr-xr-x) + -- + -- @param num number containing the ACL mode + ToAclText = function( num ) + local mode = num + local txtmode = "" + + for i=0,2 do + if ( bit.band( mode, bit.lshift(0x01, i*3) ) == bit.lshift(0x01, i*3) ) then + -- Check for SUID or SGID + if ( i>0 and bit.band( mode, 0x400 * i ) == 0x400 * i ) then + txtmode = "s" .. txtmode + else + txtmode = "x" .. txtmode + end + else + if ( i>0 and bit.band( mode, 0x400 * i ) == 0x400 * i ) then + txtmode = "S" .. txtmode + else + txtmode = "-" .. txtmode + end + end + if ( bit.band( mode, bit.lshift(0x02, i*3) ) == bit.lshift(0x02, i*3) ) then + txtmode = "w" .. txtmode + else + txtmode = "-" .. txtmode + end + if ( bit.band( mode, bit.lshift(0x04, i*3) ) == bit.lshift(0x04, i*3) ) then + txtmode = "r" .. txtmode + else + txtmode = "-" .. txtmode + end + end + + if ( bit.band(mode, 0x4000) == 0x4000 ) then + txtmode = "d" .. txtmode + else + txtmode = "-" .. txtmode + end + + return txtmode + end, + + -- + -- Calculates the number of fill bytes needed + -- @param length contains the length of the string + -- @return the amount of pad needed to be divideable by 4 + CalcFillBytes = function(length) + -- calculate fill bytes + if math.mod( length, 4 ) ~= 0 then + return (4 - math.mod( length, 4)) + else + return 0 + end + end + +} \ No newline at end of file diff --git a/scripts/nfs-acls.nse b/scripts/nfs-acls.nse new file mode 100644 index 000000000..be8cd1938 --- /dev/null +++ b/scripts/nfs-acls.nse @@ -0,0 +1,65 @@ +description = [[ +Shows NFS exports and access controls. +]] + +--- +-- @output +-- PORT STATE SERVICE +-- 111/tcp open rpcbind +-- | nfs-acls: +-- | /tmp +-- | uid: 0; gid: 0; mode: drwxrwxrwx (1777) +-- | /home/storage/backup +-- | uid: 0; gid: 0; mode: drwxr-xr-x (755) +-- | /home +-- |_ uid: 0; gid: 0; mode: drwxr-xr-x (755) +-- + +-- Version 0.6 + +-- Created 11/23/2009 - v0.1 - created by Patrik Karlsson +-- Revised 11/24/2009 - v0.2 - added RPC query to find mountd ports +-- Revised 11/24/2009 - v0.3 - added a hostrule instead of portrule +-- Revised 11/26/2009 - v0.4 - reduced packet sizes and documented them +-- Revised 01/24/2009 - v0.5 - complete rewrite, moved all NFS related code into nselib/nfs.lua +-- Revised 02/22/2009 - v0.6 - adapted to support new RPC library + + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require("shortport") +require("rpc") + +portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} ) + +action = function(host, port) + + local status, mounts, attribs + local result = {} + + status, mounts = rpc.Helper.ShowMounts( host, port ) + + if ( not(status) or mounts == nil ) then + return " \n\n Failed to list mount points" + end + + for _, mount in ipairs( mounts ) do + local item = {} + status, attribs = rpc.Helper.GetAttributes( host, port, mount.name ) + + item.name = mount.name + + if ( status ) then + table.insert(item, ("uid: %d; gid: %d; mode: %s (%d)"):format(attribs.uid, attribs.gid, rpc.Util.ToAclText( attribs.mode ), rpc.Util.ToAclMode( attribs.mode )) ) + else + table.insert(item, "ERROR: Mount failed") + end + + table.insert(result, item) + end + + return stdnse.format_output( true, result ) + +end diff --git a/scripts/nfs-dirlist.nse b/scripts/nfs-dirlist.nse new file mode 100644 index 000000000..79d222158 --- /dev/null +++ b/scripts/nfs-dirlist.nse @@ -0,0 +1,86 @@ +description = [[ +Does a directory listing of a remote NFS share +]] + +--- +-- @output +-- PORT STATE SERVICE +-- 111/tcp open rpcbind +-- | nfs-dirlist: +-- | /home/storage/backup +-- | www.cqure.net +-- | /home +-- | admin +-- | lost+found +-- | patrik +-- | storage +-- |_ web +-- + +-- Version 0.3 +-- +-- Created 01/25/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/22/2010 - v0.2 - adapted to support new RPC library +-- Revised 03/13/2010 - v0.3 - converted host to port rule + + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require("shortport") +require("rpc") + +portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} ) + +action = function(host, port) + + local status, mounts + local result, files = {}, {} + local hasmore = false + local proto + + status, mounts = rpc.Helper.ShowMounts( host, port ) + + if ( not(status) ) then + return " \n\n Failed to list mount points" + end + + for _, v in ipairs( mounts ) do + local files = {} + local status, dirlist = rpc.Helper.Dir(host, port, v.name) + + if status and dirlist then + local max_files = tonumber(nmap.registry.args.nfs_max_files) or 10 + hasmore = false + for _, v in ipairs( dirlist.entries ) do + if #files >= max_files then + hasmore = true + break + end + + if v.name ~= ".." and v.name ~= "." then + table.insert(files, v.name) + end + end + + table.sort(files) + + if hasmore then + files.name = v.name .. string.format(" (Output limited to %d files)", max_files ) + else + files.name = v.name + end + + table.insert( result, files ) + else + files.name = v.name + table.insert(files, "ERROR: Mount failed") + table.insert( result, files ) + end + + end + + return stdnse.format_output( true, result ) + +end diff --git a/scripts/nfs-showmount.nse b/scripts/nfs-showmount.nse index d581bdf74..666c8ede7 100644 --- a/scripts/nfs-showmount.nse +++ b/scripts/nfs-showmount.nse @@ -6,334 +6,48 @@ Shows NFS exports, like the showmount -e command. -- @output -- PORT STATE SERVICE -- 111/tcp open rpcbind --- --- Host script results: --- | nfs-showmount: --- | /home/storage/backup 10.46.200.0/255.255.255.0 10.46.200.66/255.255.255.255 --- |_ /home 10.46.200.0/255.255.255.0 +-- | nfs-showmount: +-- | /home/storage/backup 10.46.200.0/255.255.255.0 +-- |_ /home 1.2.3.4/255.255.255.255 10.46.200.0/255.255.255.0 -- --- Version 0.4 +-- Version 0.7 -- Created 11/23/2009 - v0.1 - created by Patrik Karlsson -- Revised 11/24/2009 - v0.2 - added RPC query to find mountd ports -- Revised 11/24/2009 - v0.3 - added a hostrule instead of portrule -- Revised 11/26/2009 - v0.4 - reduced packet sizes and documented them +-- Revised 01/24/2009 - v0.5 - complete rewrite, moved all NFS related code into nselib/nfs.lua +-- Revised 02/22/2009 - v0.6 - adapted to support new RPC library +-- Revised 03/13/2010 - v0.7 - converted host to port rule + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery", "safe"} -require 'comm' -require 'datafiles' +require("shortport") +require("rpc") -hostrule = function(host) +portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} ) - local port_t111 = nmap.get_port_state(host, {number=111, protocol="udp"}) - local port_u111 = nmap.get_port_state(host, {number=111, protocol="tcp"}) +action = function(host, port) - return ( port_t111 ~= nil and port_t111.state == "open") or - (port_u111 ~= nil and (port_u111.state == "open" or - port_u111.state == "open|filtered")) - -end - --- --- Calculates the number of fill bytes needed --- @param length contains the length of the string --- @return the amount of pad needed to be divideable by 4 --- -function calc_fill_bytes(length) - - -- calculate fill bytes - if math.mod( length, 4 ) ~= 0 then - return (4 - math.mod( length, 4)) - else - return 0 - end - -end - --- --- extracts the group from the export list entry --- @param data string should be start with 32-bit lenght field --- --- @return pos numeric new position within buffer, grp_val string the group contents --- -function extract_group(data) - - local pos, grp_val - - -- retrieve the group length - pos, grp_len = bin.unpack( ">i", data ) - data = data:sub(pos) - - -- retrieve the group contents - grp_val = data:sub(0, grp_len) - pos = 4 + calc_fill_bytes(grp_len) + grp_len + 1 - - return pos, grp_val - -end - --- --- extracts the directory from the export list entry --- @param data string should be start with 32-bit lenght field --- --- @return pos numeric new position within buffer, dir_name string the name of the directory --- -function extract_directory(data) - - local pos, dir_len, dir_name - - -- retrieve the length of the directory name - pos, dir_len = bin.unpack(">i", data) - data = data:sub(pos) - - -- retrieve the directory name - dir_name = data:sub(0, dir_len) - pos = 4 + calc_fill_bytes(dir_len) + dir_len + 1 - - return pos, dir_name - -end - --- --- processes the response back from the mountd service --- @param proto string should be either "udp" or "tcp" --- @param data string contains the response recieved from the service --- --- @return string with exports from NFS --- -function process_response(proto, data) - - local pos, val_follows - local header = {} - local response=" \n" - - -- if we're running over UDP skip first 4 bytes ( theres no 16-bit something + 16-bit length) - if "udp" == proto then - pos, header['xid'], header['type'], header['state'], - header['verifier'], header['accept_state'] = bin.unpack(">iiili", data) - else - pos, _, header['length'], header['xid'], header['type'], header['state'], - header['verifier'], header['accept_state'] = bin.unpack("S>Siiili", data) - end - - data = data:sub(pos) - - -- We should probably be doing a lot more verification here, but let's stick to basics - -- Was the response from the server = Reply(1) and - -- Accept state = RPC Executed succefully (0) - if header['type'] ~= 1 or header['accept_state'] ~= 0 then - return - end - - -- - -- - -- Each export list entry consists of: - -- - -- One or more Directory entries: - -- 32-bit - length - -- length - directory name - -- - -- One or more Group entries: - -- 32-bit - length - -- length - group contents - -- - -- Every group entry is separated by - -- 32-bit - value follows - if set to 1 more groups exist - -- - -- Every directory entry is separated by - -- 32-bit - value follows - if set to 1 more entries exist - -- - -- - -- Note: The length specifies the amount of characters for - -- both dir and group entries - -- - -- However, directories and groups are padded by zeroes so that - -- they are divideable by 4. Hence calc_fill_bytes - -- - - pos, val_follows = bin.unpack(">i", data) - data = data:sub(pos) - - while 1 == val_follows do - - local dir_name, exp_group - local grp_follows, grp_len, grp_val - - groups="" - - pos, dir_name = extract_directory( data ) - data = data:sub(pos) - - -- check if we have a group following - pos, grp_follows = bin.unpack(">i", data ) - data = data:sub(pos) - - while grp_follows == 1 do - - pos, grp_val = extract_group( data ) - groups = groups .. " " .. grp_val - - data = data:sub(pos) - - -- check if there's antoher group following - pos, grp_follows = bin.unpack(">i", data ) - data = data:sub(pos) - - end - - -- concatenate our dir_name and groups to the result - response = response .. dir_name .. "" .. groups .. "\n" - - -- are there any more directory entries? - pos, val_follows = bin.unpack(">i", data) - data = data:sub(pos) - - end - - return response -end - --- --- Ruthlessly ripped, and modified, from Sven Klemm's rpcinfo.nse script --- -function get_rpc_port_for_service(host, svc_progname, svc_version) - - local socket = nmap.new_socket() - socket:set_timeout(1000) - local catch = function() socket:close() end - local try = nmap.new_try(catch) - local rpc_numbers = try(datafiles.parse_rpc()) - - try(socket:connect(host.ip, 111)) - - -- build rpc dump call packet - local transaction_id = math.random(0x7FFFFFFF) - local request = bin.pack('>IIIIIIILL',0x80000028,transaction_id,0,2,100000,2,4,0,0) - try(socket:send(request)) - - local answer = try(socket:receive_bytes(1)) - - local _,offset,header,length,tx_id,msg_type,reply_state,accept_state,value,payload,last_fragment - last_fragment = false; offset = 1; payload = '' - - -- extract payload from answer and try to receive more packets if - -- RPC header with last_fragment set has not been received - -- If we can't get further packets don't stop but process what we - -- got so far. - while not last_fragment do - if offset > #answer then - local status, data = socket:receive_bytes(1) - if not status then break end - answer = answer .. data - end - offset,header = bin.unpack('>I',answer,offset) - last_fragment = bit.band( header, 0x80000000 ) ~= 0 - length = bit.band( header, 0x7FFFFFFF ) - payload = payload .. answer:sub( offset, offset + length - 1 ) - offset = offset + length - end - socket:close() - - offset,tx_id,msg_type,reply_state,_,_,accept_state = bin.unpack( '>IIIIII', payload ) - - -- transaction_id matches, message type reply, reply state accepted and accept state executed successfully - if tx_id == transaction_id and msg_type == 1 and reply_state == 0 and accept_state == 0 then - local dir = { udp = {}, tcp = {}} - local protocols = {[6]='tcp',[17]='udp'} - local prog, version, proto, port - local ports = {} - offset, value = bin.unpack('>I',payload,offset) - while value == 1 and #payload - offset >= 19 do - offset,prog,version,proto,port,value = bin.unpack('>IIIII',payload,offset) - proto = protocols[proto] or tostring( proto ) - - if rpc_numbers[prog] == svc_progname and version == svc_version then - ports[proto] = port - end - - end - - return ports - - end - - return - -end - -action = function(host) - - local data = {} - - -- packet copy/pasted from wireshark, running showmount -e - - data["tcp"] = string.char( - 0x80, 0x00, 0x00, 0x28, -- Fragment Length: 44 bytes (31-bit?) - -- Last Fragment: Yes - - 0x21, 0x00, 0x46, 0x4c, -- XID: 0x2100464c - 0x00, 0x00, 0x00, 0x00, -- Message type: Call(0) - 0x00, 0x00, 0x00, 0x02, -- RPC Version: 2 - 0x00, 0x01, 0x86, 0xa5, -- Program: MOUNT(100005) - 0x00, 0x00, 0x00, 0x01, -- Program Version: 1 - 0x00, 0x00, 0x00, 0x05, -- Procedure: EXPORT(5) - - -- Credentials - 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL (0) - 0x00, 0x00, 0x00, 0x00, -- Length: 0 - - -- Verifier - 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL - 0x00, 0x00, 0x00, 0x00 -- Length: 0 - ) - - data["udp"] = string.char( - 0x21, 0x00, 0x46, 0x4c, -- XID: 0x2100464c - 0x00, 0x00, 0x00, 0x00, -- Message type: Call(0) - 0x00, 0x00, 0x00, 0x02, -- RPC Version: 2 - 0x00, 0x01, 0x86, 0xa5, -- Program: MOUNT(100005) - 0x00, 0x00, 0x00, 0x01, -- Program Version: 1 - 0x00, 0x00, 0x00, 0x05, -- Procedure: EXPORT(5) - - -- Credentials - 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL (0) - 0x00, 0x00, 0x00, 0x00, -- Length: 0 - - -- Verifier - 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL - 0x00, 0x00, 0x00, 0x00 -- Length: 0 - - ) - - - local status, result - local ports = get_rpc_port_for_service(host, "mountd", 1) - - for p in pairs(ports) do - - status, result = comm.exchange(host, ports[p], data[p], {proto=p}) - - -- Fail gracefully - if not status then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: TIMEOUT" - else - return - end - end - - result = process_response( p, result ) - - if result then - return result - end - - end - - return + local status, mounts, proto + local result = {} + + status, mounts = rpc.Helper.ShowMounts( host, port ) + if not status or mounts == nil then + return " \n\n Failed to list mount points" + end + + for _, v in ipairs( mounts ) do + local entry = v.name + entry = entry .. " " .. stdnse.strjoin(" ", v) + table.insert( result, entry ) + end + + return stdnse.format_output( true, result ) + end diff --git a/scripts/nfs-statfs.nse b/scripts/nfs-statfs.nse new file mode 100644 index 000000000..1ac03520c --- /dev/null +++ b/scripts/nfs-statfs.nse @@ -0,0 +1,69 @@ +description = [[ +Retrieves disk space statistics from the remote NFS share +]] + +--- +-- @output +-- PORT STATE SERVICE +-- | nfs-statfs: +-- | /home/storage/backup +-- | Block size: 512 +-- | Total blocks: 1901338728 +-- | Free blocks: 729769328 +-- | Available blocks: 633186880 +-- | /home +-- | Block size: 512 +-- | Total blocks: 1901338728 +-- | Free blocks: 729769328 +-- |_ Available blocks: 633186880 +-- + +-- Version 0.3 + +-- Created 01/25/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/22/2010 - v0.2 - adapted to support new RPC library +-- Revised 03/13/2010 - v0.3 - converted host to port rule + + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require("shortport") +require("rpc") + +portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} ) + +action = function(host, port) + + local result, entry = {}, {} + local status, mounts = rpc.Helper.ShowMounts( host, port ) + + if ( not(status) ) then + return " \n\n Failed to list mount points" + end + + for _, v in ipairs( mounts ) do + local entry = {} + local status, stats = rpc.Helper.ExportStats(host, port, v.name) + + if ( not(status) and stats:match("Version %d not supported") ) then + return " \n\n " .. stats + end + + entry.name = v.name + + if status and stats then + table.insert( entry, string.format("Block size: %d", stats.block_size) ) + table.insert( entry, string.format("Total blocks: %d", stats.total_blocks) ) + table.insert( entry, string.format("Free blocks: %d", stats.free_blocks) ) + table.insert( entry, string.format("Available blocks: %d", stats.available_blocks) ) + else + table.insert( entry, "ERROR: Mount failed") + end + table.insert( result, entry ) + end + + return stdnse.format_output( true, result ) + +end diff --git a/scripts/rpcinfo.nse b/scripts/rpcinfo.nse index 891537217..3441be042 100644 --- a/scripts/rpcinfo.nse +++ b/scripts/rpcinfo.nse @@ -4,122 +4,43 @@ Connects to portmapper and fetches a list of all registered programs. --- -- @output +-- PORT STATE SERVICE -- 111/tcp open rpcbind --- | rpcinfo: --- | 100000 2 111/udp rpcbind --- | 100005 1,2,3 705/udp mountd --- | 100003 2,3,4 2049/udp nfs --- | 100024 1 32769/udp status --- | 100021 1,3,4 32769/udp nlockmgr --- | 100000 2 111/tcp rpcbind --- | 100005 1,2,3 706/tcp mountd --- | 100003 2,3,4 2049/tcp nfs --- | 100024 1 50468/tcp status --- |_ 100021 1,3,4 50468/tcp nlockmgr +-- | rpcinfo: +-- | 100000 2 111/tcp rpcbind +-- | 100000 2 111/udp rpcbind +-- | 100003 2 2049/tcp nfs +-- | 100003 2 2049/udp nfs +-- | 100005 1,2 953/udp mountd +-- | 100005 1,2 956/tcp mountd +-- | 100024 1 55145/tcp status +-- |_ 100024 1 59421/udp status - -require "shortport" -require "datafiles" -require "bin" -require "bit" -require "tab" - -author = "Sven Klemm" +author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"default","safe","discovery"} +categories = {"discovery", "safe"} -portrule = shortport.port_or_service(111, "rpcbind") +require 'shortport' +require 'rpc' ---- Format a table of version for output. ---@param version_table table containing the versions . ---@return string with the formatted versions. -local format_version = function( version_table ) - table.sort( version_table ) - return table.concat( version_table, ',' ) -end +portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} ) action = function(host, port) - local socket = nmap.new_socket() - socket:set_timeout(1000) - local catch = function() socket:close() end - local try = nmap.new_try(catch) - local rpc_numbers = try(datafiles.parse_rpc()) - - try(socket:connect(host.ip, port.number)) - - -- build rpc dump call packet - local transaction_id = math.random(0x7FFFFFFF) - local request = bin.pack('>IIIIIIILL',0x80000028,transaction_id,0,2,100000,2,4,0,0) - try(socket:send(request)) - - local status, answer = socket:receive_bytes(1) - if not status then - stdnse.print_debug(1, "%s failed to receive a response from %s:%d with error: %s", - filename:match( "[\\/]([^\\/]+)\.nse$" ) or filename, - host.ip, port.number, - answer or "unknown") - socket:close() - return nil - end - - local _,offset,header,length,tx_id,msg_type,reply_state,accept_state,value,payload,last_fragment - last_fragment = false; offset = 1; payload = '' - - -- extract payload from answer and try to receive more packets if - -- RPC header with last_fragment set has not been received - -- If we can't get further packets don't stop but process what we - -- got so far. - while not last_fragment do - if offset > #answer then - local status, data = socket:receive_bytes(1) - if not status then break end - answer = answer .. data - end - offset,header = bin.unpack('>I',answer,offset) - last_fragment = bit.band( header, 0x80000000 ) ~= 0 - length = bit.band( header, 0x7FFFFFFF ) - payload = payload .. answer:sub( offset, offset + length - 1 ) - offset = offset + length - end - socket:close() - - offset,tx_id,msg_type,reply_state,_,_,accept_state = bin.unpack( '>IIIIII', payload ) - - -- transaction_id matches, message type reply, reply state accepted and accept state executed successfully - if tx_id == transaction_id and msg_type == 1 and reply_state == 0 and accept_state == 0 then - local dir = { udp = {}, tcp = {}} - local protocols = {[6]='tcp',[17]='udp'} - local prog, version, proto, port - offset, value = bin.unpack('>I',payload,offset) - while value == 1 and #payload - offset >= 19 do - offset,prog,version,proto,port,value = bin.unpack('>IIIII',payload,offset) - proto = protocols[proto] or tostring( proto ) - -- collect data in a table - dir[proto] = dir[proto] or {} - dir[proto][port] = dir[proto][port] or {} - dir[proto][port][prog] = dir[proto][port][prog] or {} - table.insert( dir[proto][port][prog], version ) - end - - -- format output - local output = tab.new(4) - for proto, o in pairs(dir) do - -- get list of all used ports - local ports = {} - for port,_ in pairs(o) do table.insert(ports, port) end - table.sort(ports) - - -- iterate over ports to produce output - for _, port in ipairs(ports) do - for prog, versions in pairs(o[port]) do - local name = rpc_numbers[prog] or '' - tab.addrow(output,prog,format_version(versions),('%5d/%s'):format(port,proto),name) - end - end - end - return ' \n' .. tab.dump( output ) - - end + local result = {} + local status, rpcinfo = rpc.Helper.RpcInfo( host, port ) + + if ( not(status) ) then + return + end + + for progid, v in pairs(rpcinfo) do + for proto, v2 in pairs(v) do + table.insert( result, ("%-7d %-10s %5d/%s %s"):format(progid, stdnse.strjoin(",", v2.version), v2.port, proto, rpc.Util.ProgNumberToName(progid) ) ) + end + end + + table.sort(result) + return stdnse.format_output( true, result ) + end - diff --git a/scripts/script.db b/scripts/script.db index b0fa53fd2..7bd4ddb53 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -1,3 +1,5 @@ +Entry { filename = "afp-path-exploit.nse", categories = { "safe", "vuln", } } +Entry { filename = "afp-path-vuln.nse", categories = { "safe", "vuln", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } @@ -57,7 +59,10 @@ Entry { filename = "mysql-info.nse", categories = { "default", "discovery", "saf Entry { filename = "mysql-users.nse", categories = { "discovery", "intrusive", } } Entry { filename = "mysql-variables.nse", categories = { "discovery", "intrusive", } } Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "nfs-acls.nse", categories = { "discovery", "safe", } } +Entry { filename = "nfs-dirlist.nse", categories = { "discovery", "safe", } } Entry { filename = "nfs-showmount.nse", categories = { "discovery", "safe", } } +Entry { filename = "nfs-statfs.nse", categories = { "discovery", "safe", } } Entry { filename = "ntp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "oracle-sid-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "p2p-conficker.nse", categories = { "default", "safe", } } @@ -68,7 +73,7 @@ Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery Entry { filename = "pptp-version.nse", categories = { "version", } } Entry { filename = "realvnc-auth-bypass.nse", categories = { "default", "safe", "vuln", } } Entry { filename = "robots.txt.nse", categories = { "default", "discovery", "safe", } } -Entry { filename = "rpcinfo.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "rpcinfo.nse", categories = { "discovery", "safe", } } Entry { filename = "skypev2-version.nse", categories = { "version", } } Entry { filename = "smb-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }