Lucene search

K
nmapHani BenhabilesNMAP:RPC-GRIND.NSE
HistoryAug 17, 2012 - 6:44 p.m.

rpc-grind NSE Script

2012-08-1718:44:35
Hani Benhabiles
nmap.org
205

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Fingerprints the target RPC port to extract the target service, RPC number and version.

The script works by sending RPC Null call requests with a random high version unsupported number to the target service with iterated over RPC program numbers from the nmap-rpc file and check for replies from the target port. A reply with a RPC accept state 2 (Remote can’t support version) means that we the request sent the matching program number, and we proceed to extract the supported versions. A reply with an accept state RPC accept state 1 (remote hasn’t exported program) means that we have sent the incorrect program number. Any other accept state is an incorrect behaviour.

See also:

Script Arguments

rpc-grind.threads

Number of grinding threads. Defaults to 4

mount.version, nfs.version, rpc.protocol

See the documentation for the rpc library.

Example Usage

nmap -sV <target>
nmap --script rpc-grind <target>
nmap --script rpc-grind --script-args 'rpc-grind.threads=8' -p <targetport>
<target>

Script Output

PORT      STATE SERVICE VERSION
53344/udp open  walld   1 (RPC #100008)

Requires


local stdnse = require "stdnse"
local string = require "string"
local nmap = require "nmap"
local rpc = require "rpc"
local math = require "math"
local io = require "io"
local coroutine = require "coroutine"
local table = require "table"

description = [[
Fingerprints the target RPC port to extract the target service, RPC number and version.

The script works by sending RPC Null call requests with a random high version
unsupported number to the target service with iterated over RPC program numbers
from the nmap-rpc file and check for replies from the target port.
A reply with a RPC accept state 2 (Remote can't support version) means that we
the request sent the matching program number, and we proceed to extract the
supported versions. A reply with an accept state RPC accept state 1 (remote
hasn't exported program) means that we have sent the incorrect program number.
Any other accept state is an incorrect behaviour.
]]

---
-- @args rpc-grind.threads Number of grinding threads. Defaults to <code>4</code>
--
-- @usage
-- nmap -sV <target>
-- nmap --script rpc-grind <target>
-- nmap --script rpc-grind --script-args 'rpc-grind.threads=8' -p <targetport>
-- <target>
--
--@output
--PORT      STATE SERVICE VERSION
--53344/udp open  walld   1 (RPC #100008)
--
-- @see rpcinfo.nse


author = "Hani Benhabiles"

license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"version"}

-- Depend on rpcinfo so we don't grind something that's already known.
dependencies = {"rpcinfo"}

portrule = function(host, port)
  -- Do not run for excluded ports
  if (nmap.port_is_excluded(port.number, port.protocol)) then
    return false
  end
  if port.service ~= nil and port.version.service_dtype ~= "table" and port.service ~= 'rpcbind' then
    -- Exclude services that have already been detected as something
    -- different than rpcbind.
    return false
  end
  return nmap.version_intensity() >= 7
end

--- Function that determines if the target port of host uses RPC protocol.
--@param host Host table as commonly used in Nmap.
--@param port Port table as commonly used in Nmap.
--@return status boolean True if target port uses RPC protocol, false else.
local isRPC = function(host, port)
  -- If rpcbind is already set up by -sV
  -- which does practically the same check as in the "else" part.
  -- The nmap-services-probe entry "rpcbind" is not correctly true, and should
  -- be changed to something like "sunrpc"
  if port.service == 'rpcbind' then
    return true
  else
    -- this check is important if we didn't run the scan with -sV.
    -- If we run the scan with -sV, this check shouldn't return true as it is pretty much similar
    -- to the "rpcbind" service probe in nmap-service-probes.
    local rpcConn, status, err, data, rxid, msgtype, _

    -- Create new socket
    -- rpcbind is not really important, we could have used another protocol from rpc.lua
    -- such as nfs or mountd. Same thing for version 2.
    rpcConn = rpc.Comm:new("rpcbind", 2)
    status, err = rpcConn:Connect(host, port)
    if not status then
      stdnse.debug1("%s", err)
      return
    end

    -- Send packet
    local xid = math.random(1234567890)
    data = rpcConn:EncodePacket(xid)
    status, err = rpcConn:SendPacket(data)
    if not status then
      stdnse.debug1("SendPacket(): %s", err)
      return
    end

    -- And check response
    status, data = rpcConn:ReceivePacket()
    if not status then
      stdnse.debug1("isRPC didn't receive response.")
      return
    else
      -- If we got response, set port to open
      nmap.set_port_state(host, port, "open")

      rxid, msgtype = string.unpack(">I4 I4", data)
      -- If response XID does match request XID
      -- and message type equals 1 (REPLY) then
      -- it is a RPC port.
      if rxid == xid and msgtype == 1 then
        return true
      end
    end
  end
  stdnse.debug1("RPC checking function response data is not RPC.")
end

-- Function that iterates over the nmap-rpc file and
-- returns program name and number pairs.
-- @return name Name of the RPC service.
-- @return number RPC number of the matching service name.
local rpcIterator = function()
  -- Check if nmap-rpc file is present.
  local path = nmap.fetchfile("nmap-rpc")
  if not path then
    stdnse.debug1("Could not find nmap-rpc file.")
    return false
  end

  -- And is readable
  local nmaprpc, _, _ = io.open( path, "r" )
  if not nmaprpc then
    stdnse.debug1("Could not open nmap-rpc for reading.")
    return false
  end

  return function()
    while true do
      local line = nmaprpc:read()
      if not line then
        break
      end
      -- Now, we parse lines for meaningful ones
      local name, number = line:match("^%s*([^%s#]+)%s+(%d+)")
      -- And return program name and number
      if name and number then
        return name, tonumber(number)
      end
    end
  end
end

--- Function that sends RPC null commands with a random version number and
-- iterated over program numbers and checks the response for a sign that the
-- sent program number is the matching one for the target service.
-- @param host Host table as commonly used in Nmap.
-- @param port Port table as commonly used in Nmap.
-- @param iterator Iterator function that returns program name and number pairs.
-- @param result table to put result into.
local rpcGrinder = function(host, port, iterator, result)
  local condvar = nmap.condvar(result)
  local rpcConn, version, xid, status, response, packet, err, data, _

  xid = math.random(123456789)
  -- We use a random, most likely unsupported version so that
  -- we also trigger min and max version disclosure for the target service.
  version = math.random(12345, 123456789)
  rpcConn = rpc.Comm:new("rpcbind", version)
  rpcConn:SetCheckProgVer(false)
  status, err = rpcConn:Connect(host, port)

  if not status then
    stdnse.debug1("Connect(): %s", err)
    condvar "signal";
    return
  end
  for program, number in iterator do
    -- No need to continue further if we found the matching service.
    if #result > 0 then
      break
    end

    xid = xid + 1 -- XiD increased by 1 each time (from old RPC grind) <= Any important reason for that?
    rpcConn:SetProgID(number)
    packet = rpcConn:EncodePacket(xid)
    status, err = rpcConn:SendPacket(packet)
    if not status then
      stdnse.debug1("SendPacket(): %s", err)
      condvar "signal";
      return
    end

    status, data = rpcConn:ReceivePacket()
    if not status then
      stdnse.debug1("ReceivePacket(): %s", data)
      condvar "signal";
      return
    end

    _,response = rpcConn:DecodeHeader(data, 1)
    if type(response) == 'table' then
      if xid ~= response.xid then
        -- Shouldn't happen.
        stdnse.debug1("XID mismatch.")
      end
      -- Look at accept state
      -- Not supported version means that we used the right program number
      if response.accept_state == rpc.Portmap.AcceptState.PROG_MISMATCH then
        result.program = program
        result.number = number
        result.lowver, result.highver = string.unpack(">I4 I4", data, #data - 7)
        table.insert(result, true) -- To make #result > 1

        -- Otherwise, an Accept state other than Program unavailable is not normal behaviour.
      elseif response.accept_state ~= rpc.Portmap.AcceptState.PROG_UNAVAIL then
        stdnse.debug1("returned %s accept state for %s program number.", response.accept_state, number)
      end
    end
  end
  condvar "signal";
  return result
end

action = function(host, port)
  local result, lthreads = {}, {}

  if not isRPC(host, port) then
    stdnse.debug1("Target port %s is not a RPC port.", port.number)
    return
  end
  local threads = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".threads")) or 4

  local iterator = rpcIterator()
  if not iterator then
    return
  end
  -- And now, exec our grinder
  for i = 1,threads do
    local co = stdnse.new_thread(rpcGrinder, host, port, iterator, result)
    lthreads[co] = true
  end

  local condvar = nmap.condvar(result)
  repeat
    for thread in pairs(lthreads) do
      if coroutine.status(thread) == "dead" then
        lthreads[thread] = nil
      end
    end
    if ( next(lthreads) ) then
      condvar "wait";
    end
  until next(lthreads) == nil;

  -- Check the result and set the port version.
  if #result > 0 then
    port.version.name = result.program
    port.version.extrainfo = "RPC #" .. result.number
    if result.highver ~= result.lowver then
      port.version.version = ("%s-%s"):format(result.lowver, result.highver)
    else
      port.version.version = result.highver
    end
    nmap.set_port_version(host, port, "hardmatched")
  else
    stdnse.debug1("Couldn't determine the target RPC service. Running a service not in nmap-rpc ?")
  end
  return nil
end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Related for NMAP:RPC-GRIND.NSE