diff --git a/CHANGELOG b/CHANGELOG
index d4fa7f480..894c5c20b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,11 @@
# Nmap Changelog ($Id$); -*-text-*-
+o Removed pos_scan scan engine as the old implementation of RPC grind was the
+ last scan type to use it. [Hani Benhabiles]
+
+o [NSE] Replaced old rpc grind implementation with a new NSE based implementation
+ for easier maintainability and improved performance. [Hani Benhabiles]
+
o [NSE] Added broadcast-pim-discovery script which discovers routers that are
running PIM (Protocol Independant Multicast). [Hani Benhabiles]
diff --git a/scripts/rpc-grind.nse b/scripts/rpc-grind.nse
new file mode 100644
index 000000000..70137041b
--- /dev/null
+++ b/scripts/rpc-grind.nse
@@ -0,0 +1,261 @@
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+local rpc = require "rpc"
+local datafiles = require "datafiles"
+local bin = require "bin"
+local math = require "math"
+local io = require "io"
+
+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 4
+--
+-- @usage
+-- nmap -sV
+-- nmap --script rpc-grind
+-- nmap --script rpc-grind --script-args 'rpc-grind.threads=8' -p
+--
+--
+--@output
+--PORT STATE SERVICE VERSION
+--53344/udp open walld (walld V1) 1 (RPC #100008)
+--
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+
+categories = {"version"}
+
+
+portrule = function(host, port)
+ if port.service ~= nil and port.service ~= 'rpcbind' then
+ -- Exclude services that have already been detected as something
+ -- different than rpcbind.
+ return false
+ end
+ return true
+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.print_debug("%s: %s", SCRIPT_NAME, err)
+ return
+ end
+
+ -- Send packet
+ local xid = math.random(1234567890)
+ data = rpcConn:EncodePacket(xid)
+ status, err = rpcConn:SendPacket(data)
+ if not status then
+ stdnse.print_debug("%s SendPacket(): %s", SCRIPT_NAME, err)
+ return
+ end
+
+ -- And check response
+ _, data = rpcConn:ReceivePacket()
+ if not data then
+ stdnse.print_debug("%s: isRPC didn't receive response.", SCRIPT_NAME)
+ return
+ else
+ -- If we got response, set port to open
+ nmap.set_port_state(host, port, "open")
+
+ _, rxid = bin.unpack(">I", data, 1)
+ _, msgtype = bin.unpack(">I", data, 5)
+ -- 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.print_debug("%s: RPC checking function response data is not RPC.", SCRIPT_NAME)
+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.print_debug("%s: Could not find nmap-rpc file.", SCRIPT_NAME)
+ return false
+ end
+
+ -- And is readable
+ local nmaprpc, _, _ = io.open( path, "r" )
+ if not nmaprpc then
+ stdnse.print_debug("%s: Could not open nmap-rpc for reading.", SCRIPT_NAME)
+ 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()
+ rpcConn:SetCheckProgVer(false)
+ rpcConn:SetVersion(version)
+ status, err = rpcConn:Connect(host, port)
+
+ if not status then
+ stdnse.print_debug("%s Connect(): %s", SCRIPT_NAME, 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.print_debug("%s SendPacket(): %s", SCRIPT_NAME, err)
+ condvar "signal";
+ return
+ end
+
+ status, data = rpcConn:ReceivePacket()
+ if not status then
+ stdnse.print_debug("%s ReceivePacket(): %s", SCRIPT_NAME, err)
+ condvar "signal";
+ return
+ end
+
+ _,response = rpcConn:DecodeHeader(data, 1)
+ if type(response) == 'table' then
+ if xid ~= response.xid then
+ -- Shouldn't happen.
+ stdnse.print_debug("%s: XID mismtach.", SCRIPT_NAME)
+ 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.highver = bin.unpack(">I", data, #data - 3)
+ _, result.lowver = bin.unpack(">I", 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.print_debug("%s: returned %s accept state for %s program number.",SCRIPT_NAME, 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.print_debug("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
+ condvar "wait";
+ for thread in pairs(lthreads) do
+ if coroutine.status(thread) == "dead" then
+ lthreads[thread] = nil
+ end
+ 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.print_debug("Couldn't determine the target RPC service. Running a service not in nmap-rpc ?")
+ end
+ return nil
+end
diff --git a/scripts/script.db b/scripts/script.db
index 1d92b0427..dacd57993 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -325,6 +325,7 @@ Entry { filename = "riak-http-info.nse", categories = { "discovery", "safe", } }
Entry { filename = "rlogin-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "rmi-dumpregistry.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "rmi-vuln-classloader.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "rpc-grind.nse", categories = { "version", } }
Entry { filename = "rpcap-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "rpcap-info.nse", categories = { "discovery", "safe", } }
Entry { filename = "rpcinfo.nse", categories = { "default", "discovery", "safe", } }