1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-09 06:01:28 +00:00
Files
nmap/nselib/rpc.lua
david dc03a70c79 Fix some NSEDoc. Some libraries had @copyright and @author right at the
top of the first comment, so the entire description got stuffed into the
@author field. I also discovered a limitation in the NSEDoc parser: the
first non-empty line following the first --- comment must be the
"module" call, or else the block isn't recognized as belonging to a
module. This was preventing @args from appearing in certain libraries.
Djalal Harouni told me about this.
2010-04-15 19:21:13 +00:00

1683 lines
49 KiB
Lua

---
-- RPC Library supporting a very limited subset of operations
--
-- 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:
--
-- <code>
-- -- 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
-- </code>
--
-- 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.
--
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
--
-- @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")
module(... or "rpc", package.seeall)
require("datafiles")
-- Version 0.3
--
-- Created 01/24/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- 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
--
-- 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 <code>needed</code> 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 <code>xid</code>, <code>type</code>, <code>state</code>,
-- <code>verifier</code> and <code>accept_state</code>
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() self.socket:close() end
local try = nmap.new_try(catch)
local msg_type = 0
local prg_mount = Util.ProgNameToNumber("mountd")
local packet
local pos = 1
local header = {}
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() self.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() self.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 <code>attributes</code>
-- and <code>entries</code>. The attributes entry is only present when
-- using NFS version 3. The <code>entries</code> field contain one
-- table for each file/directory entry. It has the following fields
-- <code>file_id</code>, <code>name</code> and <code>cookie</code>
--
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 <code>decodeReadDir</code>
ReadDir = function( self, file_handle )
local status, packet
local cookie, count = 0, 8192
local pos, data, _ = 1, "", ""
local header, response = {}, {}
if ( not(file_handle) ) then
return false, "No 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 <code>transfer_size</code>, <code>block_size</code>,
-- <code>total_blocks</code>, <code>free_blocks</code> and <code>available_blocks</code>
-- @return 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: <code>type</code>, <code>mode</code>,
-- <code>nlink</code>, <code>uid</code>, <code>gid</code>, <code>size</code>,
-- <code>blocksize</code>, <code>rdev</code>, <code>blocks</code>, <code>fsid</code>,
-- <code>fileid</code>, <code>atime</code>, <code>mtime</code> and <code>ctime</code>
--
GetAttrDecode = function( self, data, pos )
local attrib = {}
local catch = function() self.socket:close() end
local try = nmap.new_try(catch)
local NFS_OK = 0
local status
status, data = self.comm:GetAdditionalBytes( data, pos, 4 )
if ( not(status) ) then
return false, "GetAttrDecode: GetAdditionalBytes failed"
end
pos, attrib.status = bin.unpack(">I", data, pos)
if attrib.status ~= NFS_OK then
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 <code>type</code>, <code>mode</code>,
-- <code>nlink</code>, <code>uid</code>, <code>gid</code>, <code>size</code>,
-- <code>blocksize</code>, <code>rdev</code>, <code>blocks</code>, <code>fsid</code>,
-- <code>fileid</code>, <code>atime</code>, <code>mtime</code> and <code>ctime</code>
-- @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: <code>transfer_size</code>, <code>block_size</code>,
-- <code>total_blocks</code>, <code>free_blocks</code> and <code>available_blocks</code>
--
StatFsDecode = function( self, data, pos )
local catch = function() self.socket:close() end
local try = nmap.new_try(catch)
local statfs = {}
local NFS_OK, NSFERR_ACCESS = 0, 13
data = try( self.comm:GetAdditionalBytes( data, pos, 4 ) )
pos, statfs.status = bin.unpack(">I", data, pos)
if statfs.status ~= NFS_OK then
if statfs.status == NSFERR_ACCESS then
stdnse.print_debug("STATFS query received NSFERR_ACCESS")
end
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 <code>transfer_size</code>, <code>block_size</code>,
-- <code>total_blocks</code>, <code>free_blocks</code> and <code>available_blocks</code>
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 <code>decodeReadDir</code>
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 <code>transfer_size</code>, <code>block_size</code>,
-- <code>total_blocks</code>, <code>free_blocks</code> and <code>available_blocks</code>
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
-- <code>Portmap.Dump</code>
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 protocol string containing either "tcp" or "udp"
-- @return status true on success, false on failure
-- @return table containing the portmapper information as returned by
-- <code>Portmap.Dump</code>
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 <code>port</code>, <code>port.number</code>
-- <code>port.protocol</code> and <code>version</code>
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:
--
-- <code>
-- table[program_id][protocol]["port"] = <port number>
-- table[program_id][protocol]["version"] = <table of versions>
-- </code>
--
-- 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 <code>mnt.GetAttr</code>
-- 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
}