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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
|
---
-- Read and parse some of Nmap's data files: <code>nmap-protocols</code>,
-- <code>nmap-rpc</code>, <code>nmap-services</code>, and
-- <code>nmap-mac-prefixes</code>.
--
-- The functions in this module return values appropriate for use with exception
-- handling via <code>nmap.new_try</code>. On success, they return true and
-- the function result. On failure, they return false and an error message.
-- @author Kris Katterjohn 03/2008
-- @author jah 08/2008
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
local io = require "io"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
-- mostly undocumented library for direct lookups in Nmap datafiles:
local nmapdb = require "nmapdb"
_ENV = stdnse.module("datafiles", stdnse.seeall)
---
-- Capture patterns for common data files, indexed by filename.
-- @class table
-- @name common_files
-- @see parse_file
local common_files = {
["nmap-rpc"] = { [function(ln) return tonumber( ln:match( "^%s*[^%s#]+%s+(%d+)" ) ) end] = "^%s*([^%s#]+)%s+%d+" },
["nmap-protocols"] = { [function(ln) return tonumber( ln:match( "^%s*[^%s#]+%s+(%d+)" ) ) end] = "^%s*([^%s#]+)%s+%d+" },
["nmap-services"] = { ["tcp"] = { [function(ln) return tonumber( ln:match( "^%s*[^%s#]+%s+(%d+)/tcp" ) ) end] = "^%s*([^%s#]+)%s+%d+/tcp" },
["udp"] = { [function(ln) return tonumber( ln:match( "^%s*[^%s#]+%s+(%d+)/udp" ) ) end] = "^%s*([^%s#]+)%s+%d+/udp" }
},
["nmap-mac-prefixes"] = { [ "^%s*(%w+)%s+[^#]+" ] = "^%s*%w+%s+([^#]+)" }
}
-- Helper for parse_* functions
local parse_and_cache = function(filename)
nmap.registry.datafiles = nmap.registry.datafiles or {}
if not nmap.registry.datafiles[filename] then
local status
status, nmap.registry.datafiles[filename] = parse_file(filename)
if not status then
return false, string.format("Error parsing %s", filename)
end
end
return true, nmap.registry.datafiles[filename]
end
---
-- Read and parse <code>nmap-protocols</code>.
--
-- On success, return true and a table mapping protocol numbers to protocol
-- names.
-- @return Status (true or false).
-- @return Table (if status is true) or error string (if status is false).
-- @see parse_file
parse_protocols = function()
return parse_and_cache("nmap-protocols")
end
---
-- Read and parse <code>nmap-rpc</code>.
--
-- On success, return true and a table mapping RPC numbers to RPC names.
-- @return Status (true or false).
-- @return Table (if status is true) or error string (if status is false).
-- @see parse_file
parse_rpc = function()
return parse_and_cache("nmap-rpc")
end
local prohibited = function()
error("Invalid function")
end
local services_table = {}
local portlookup_mt = {
__index = function(t, port)
return nmapdb.getservbyport(port, rawget(t, "proto"))
end,
__newindex = prohibited,
}
for _, proto in ipairs({"tcp", "udp", "sctp"}) do
services_table[proto] = setmetatable({proto=proto}, portlookup_mt)
end
---
-- Read and parse <code>nmap-services</code>.
--
-- On success, return true and a table containing subtables indexed by the
-- keys "tcp", "udp", and "sctp". You can
-- pass a protocol name as an argument to <code>parse_services</code> to get
-- only one of the results tables.
-- @param protocol Optional: The protocol table to return (e.g. <code>"tcp"</code> or
-- <code>"udp"</code>).
-- @return Status (true or false).
-- @return Table (if status is true) or error string (if status is false).
-- @see parse_file
parse_services = function(protocol)
local t
if protocol then
t = services_table[protocol]
if not t then
return false, "Bad protocol for nmap-services"
end
else
t = services_table
end
return true, t
end
local mac_table = setmetatable({}, {
__index = function(t, mac)
if #mac < 6 then
-- probably binary
mac = mac .. ("\0"):rep(6 - #mac)
elseif #mac < 12 then
-- probably hex
mac = mac .. ("0"):rep(12 - #mac)
end
return nmapdb.mac2corp(mac)
end,
__newindex = prohibited,
})
---
-- Read and parse <code>nmap-mac-prefixes</code>.
--
-- On success, return true and a table mapping MAC prefixes to manufacturer
-- names. The whole MAC can also be used as a key, since the table calls an
-- internal Nmap function to do the lookup.
-- @return Status (true or false).
-- @return Table (if status is true) or error string (if status is false).
-- @see parse_file
parse_mac_prefixes = function()
return true, mac_table
end
---
-- Read and parse a generic data file. The other parse functions are
-- defined in terms of this one.
--
-- If filename is a key in <code>common_files</code>, use the corresponding
-- capture pattern. Otherwise the second argument must be a table of the kind
-- taken by <code>parse_lines</code>.
-- @param filename Name of the file to parse.
-- @param ... A table of capture patterns.
-- @return Boolean status, false on failure
-- @return A table whose structure mirrors that of the capture table,
-- filled in with captured values.
function parse_file(filename, ...)
local data_struct
-- must have a filename
if type( filename ) ~= "string" or filename == "" then
return false, "Error in datafiles.parse_file: No file to parse."
end
-- is filename a member of common_files? is second parameter a key in common_files or is it a table?
if common_files[filename] and type( (...) ) == "string" and common_files[filename][(...)] then
data_struct = { common_files[filename][(...)] }
elseif common_files[filename] and select("#", ...) == 0 then
data_struct = { common_files[filename] }
elseif type( (...) ) == "table" then
data_struct = {...}
elseif type( (...) ) ~= "table" then
return false, "Error in datafiles.parse_file: Expected second parameter as table."
end
if type( data_struct ) == "table" then
for i, struc in ipairs( data_struct ) do
-- check that all varargs are tables
if type( struc ) ~= "table" then return false, "Error in datafiles.parse_file: Bad Parameter." end
-- allow empty table as sugar for ^(.+)$ capture the whole line
if not next( struc ) and #struc == 0 then data_struct[i] = { "^(.+)$" } end
end
if #data_struct == 0 then
return false, "Error in datafiles.parse_file: I've no idea how you want your data."
end
end
-- get a table of lines
local status, lines = read_from_file( filename )
if not status then
return false, ( "Error in datafiles.parse_file: %s could not be read: %s." ):format( filename, lines )
end
-- do the actual parsing
local ret = {}
for _, ds in ipairs( data_struct ) do
status, ret[#ret+1] = parse_lines( lines, ds )
-- hmmm should we fail all if there are any failures? yes? ok
if not status then return false, ret[#ret] end
end
return true, table.unpack( ret )
end
---
-- Generic parsing of an array of strings.
-- @param lines An array of strings to operate on.
-- @param data_struct A table containing capture patterns to be applied
-- to each string in the array. A capture will be applied to each string
-- using <code>string.match</code> and may also be enclosed within a table or
-- a function. If a function, it must accept a string as its parameter and
-- should return one value derived from that string.
-- @return A table whose structure mirrors that of the capture table,
-- filled in with captured values.
function parse_lines(lines, data_struct)
if type( lines ) ~= "table" or #lines < 1 then
return false, "Error in datafiles.parse_lines: No lines to parse."
end
if type( data_struct ) ~= "table" or not next( data_struct ) then
return false, "Error in datafiles.parse_lines: Expected second parameter as a non-empty table."
end
local ret = {}
-- traverse data_struct and enforce sensible index-value pairs. Call functions to process the members of lines.
for index, value in pairs( data_struct ) do
if type(index) == nil then return false, "Error in datafiles.parse_lines: Invalid index." end
if type(index) == "number" or type(value) == "table" then
if type(value) == "number" then
return false, "Error in datafiles.parse_lines: No patterns for data capture."
elseif type(value) == "string" or type(value) == "function" then
ret = get_array( lines, value )
elseif type(value) == "table" then
local _
_, ret[index] = parse_lines( lines, value )
else
-- TEMP
stdnse.debug1("Error in datafiles.parse_lines: Index with type %s has unexpected value %s", type(index), type(value))
end
elseif type(index) == "string" or type(index) == "function" then
if type( value ) == "string" or type( value ) == "function" then
ret = get_assoc_array( lines, index, value )
else
return false, ( "Error in datafiles.parse_lines: Invalid value for index %s." ):format( index )
end
else
-- TEMP
stdnse.debug1("Error in datafiles.parse_lines: Index with type %s has unexpected value %s", type(index), type(value))
end
end
return true, ret
end
---
-- Read a file, line by line, into a table.
-- @param file String with the name of the file to read.
-- @return Status (true or false).
-- @return Array of lines read from the file (if status is true) or error
-- message (if status is false).
function read_from_file( file )
-- get path to file
local filepath = nmap.fetchfile( file )
if not filepath then
return false, ( "Error in nmap.fetchfile: Could not find file %s." ):format( file )
end
local f, err, _ = io.open( filepath, "r" )
if not f then
return false, ( "Error in datafiles.read_from_file: Cannot open %s for reading: %s" ):format( filepath, err )
end
local ret = {}
for line in f:lines() do
ret[#ret+1] = line
end
f:close()
return true, ret
end
---
-- Return an array-like table of values captured from each line.
-- @param lines Table of strings containing the lines to process.
-- @param v_pattern Pattern to use on the lines to produce the value for the
-- array.
get_array = function(lines, v_pattern)
local ret = {}
for _, line in ipairs( lines ) do
assert( type( line ) == "string" )
local captured
if type( v_pattern ) == "function" then
captured = v_pattern( line )
else
captured = line:match( v_pattern )
end
table.insert( ret, captured )
end
return ret
end
---
-- Return a table of index-value pairs captured from each line.
-- @param lines Table of strings containing the lines to process.
-- @param i_pattern Pattern to use on the lines to produce the key for the
-- associative array.
-- @param v_pattern Pattern to use on the lines to produce the value for the
-- associative array.
get_assoc_array = function(lines, i_pattern, v_pattern)
local ret = {}
for _, line in ipairs(lines) do
assert( type( line ) == "string" )
local index
if type(i_pattern) == "function" then
index = i_pattern(line)
else
index = line:match(i_pattern)
end
if index and type(v_pattern) == "function" then
local m = v_pattern(line)
if m then ret[index] = m end
elseif index then
local m = line:match(v_pattern)
if m then ret[index] = m end
end
end
return ret
end
return _ENV;
|