-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathwhois.nse
2193 lines (1746 loc) · 88.3 KB
/
whois.nse
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local http = require "http"
local io = require "io"
local ipOps = require "ipOps"
local math = require "math"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Queries the WHOIS services of Regional Internet Registries (RIR) and attempts to retrieve information about the IP Address
Assignment which contains the Target IP Address.
The fields displayed contain information about the assignment and the organisation responsible for managing the address
space. When output verbosity is requested on the Nmap command line (<code>-v</code>) extra information about the assignment will
be displayed.
To determine which of the RIRs to query for a given Target IP Address this script utilises Assignments Data hosted by IANA.
The data is cached locally and then parsed for use as a lookup table. The locally cached files are refreshed periodically
to help ensure the data is current. If, for any reason, these files are not available to the script then a default sequence
of Whois services are queried in turn until: the desired record is found; or a referral to another (defined) Whois service is
found; or until the sequence is exhausted without finding either a referral or the desired record.
The script will recognize a referral to another Whois service if that service is defined in the script and will continue by
sending a query to the referred service. A record is assumed to be the desired one if it does not contain a referral.
To reduce the number unnecessary queries sent to Whois services a record cache is employed and the entries in the cache can be
applied to any targets within the range of addresses represented in the record.
In certain circumstances, the ability to cache responses prevents the discovery of other, smaller IP address assignments
applicable to the target because a cached response is accepted in preference to sending a Whois query. When it is important
to ensure that the most accurate information about the IP address assignment is retrieved the script argument <code>whodb</code>
should be used with a value of <code>"nocache"</code> (see script arguments). This reduces the range of addresses that may use a
cached record to a size that helps ensure that smaller assignments will be discovered. This option should be used with caution
due to the potential to send large numbers of whois queries and possibly be banned from using the services.
In using this script your IP address will be sent to iana.org. Additionally
your address and the address of the target of the scan will be sent to one of
the RIRs.
]]
---
-- @args whodb Takes any of the following values, which may be combined:
-- * <code>whodb=nofile</code> Prevent the use of IANA assignments data and instead query the default services.
-- * <code>whodb=nofollow</code> Ignore referrals and instead display the first record obtained.
-- * <code>whodb=nocache</code> Prevent the acceptance of records in the cache when they apply to large ranges of addresses.
-- * <code>whodb=[service-ids]</code> Redefine the default services to query. Implies <code>nofile</code>.
-- @usage
-- # Basic usage:
-- nmap target --script whois
--
-- # To prevent the use of IANA assignments data supply the nofile value
-- # to the whodb argument:
-- nmap target --script whois --script-args whodb=nofile
-- nmap target --script whois --script-args whois.whodb=nofile
--
-- # Supplying a sequence of whois services will also prevent the use of
-- # IANA assignments data and override the default sequence:
-- nmap target --script whois --script-args whodb=arin+ripe+afrinic
-- nmap target --script whois --script-args whois.whodb=apnic*lacnic
-- # The order in which the services are supplied is the order in which
-- # they will be queried. (N.B. commas or semi-colons should not be
-- # used to delimit argument values.)
--
-- # To return the first record obtained even if it contains a referral
-- # to another service, supply the nofollow value to whodb:
-- nmap target --script whois --script-args whodb=nofollow
-- nmap target --script whois --script-args whois.whodb=nofollow+ripe
-- # Note that only one service (the first one supplied) will be used in
-- # conjunction with nofollow.
--
-- # To ensure discovery of smaller assignments even if larger ones
-- # exist in the cache, supply the nocache value to whodb:
-- nmap target --script whois --script-args whodb=nocache
-- nmap target --script whois --script-args whois.whodb=nocache
-- @output
-- Host script results:
-- | whois: Record found at whois.arin.net
-- | netrange: 64.13.134.0 - 64.13.134.63
-- | netname: NET-64-13-143-0-26
-- | orgname: Titan Networks
-- | orgid: INSEC
-- |_ country: US stateprov: CA
author = "jah"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "external", "safe"}
-------------------------------------------------------------------------------------------------------------------------
--
--
--
--
-- This script will run only if the target IP address has been determined to be routable on the Internet.
hostrule = function( host )
local is_private, err = ipOps.isPrivate( host.ip )
if is_private == nil then
stdnse.print_debug( "%s Error in Hostrule: %s.", SCRIPT_NAME, err )
return false
end
return not is_private
end
-------------------------------------------------------------------------------------------------------------------------
--
--
--
--
-- Queries WHOIS services until an applicable record is found or the list of services to query
-- is exhausted and finishes by displaying elements of an applicable record.
action = function( host )
if not nmap.registry.whois then
---
-- Data and flags shared between threads.
-- @name whois
-- @class table
--@field whoisdb_default_order The default number and order of whois services to query.
--@field using_local_assignments_file Set this to: false; to avoid using the data from IANA hosted assignments files (false when whodb=nofile).
--@field local_assignments_file_expiry A period, between 0 and 7 days, during which cached assignments data may be used without being refreshed.
--@field init_done Set when <code>script_init</code> has been called and prevents it being called again.
--@field mutex A table of mutex functions, one for each service defined herein. Allows a thread exclusive access to a
-- service, preventing concurrent connections to it.
--@field nofollow A flag that prevents referrals to other whois records and allows the first record retrieved to be
-- returned instead. Set to true when whodb=nofollow
--@field using_cache A flag which modifies the size of ranges in a cache entry. Set to false when whodb=nocache
--@field cache Storage for cached redirects, records and other data for output.
nmap.registry.whois = {}
nmap.registry.whois.whoisdb_default_order = {"arin","ripe","apnic"}
nmap.registry.whois.using_cache = true
nmap.registry.whois.using_local_assignments_file = true
nmap.registry.whois.local_assignments_file_expiry = "16h"
nmap.registry.whois.nofollow = false
nmap.registry.whois.cache = {}
end
-- script initialisation - threads must wait until this has been completed before continuing
local mutex = nmap.mutex( "whois" )
mutex "lock"
if not nmap.registry.whois.init_done then
script_init( host.ip )
end
mutex "done"
---
-- Holds field data captured from the responses of each service queried and includes additional information about the final desired record.
--
-- The table, indexed by whois service id, holds a table of fields captured from each queried service. Once it has been determined that a record
-- represents the final record we wish to output, the existing values are destroyed and replaced with the one required record. This is done purely
-- to make it easier to reference the data of a desired record. Other values in the table are as follows.
-- @name data
-- @class table
--@field data.iana is set after the table is initialised and is the number of times a response encountered represents "The Whole Address Space".
-- If the value reaches 2 it is assumed that a valid record is held at ARIN.
--@field data.id is set in <code>analyse_response</code> after final record and is the service name at which a valid record has been found. Used in
-- <code>format_data_for_output</code>.
--@field data.mirror is set in <code>analyse_response</code> after final record and is the service name from which a mirrored record has been found. Used in
-- <code>format_data_for_output</code>.
--@field data.comparison is set in <code>analyse_response</code> after final record and is a string concatenated from fields extracted from a record and which
-- serves as a fingerprint for a record, used in <code>get_cache_key</code>, to compare two records for equality.
local data = {}
data.iana = 0
---
-- Used in the main loop to manage mutexes, the structure of tracking is as follows.
-- @name tracking
-- @class table
--@field this_db The service for which a thread will wait for exclusive access before sending a query to it.
--@field next_db The next service to query. Allows a thread to continue in the main "while do" loop.
--@field last_db The value of this_db after sending a query, used when exclusive access to a service is no longer required.
--@field completed An array of services previously queried.
local tracking = {}
tracking.completed = {}
tracking = get_next_action( tracking, host.ip )
-- main loop
while tracking.next_db do
local status, retval
tracking.this_db, tracking.next_db = tracking.next_db, nil
nmap.registry.whois.mutex[tracking.this_db] "lock"
status, retval = pcall( get_next_action, tracking, host.ip )
if not status then
stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval )
else tracking = retval end
if tracking.this_db then
-- do query
local response = do_query( tracking.this_db, host.ip )
tracking.completed[#tracking.completed+1] = tracking.this_db
-- analyse data
status, retval = pcall( analyse_response, tracking, host.ip, response, data )
if not status then
stdnse.print_debug( "%s %s pcall caught an exception in analyse_response: %s.", SCRIPT_NAME, host.ip, retval )
else data = retval end
-- get next action
status, retval = pcall( get_next_action, tracking, host.ip )
if not status then
stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval )
if not tracking.last_db then tracking.last_db, tracking.this_db = tracking.this_db or tracking.next_db, nil end
else tracking = retval end
end
nmap.registry.whois.mutex[tracking.last_db] "done"
tracking.last_db = nil
end
return output( host.ip, tracking.completed )
end -- action
----------------------------------------------------------------------------------------------------------------------------
--
--
--
--
-- Determines whether or not to query a whois service and which one to query. Checks the cache first - where there may be a redirect or a
-- cached record. If not, it trys to get a service from the assignments files if this was not previously attempted. Finally, if a service has
-- not yet been obtained the first unqueried service from whoisdb_default_order is used. The tracking table is manipulated such that a thread
-- knows its next move in the main loop.
-- @param tracking The Tracking table.
-- @param ip String representing the Target's IP address.
-- @return The supplied and possibly modified tracking table.
-- @see tracking, check_response_cache, get_db_from_assignments
function get_next_action( tracking, ip )
if type( ip ) ~= "string" or ip == "" or type( tracking ) ~= "table" or type( tracking.completed ) ~= "table" then return nil end
--next_db should always be nil when calling this
if tracking.next_db then return tracking end
-- check for cached redirects and records
local in_cache
in_cache, tracking.next_db = check_response_cache( ip )
if in_cache and not tracking.next_db then
-- found cached data - quit
tracking.this_db, tracking.last_db = nil, tracking.this_db
return tracking
elseif in_cache and tracking.next_db then
-- found cached redirect
if tracking.next_db ~= tracking.this_db then
-- skip query to this_db and set last_db so we can unlock mutex
tracking.this_db, tracking.last_db = nil, tracking.this_db
else
-- we were already about to query this_db
tracking.next_db = nil
end
-- kill redirect if the user specified "nofollow"
if nmap.registry.whois.nofollow then tracking.next_db = nil end
return tracking
elseif not in_cache and tracking.this_db and table.concat( tracking.completed, " " ):match( tracking.this_db ) then
-- we've already queried this_db so lets skip it and try whoisdb_default_order
tracking.last_db, tracking.this_db = tracking.this_db, nil
end
-- try to find a service to query in the assignments files, if allowed
if nmap.registry.whois.using_local_assignments_file and not tracking.this_db and not tracking.last_db then
tracking.next_db = get_db_from_assignments( ip )
if tracking.next_db and not table.concat( tracking.completed, " " ):match( tracking.next_db ) then
-- we got one we haven't queried - we probably haven't queried any yet.
return tracking
end
end
-- get the next untried service from whoisdb_default_order
if not tracking.this_db and nmap.registry.whois.whoisdb_default_order then
for i, db in ipairs( nmap.registry.whois.whoisdb_default_order ) do
if not table.concat( tracking.completed, " " ):match( db ) then
tracking.next_db = db
break
end
end
end
return tracking
end
---
-- Checks the registry for cached redirects and results applicable to the supplied Target's IP address.
-- @param ip String representing the Target's IP address.
-- @return Boolean True if the supplied IP address is within a range of addresses for which there is a cache entry and a redirect or a
-- record is present; otherwise false.
-- @return ID of a service defined in whoisdb if a redirect is present; otherwise nil.
-- @see get_cache_key
function check_response_cache( ip )
if not next( nmap.registry.whois.cache ) then return false, nil end
if type( ip ) ~= "string" or ip == "" then return false, nil end
local ip_key = get_cache_key( ip )
if not ip_key then return false, nil end
local cache_data = nmap.registry.whois.cache[ip_key]
if cache_data.redirect then
-- redirect found in cache
return true, cache_data.redirect
elseif cache_data.data then
-- record found in cache
return true, nil
else
stdnse.print_debug( 1, "%s %s Error in check_response_cache: Empty Cache Entry was found.", SCRIPT_NAME, ip )
end
return false, nil
end
---
-- Determines which entry in the cache is applicable to the Target and returns the key for that entry.
-- @param ip String representing the Target's IP address.
-- @return String key (IP address) of the cache entry applicable to the Target.
function get_cache_key( ip )
-- if this ip cached an entry, then we'll use it except when it represents a found record and we're not using_cache
if nmap.registry.whois.cache[ip] and ( nmap.registry.whois.using_cache or nmap.registry.whois.cache[ip].redirect ) then
return ip
end
-- When not using_cache, we compare our record to any others in the cache to avoid printing out the same record repeatedly.
local self_compare
if nmap.registry.whois.cache[ip] and nmap.registry.whois.cache[ip].data then
-- we should have a string which we can use to compare with other records
self_compare = nmap.registry.whois.cache[ip].data.comparison
end
local cache_entries = {}
for ip_key, cache_data in pairs( nmap.registry.whois.cache ) do
if type( ip_key ) == "string" and ip_key ~= "" and type( cache_data ) == "table" then
-- compare and return original pointer
if self_compare and ip ~= ip_key and not cache_data.pointer and self_compare == cache_data.data.comparison then
nmap.registry.whois.cache[ip].pointer = ip_key
return ip_key
end
-- check if ip is in a cached range and add the entry to cache_entries if it is
local in_range, err = ipOps.ip_in_range( ip, cache_data.range )
if in_range then
local t = {}
t.key = ip_key
t.range = cache_data.range
t.pointer = cache_data.pointer
cache_entries[#cache_entries+1] = t
end
end
end
if #cache_entries == 0 then
-- no applicable cache entries
return nil
elseif #cache_entries == 1 then
-- just one applicable entry
return cache_entries[1].pointer or cache_entries[1].key
end
-- more than one entry need sorting into ascending order
table.sort( cache_entries, smallest_range )
-- we'll choose the smallest range
return cache_entries[1].key
end
---
-- Calculates the prefix length for the given assignment.
-- @param range String representing an IP address assignment
-- @return Number - prefix length of the assignment
function get_prefix_length( range )
if type( range ) ~= "string" or range == "" then return nil end
local first, last, err = ipOps.get_ips_from_range( range )
if err then return nil end
first = ipOps.ip_to_bin( first ):reverse()
last = ipOps.ip_to_bin( last ):reverse()
local hostbits = 0
for pos = 1, string.len( first ), 1 do
if first:sub( pos, pos ) == "0" and last:sub( pos, pos ) == "1" then
hostbits = hostbits + 1
else
break
end
end
return ( string.len( first ) - hostbits )
end
---
-- Performs a lookup against assignments data to determine which service to query for the supplied Target.
-- @param ip String representing the Target's IP address.
-- @return String id of the whois service to query, or nil.
function get_db_from_assignments( ip )
if type( ip ) ~= "string" or ip == "" then return nil end
local af
if ip:match( ":" ) then
af = "ipv6"
else
af = "ipv4"
end
if not nmap.registry.whois.local_assignments_data or not nmap.registry.whois.local_assignments_data[af] then
stdnse.print_debug( 1, "%s Error in get_db_from_assignments: Missing assignments data in registry.", SCRIPT_NAME )
return nil
end
if next( nmap.registry.whois.local_assignments_data[af] ) then
for _, assignment in ipairs( nmap.registry.whois.local_assignments_data[af] ) do
if ipOps.ip_in_range( ip, assignment.range.first .. "-" .. assignment.range.last ) then
return assignment.service
end
end
end
return nil
end
---
-- Connects to a whois service (usually TCP port 43) and sends an IP address query, returning any response.
-- @param db String id of a service defined in whoisdb.
-- @param ip String representing the Target's IP address.
-- @return String response to query or nil.
function do_query(db, ip)
if type( db ) ~= "string" or not nmap.registry.whois.whoisdb[db] then
stdnse.print_debug("%s %s Error in do_query: %s is not a defined Whois service.", SCRIPT_NAME, ip, db)
return nil
end
local service = nmap.registry.whois.whoisdb[db]
if type( service.hostname ) ~= "string" or service.hostname == "" then
stdnse.print_debug("%s %s Error in do_query: Invalid hostname for %s.", SCRIPT_NAME, ip, db)
return nil
end
local query_data = ""
if type( service.preflag ) == "string" and service.preflag ~= "" then
query_data = service.preflag .. " "
end
query_data = query_data .. ip
if type( service.postflag ) == "string" and service.postflag ~= "" then
query_data = query_data .. service.postflag
end
query_data = query_data .. "\n"
local socket = nmap.new_socket()
local catch = function()
stdnse.print_debug( "%s %s Connection to %s failed or was aborted! No Output for this Target.", SCRIPT_NAME, ip, db )
nmap.registry.whois.mutex[db] "done"
socket:close()
end
local result, status, line = {}
local try = nmap.new_try( catch )
socket:set_timeout( 10000 )
try( socket:connect( service.hostname, 43 ) )
try( socket:send( query_data ) )
while true do
local status, lines = socket:receive_lines(1)
if not status then
break
else
result[#result+1] = lines
end
end
socket:close()
stdnse.print_debug(3, "%s %s Ended Query at %s.", SCRIPT_NAME, ip, db)
if #result == 0 then
return nil
end
return table.concat( result )
end
---
-- Extracts fields (if present) from the information returned in response to our query and determines whether it represents a referral to a
-- record hosted elsewhere. The referral is cached in the registry to allow threads for targets in the same assignment to avoid performing
-- their queries to this service. If it is not a referral, we assume it is the desired record and the extracted fields are cached in the
-- registry ready for output.
-- @param tracking Tracking table.
-- @param ip String representing a Target's IP address.
-- @param response String obtained from a service in response to our query.
-- @param data Table of fields captured from previously queried services, indexed by service name.
-- @return The data table passed as a parameter which may have been added to or may contain only the fields extracted from the desired
-- record (in which case it will no longer be indexed by service name).
-- @see extract_objects_from_response, redirection_rules, constrain_response, add_to_cache
function analyse_response( tracking, ip, response, data )
if type( response ) ~= "string" or response == "" then return data end
local meta, mirrored_db
local last_db, this_db, next_db = tracking.last_db, (tracking.this_db or tracking.last_db), tracking.next_db
data[this_db] = {}
-- check for foreign resource
for _, db in pairs( nmap.registry.whois.whoisdb ) do
if type( db ) == "table" and type( db.id ) == "string" and db.id ~= "iana" and db.id ~= this_db and type( db.hostname ) == "string" then
local pattern = db.id:upper() .. ".*%s*resource:%s*" .. db.hostname
if response:match( pattern ) then
mirrored_db = db.id
meta = db
meta.redirects = nil
break
end
end
end
meta = meta or nmap.registry.whois.whoisdb[this_db]
-- do we recognize objects in the response?.
local have_objects
if type( meta ) == "table" and type( meta.fieldreq ) == "table" and type( meta.fieldreq.ob_exist ) == "string" then
have_objects = response:match( meta.fieldreq.ob_exist )
else
stdnse.print_debug( 2, "%s %s Could not check for objects, problem with meta data.", SCRIPT_NAME, ip )
have_objects = false
end
-- if we do not recognize objects check for an known error/non-object message
if not have_objects then
stdnse.print_debug( 4, "%s %s %s has not responded with the expected objects.", SCRIPT_NAME, ip, this_db )
local tmp, msg
-- may have found our record saying something similar to "No Record Found"
for _, pattern in ipairs( nmap.registry.whois.m_none ) do
local pattern_l = pattern:gsub( "$addr", ip:lower() )
local pattern_u = pattern:gsub( "$addr", ip:upper() )
msg = response:match( pattern_l ) or response:match( pattern_u )
if msg then
stdnse.print_debug( 4, "%s %s %s responded with a message which is assumed to be authoritative (but may not be).", SCRIPT_NAME, ip, this_db )
break
end
end
-- may have an error
if not msg then
for _, pattern in ipairs( nmap.registry.whois.m_err ) do
msg = response:match( pattern )
if msg then
stdnse.print_debug( 4, "%s %s %s responded with an ERROR message.", SCRIPT_NAME, ip, this_db )
break
end
end
end
-- if we've recognized a non-object message,
if msg then
add_to_cache( ip, nil, nil, "Message from " .. nmap.registry.whois.whoisdb[this_db].hostname .. "\n" .. msg )
return data
end
end
-- the query response may not contain the set of objects we were expecting and we do not recognize the response message.
-- it may contain a record mirrored (or found by recursion) from a different service
if not have_objects then
local foreign_obj
for setname, set in pairs( nmap.registry.whois.fields_meta ) do
if set ~= nmap.registry.whois.whoisdb[this_db].fieldreq and response:match(set.ob_exist) then
foreign_obj = setname
stdnse.print_debug( 4, "%s %s %s seems to have responded using the set of objects named: %s.", SCRIPT_NAME, ip, this_db, foreign_obj )
break
end
end
if foreign_obj and foreign_obj == "rpsl" then
mirrored_db = nmap.registry.whois.whoisdb.ripe.id
meta = nmap.registry.whois.whoisdb.ripe
meta.redirects = nil
have_objects = true
stdnse.print_debug( 4, "%s %s %s will use the display properties of ripe.", SCRIPT_NAME, ip, this_db )
elseif foreign_obj then
-- find a display to match the objects.
for some_db, db_props in pairs( nmap.registry.whois.whoisdb ) do
if db_props.fieldreq and nmap.registry.whois.fields_meta[foreign_obj] and db_props.fieldreq == nmap.registry.whois.fields_meta[foreign_obj] then
mirrored_db = nmap.registry.whois.whoisdb[some_db].id
meta = nmap.registry.whois.whoisdb[some_db]
meta.redirects = nil
have_objects = true
stdnse.print_debug( 4, "%s %s %s will use the display properties of %s.", SCRIPT_NAME, ip, this_db, some_db )
break
end
end
end -- if foreign_obj
end
-- extract fields from the entire response for record/redirect discovery
if have_objects then
stdnse.print_debug( 4, "%s %s Parsing Query response from %s.", SCRIPT_NAME, ip, this_db )
data[this_db] = extract_objects_from_response( response, this_db, ip, meta )
end
local response_chunk, found, nextdb
-- do record/redirect discovery, cache found redirect
if not nmap.registry.whois.nofollow and have_objects and meta.redirects then
stdnse.print_debug( 4, "%s %s Testing response for redirection.", SCRIPT_NAME, ip )
found, nextdb, data.iana = redirection_rules( this_db, ip, data, meta )
end
-- get most specific assignment and handle arin's organisation-focused record layout and then
-- modify the data table depending on whether we're redirecting or quitting
if have_objects then
stdnse.print_debug( 5, "%s %s Extracting Fields from response.", SCRIPT_NAME, ip )
-- optionally constrain response to a more focused area
-- discarding previous extraction
if meta.smallnet_rule then
local offset, ptr, strbgn, strend
response_chunk, offset = constrain_response( response, this_db, ip, meta )
if offset > 0 then
data[this_db] = extract_objects_from_response( response_chunk, this_db, ip, meta )
end
if offset > 1 and meta.unordered then
-- fetch an object immediately in front of inetnum
stdnse.print_debug( 5, "%s %s %s Searching for an object group immediately before this range.", SCRIPT_NAME, ip, this_db )
-- split objects from the record, up to offset. Last object should be the one we want.
local obj_sel = stdnse.strsplit( "\r?\n\r?\n", response:sub( 1, offset ) )
response_chunk = "\n" .. obj_sel[#obj_sel] .. "\n"
-- check if any of the objects we like match this single object in response chunk
for ob, t in pairs( meta.fieldreq ) do
if ob ~= "ob_exist" and type( t.ob_start ) == "string" and response_chunk:match( t.ob_start ) then
data[this_db][ob] = extract_objects_from_response( response_chunk, this_db, ip, meta, ob )
end
end
end -- if offset
end -- if meta.smallnet_rule
-- collect, from each extracted object, the tables of field values and positions and concatenate these
-- to provide the ability to easily compare two results
local coll, comp = {}, ""
for ob, t in pairs( data[this_db] ) do
for i, comp_string in pairs( t.for_compare ) do
coll[#coll+1] = { i, comp_string }
end
-- kill these now they're collected
data[this_db][ob].for_compare = nil
end
-- sort them by position in the record, ascending
table.sort( coll, function(a,b) return a[1]<b[1] end )
-- concatenate them to create a long string we can compare. Assign to .comparison after the debug bit following...
for i, v in ipairs( coll ) do
comp = comp .. v[2]
end
-- DEBUG
stdnse.print_debug( 6, "%s %s %s Fields captured :", SCRIPT_NAME, ip, this_db )
for ob, t in pairs( data[this_db] ) do
for fieldname, fieldvalue in pairs( t ) do
stdnse.print_debug( 6, "%s %s %s %s.%s %s.", SCRIPT_NAME, ip, this_db, ob, fieldname, fieldvalue )
end
end
-- add comparison string to extracted data
data[this_db].comparison = comp
-- add mirrored_db to extracted data
data[this_db].mirror = mirrored_db
-- If we are accepting a record, only cache the data for that record
if not nextdb or nmap.registry.whois.nofollow then
-- no redirect - accept as result and clear any previous data
data = data[this_db]
data.id = this_db
elseif nextdb and table.concat( tracking.completed, " " ):match( nextdb ) then
-- redirected to a previously queried service - accept as result
data = data[nextdb]
data.id = nextdb
nextdb = nil
elseif have_objects and ( data.iana > 1 ) and not table.concat( tracking.completed, " " ):match( nmap.registry.whois.whoisdb.arin.id ) then
-- two redirects to IANA - query ARIN next (which we should probably have done already!)
nextdb = nmap.registry.whois.whoisdb.arin.id
elseif have_objects and ( data.iana > 1 ) and table.concat( tracking.completed, " " ):match( nmap.registry.whois.whoisdb.arin.id ) then
-- two redirects to IANA - accept result from ARIN
data = data[nmap.registry.whois.whoisdb.arin.id]
data.id = nmap.registry.whois.whoisdb.arin.id
nextdb = nil
end
-- cache our analysis
local range
if data[this_db] and data[this_db].ob_netnum then
range = data[this_db].ob_netnum[meta.reg]
elseif data.ob_netnum and data.mirror then
range = data.ob_netnum[nmap.registry.whois.whoisdb[data.mirror].reg]
elseif data.ob_netnum then
range = data.ob_netnum[nmap.registry.whois.whoisdb[data.id].reg]
end
-- if nocache then enforce a smallest allowed prefix length
-- (these values should match those in add_to_cache)
if not nmap.registry.whois.using_cache and not nextdb then
local smallest_allowed_prefix = 29
if range:match( ":" ) then
smallest_allowed_prefix = 48
end
local range_prefix = get_prefix_length( range )
if type( range_prefix ) ~= "number" or range_prefix < smallest_allowed_prefix then
range = nil
end
end
-- prevent caching (0/0 or /8) or (::/0 or /23) or
range = not_short_prefix( ip, range, nextdb )
-- add to cache
add_to_cache( ip, range, nextdb, data )
end -- if have_objects
return data
end
---
-- Extracts Whois record objects (or a single object) and accompanying fields from the supplied (possibly partial) response to a whois query.
-- If a fifth parameter specific_object is not supplied, all objects defined in fields_meta will be captured if they are present in the response.
-- @param response_string String obtained from a service in response to our query.
-- @param db String id of the whois service queried.
-- @param ip String representing the Target's IP address.
-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
-- @param specific_object Optional string index of a single object defined in fields_meta (e.g. "inetnum").
-- @return Table indexed by object name containing the fields captured for each object found.
function extract_objects_from_response( response_string, db, ip, meta, specific_object )
local objects_to_extract = {}
local extracted_objects = {}
if type( response_string ) ~= "string" or response_string == "" then return {} end
if type( meta ) ~= "table" or type( meta.fieldreq ) ~= "table" then return {} end
-- we either receive a table for one object or for all objects
if type( specific_object ) == "string" and meta.fieldreq[specific_object] then
objects_to_extract[specific_object] = meta.fieldreq[specific_object]
stdnse.print_debug( 5, "%s %s Extracting a single object: %s.", SCRIPT_NAME, ip, specific_object )
else
stdnse.print_debug( 5, "%s %s Extracting all objects.", SCRIPT_NAME, ip )
objects_to_extract = meta.fieldreq
end
for object_name, object in pairs( objects_to_extract ) do
if object_name and object_name ~= "ob_exist" then
stdnse.print_debug(5, "%s %s Seeking object group: %s.", SCRIPT_NAME, ip, object_name)
extracted_objects[object_name] = {}
extracted_objects[object_name].for_compare = {} -- this will allow us to compare two tables
-- get a substr of response_string that corresponds to a single object
local ob_start, j = response_string:find( object.ob_start )
local i, ob_end = response_string:find( object.ob_end, j )
-- if we could not find the end, make the end EOF
ob_end = ob_end or -1
if ob_start and ob_end then
stdnse.print_debug(5, "%s %s Capturing: %s with indices %s and %s.", SCRIPT_NAME, ip, object_name, ob_start, ob_end )
local obj_string = response_string:sub( ob_start, ob_end )
for fieldname, pattern in pairs( object ) do
if fieldname ~= "ob_start" and fieldname ~= "ob_end" then
local data_pos, data_string = obj_string:find( pattern ), trim( obj_string:match( pattern ) )
if data_string then
extracted_objects[object_name][fieldname] = data_string
extracted_objects[object_name].for_compare[data_pos+ob_start] = data_string
end
end
end
end -- if ob_start and ob_end
end -- if object_name
end -- for object_name
if specific_object then extracted_objects = extracted_objects[specific_object] end -- returning one object
return extracted_objects
end -- function
---
-- Checks for referrals in fields extracted from the whois query response.
-- @param db String id of the whois service queried.
-- @param ip String representing the Target's IP address.
-- @param data Table, indexed by whois service id, of extracted fields.
-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
-- @return Boolean "found". True if a referral is not found (i.e. No Referral means the desired record has been "found"), otherwise False.
-- @return String "redirect". Service id to which we are referred, or nil.
-- @return Number "iana_count". This is the total number of referral to IANA for this Target (for all queries) and is stored in data.iana.
-- @see redirection_validation
function redirection_rules( db, ip, data, meta )
if type( db ) ~= "string" or db == "" or type( ip ) ~= "string" or ip == "" or type( data ) ~= "table" or not next( data ) then
return false, nil, nil
end
local found = false
local redirect = nil
local iana_count
if type( data.iana ) == "number" then
iana_count = data.iana
else
iana_count = 0
end
if not meta or not meta.redirects then
return found, redirect, iana_count
end
---
-- Decides the value of a redirect and whether it should be followed. Referrals to IANA, found in whois records that represent the
-- "Whole Address Space", are acted upon by redirecting to ARIN or accepting the record from ARIN if it was previously queried. This
-- function also catches (ignores) referrals to the referring service - which happens as a side-effect of the method of redirection detection.
-- The return values of this function will be returned by its parent function.
-- @param directed_to String id of a whois service.
-- @param directed_from String id of a whois service.
-- @param icnt Number of total redirects to IANA.
-- @return Boolean "found". True if a redirect is not found or ignored, otherwise False.
-- @return String "redirect". Service id to which we are redirected, or nil.
-- @return Number "iana_count" which is incremented here if applicable.
local redirection_validation = function( directed_to, directed_from, icnt )
local iana = nmap.registry.whois.whoisdb.iana.id
local arin = nmap.registry.whois.whoisdb.arin.id
-- arin record points to iana so we won't follow and we assume we have our record
if directed_to == iana and directed_from == arin then
stdnse.print_debug( 4, "%s %s %s Accept arin record (matched IANA).", SCRIPT_NAME, ip, directed_from )
return true, nil, ( icnt+1 )
end
-- non-arin record points to iana so we query arin next
if directed_to == iana then
stdnse.print_debug( 4, "%s %s Redirecting to arin (matched IANA).", SCRIPT_NAME, ip )
return false, arin, ( icnt+1 )
end
-- a redirect, but not to iana or to self, so we follow it.
if directed_to ~= nmap.registry.whois.whoisdb[directed_from].id then
stdnse.print_debug( 4, "%s %s %s redirects us to %s.", SCRIPT_NAME, ip, directed_from, directed_to )
return false, directed_to, icnt
end
-- redirect to self
return true, nil, icnt
end --redirection_validation
-- iterate over each table of redirect info for a specific field
for _, redirect_elems in ipairs( meta.redirects ) do
local obj, fld, pattern = table.unpack( redirect_elems ) -- three redirect elements
-- if a field has been captured for the given redirect info
if data[db][obj] and data[db][obj][fld] then
stdnse.print_debug( 5, "%s %s Seek redirect in object: %s.%s for %s.", SCRIPT_NAME, ip, obj, fld, pattern )
-- iterate over nmap.registry.whois.whoisdb to find pattern (from each service) in the designated field
for member, mem_properties in pairs( nmap.registry.whois.whoisdb ) do
-- if pattern if found in the field, we have a redirect to member
if type( mem_properties[pattern] ) == "string" and string.lower( data[db][obj][fld] ):match( mem_properties[pattern] ) then
stdnse.print_debug( 5, "%s %s Matched %s in %s.%s.", SCRIPT_NAME, ip, pattern, obj, fld )
return redirection_validation( nmap.registry.whois.whoisdb[member].id, db, iana_count )
elseif type( mem_properties[pattern] ) == "table" then
-- pattern is an array of patterns
for _, pattn in ipairs( mem_properties[pattern] ) do
if type( pattn ) == "string" and string.lower( data[db][obj][fld] ):match( pattn ) then
stdnse.print_debug( 5, "%s %s Matched %s in %s.%s.", SCRIPT_NAME, ip, pattern, obj, fld )
return redirection_validation( nmap.registry.whois.whoisdb[member].id, db, iana_count )
end
end
end
end -- for mem, mem_properties
end
end -- for _,v in ipairs
-- if redirects have not been found then assume that the record has been found.
found = true
return found, redirect, iana_count
end
---
-- Attempts to reduce the query response to a subset containing the most specific assignment information.
-- It does this by collecting inetnum objects (and their positions in the response) and choosing the smallest assignment represented by them.
-- A subset beginning with the most specific inetnum object and ending before any further inetnum objects is returned along with the position
-- of the subset within the entire response.
-- @param response String obtained from a whois service in response to our query.
-- @param db String id of the service from which the response was obtained.
-- @param ip String representing the Target's IP address.
-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
-- @return String containing the most specific part of the response (or the entire response if only one inetneum object is present).
-- @return Number position of the start of the most specific part of the response.
-- @see smallest_range
function constrain_response( response, db, ip, meta )
local strbgn = 1
local strend = 1
local ptr = 1
local mptr = {}
local bound = nil
-- collect all inetnums objects (and their position) into a table
while strbgn and meta.fieldreq do
strbgn, strend = response:find( meta.fieldreq.ob_exist, strend )
if strbgn then
local pair = {}
pair.pointer = strbgn
pair.range = trim( response:match( meta.smallnet_rule, strbgn ) )
mptr[#mptr+1] = pair
end
end