mirror of
https://github.com/nmap/nmap.git
synced 2025-12-07 21:21:31 +00:00
Added rpc-grind.nse
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
261
scripts/rpc-grind.nse
Normal file
261
scripts/rpc-grind.nse
Normal file
@@ -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 <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 (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
|
||||
@@ -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", } }
|
||||
|
||||
Reference in New Issue
Block a user