1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
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
|