source: trunk/plugins/nikto_core.plugin @ 376

Revision 376, 94.0 KB checked in by sullo, 3 years ago (diff)

Minor output tweak

  • Property svn:keywords set to Id
Line 
1#VERSION,2.1.1
2# $Id$
3###############################################################################
4#  Copyright (C) 2006 CIRT, Inc.
5#
6#  This program is free software; you can redistribute it and/or
7#  modify it under the terms of the GNU General Public License
8#  as published by the Free Software Foundation; version 2
9#  of the License only.
10#
11#  This program is distributed in the hope that it will be useful,
12#  but WITHOUT ANY WARRANTY; without even the implied warranty of
13#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#  GNU General Public License for more details.
15#
16#  You should have received a copy of the GNU General Public License
17#  along with this program; if not, write to the Free Software
18#  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
19###############################################################################
20# PURPOSE:
21# Nikto core functionality
22###############################################################################
23
24sub change_variables {
25
26    # $line is the unfiltered variable
27    my $line = $_[0];
28    my @subtests;    # @subtests is the returned array of expanded variables
29    my $cooked;
30
31    my $shname = $mark->{'hostname'} || $mark->{'ip'};
32    $line =~ s/\@IP/$mark->{'ip'}/g;
33    $line =~ s/\@HOSTNAME/$shname/g;
34    $line =~ s/JUNK\(([0-9]+)\)/LW2::utils_randstr($1)/e;
35
36    if ($line !~ "\@") {
37        push(@subtests, $line);
38    }
39    else {
40        foreach my $varname (keys %VARIABLES) {
41            if ($line =~ /$varname/) {
42
43                # We've found the variable; now to expand it!
44                foreach my $value (split(/ /, $VARIABLES{$varname})) {
45                    $cooked = $line;
46                    $cooked =~ s/$varname/$value/g;
47                    push(@subtests, change_variables($cooked));
48                }
49            }
50        }
51    }
52
53    return @subtests;
54}
55
56###############################################################################
57sub is_404 {
58    my ($uri, $content, $rescode) = @_;
59    $ext = get_ext($uri);
60
61    if (($FoF{$ext}{'mode'} eq "STD") && ($rescode =~ /4[0-9][0-9]/)) {
62        return 1;
63    }
64    elsif ($FoF{$ext}{'mode'} eq "REDIR") {
65        if ($result{'location'} eq $FoF{$ext}{'location'}) {
66            return 1;
67        }
68    }
69    elsif (($FoF{$ext}{'type'} eq "BLANK") && ($content eq "")) {
70        return 1;
71    }
72    elsif ($FoF{$ext}{'type'} eq "HASH") {
73        my $content = rm_active_content($content, $uri);
74        if (LW2::md4($content) eq $FoF{$ext}{'match'}) {
75            return 1;
76        }
77    }
78    else {
79        foreach my $string (keys %ERRSTRINGS) {
80            if ($content =~ /$string/i) {
81                return 1;
82            }
83        }
84    }
85
86    return 0;
87}
88
89###############################################################################
90sub nprint {
91    my $line   = shift;
92    my $mode   = shift;
93    my ($mark) = @_;
94    chomp($line);
95
96    # scrub values
97    if ($OUTPUT{'scrub'}) {
98
99        # name
100        $line =~ s/$mark->{'hostname'}/example.com/ig unless $mark->{'hostname'} eq '';
101
102        # ip
103        $line =~ s/$mark->{'ip'}/0.0.0.0/ig unless $mark->{'ip'} eq '';
104
105        # vhost
106        $line =~ s/$CLI{'vhost'}/example.com/ig unless $CLI{'vhost'} eq '';
107
108        # and in case we got here from set_target
109        $line =~ s/$mark->{'ident'}/example.com/ig unless $mark->{'ident'} eq '';
110    }
111
112    # don't print debug & verbose to output file...
113    if ($mode ne '') {
114        if ($mode eq "d" && $OUTPUT{'debug'}) {
115            print "D:" . localtime() . " $line\n";
116        }
117        if ($mode eq "v" && $OUTPUT{'verbose'}) {
118            print "V:" . localtime() . " $line\n";
119        }
120        if ($mode eq "e" && $OUTPUT{'errors'}) {
121            print "E:" . localtime() . " $line\n";
122        }
123        return;
124    }
125
126    # print errors to STDERR
127    if ($line =~ /^\+ ERROR\:/) { print STDERR "$line\n"; return; }
128
129    # don't print to STDOUT if output file is "-"
130    if ((defined $CLI{'file'}) && ($CLI{'file'} eq "-")) { return; }
131
132# print to scan details to standard output if the users wants another format and is saving results to a file
133    $line =~
134      s/((CVE|CAN)\-[0-9]{4}-[0-9]{4})/http:\/\/cve.mitre.org\/cgi-bin\/cvename.cgi?name\=$1/g;
135    $line =~ s/(CA\-[0-9]{4}-[0-9]{2})/http:\/\/www.cert.org\/advisories\/$1.html/g;
136    $line =~ s/BID\-([0-9]{4})/http:\/\/www.securityfocus.com\/bid\/$1/g;
137    $line =~ s/(IN\-[0-9]{4}\-[0-9]{2})/http:\/\/www.cert.org\/incident_notes\/$1.html/gi;
138    $line =~
139      s/(MS[0-9]{2}\-[0-9]{3})/http:\/\/www.microsoft.com\/technet\/security\/bulletin\/$1.asp/gi;
140    print "$line\n";
141
142    return;
143}
144
145###############################################################################
146sub get_ext {
147    my $uri = $_[0] || return;
148    if ($uri =~ /\/$/) { return "DIRECTORY"; }
149    $uri =~ s/^.*\///;
150    $uri =~ s/(\?|\&|\%).*$//;
151    if ($uri =~ /^\.[^.%]/) { return "DOTFILE"; }
152    if ($uri !~ /\./) { return "NONE"; }
153    $uri =~ s/\".*$//;
154    $uri =~ s/^.*\.//;
155    return $uri;
156}
157
158###############################################################################
159sub date_disp {
160    my @time = localtime($_[0]);
161    $time[5] += 1900;
162    $time[4]++;
163    if (length($time[4]) eq 1) { $time[4] = "0$time[4]"; }
164    $time[3]++;
165    if (length($time[3]) eq 1) { $time[3] = "0$time[3]"; }
166    if (length($time[0]) eq 1) { $time[0] = "0$time[0]"; }
167    if (length($time[1]) eq 1) { $time[1] = "0$time[1]"; }
168    if (length($time[2]) eq 1) { $time[0] = "0$time[2]"; }
169    return "$time[5]-$time[4]-$time[3] $time[2]:$time[1]:$time[0]";
170}
171
172###############################################################################
173sub map_codes {
174    my %REQS;
175    my $rs = LW2::utils_randstr(8);
176
177    # / for OK response
178    $NIKTO{'totalrequests'}++;
179    $request{'whisker'}->{'uri'} = "/";
180    delete $request{'whisker'}->{'data'};
181    $request{'whisker'}->{'method'} = GET;
182
183    delete $request{'whisker'}->{'Content-Length'};
184    LW2::http_close(\%request);    # force-close any old connections
185    dump_var("Request Hash", \%request);
186    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
187    LW2::http_do_request_timeout(\%request, \%result);
188    $NIKTO{'totalrequests'}++;
189    dump_var("Result Hash", \%result);
190    if (defined $result{'location'}) {
191        nprint("- Root page / redirects to: $result{'location'}");
192        if ($result{'location'} =~ /^$request{'whisker'}{'host'}/i)    # same host
193        {
194            $request{'whisker'}->{'uri'} = $result{'location'};
195            $request{'whisker'}->{'uri'} =~ s/^http(s)?\:\/\/$request{'whisker'}{'host'}//i;
196            LW2::http_close(\%request);    # force-close any old connections
197            LW2::http_fixup_request(\%request);
198            dump_var("Request Hash", \%request);
199            if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
200            LW2::http_do_request_timeout(\%request, \%result);
201            $NIKTO{'totalrequests'}++;
202            dump_var("Result Hash", \%result);
203        }
204        else                               # different host... ugh... guess
205        {
206            $FoF{'okay'}{'response'} = 200;
207            $FoF{'okay'}{'type'}     = "STD";
208        }
209    }
210    else {
211        $FoF{'okay'}{'response'} = $result{'whisker'}->{'code'};
212        my $content = rm_active_content($result{'whisker'}->{'data'});
213        $FoF{'okay'}{'type'}  = "HASH";
214        $FoF{'okay'}{'match'} = LW2::md4($content);
215    }
216
217    # these are some used in mutate that may not be in the db_tests
218    $db_extensions{'bak'}  = 1;
219    $db_extensions{'data'} = 1;
220    $db_extensions{'dbc'}  = 1;
221    $db_extensions{'dbf'}  = 1;
222    $db_extensions{'lst'}  = 1;
223    $db_extensions{'htx'}  = 1;
224
225    foreach my $ext (keys %db_extensions) {
226        if ($ext eq "DIRECTORY") {
227            next;
228        }    # don't test generic type holder as real extension (added to db_extensions by get_ext)
229        if ($ext eq "NONE") {
230            next;
231        }    # don't test generic type holder as real extension (added to db_extensions by get_ext)
232        if ($ext eq "DOTFILE") {
233            next;
234        }    # don't test generic type holder as real extension (added to db_extensions by get_ext)
235        $REQS{"/$rs.$ext"} = $ext;
236    }
237
238    # add those generic type holders back as real files
239    $REQS{"/$rs/"} = "DIRECTORY";
240    $REQS{"/$rs"}  = "NONE";
241    $REQS{"/.$rs"} = "DOTFILE";
242
243    foreach my $file (keys %REQS) {
244        nprint("- Testing error for file: $file\n", "v");
245        $NIKTO{'totalrequests'}++;
246        $request{'whisker'}->{'uri'}    = $file;
247        $request{'whisker'}->{'method'} = GET;
248        LW2::http_close(\%request);    # force-close any old connections
249        delete $request{'whisker'}->{'data'};
250        delete $request{'Content-Encoding'};
251        delete $request{'Content-Length'};
252        LW2::http_fixup_request(\%request);
253
254        dump_var("Request Hash", \%request);
255        if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
256        LW2::http_do_request_timeout(\%request, \%result);
257        $NIKTO{'totalrequests'}++;
258        dump_var("Result Hash", \%result);
259
260        $ext = $REQS{$file};
261        $FoF{$ext}{'response'} = $result{'whisker'}->{'code'};
262
263        # handle .com to .org redirs or whatnot
264        if (defined $result{'location'}) {
265            $FoF{$ext}{'location'} = $result{'location'};
266            $file = char_escape($file);
267            $FoF{$ext}{'location'} =~ s/$file//;
268        }
269
270        # if it is not specific type, figure out Content or HASH method...
271        if ($FoF{$ext}{'response'} eq 404) { $FoF{$ext}{'mode'} = "STD"; next; }
272        elsif ($FoF{$ext}{'response'} eq 200) { $FoF{$ext}{'mode'} = "OK"; }
273        elsif ($FoF{$ext}{'response'} eq 410) { $FoF{$ext}{'mode'} = "STD"; next; }
274        elsif ($FoF{$ext}{'response'} eq 401) { $FoF{$ext}{'mode'} = "STD"; next; }
275        elsif ($FoF{$ext}{'response'} eq 403) { $FoF{$ext}{'mode'} = "STD"; next; }
276        elsif ($FoF{$ext}{'response'} eq 300) { $FoF{$ext}{'mode'} = "REDIR"; next; }
277        elsif ($FoF{$ext}{'response'} eq 301) { $FoF{$ext}{'mode'} = "REDIR"; next; }
278        elsif ($FoF{$ext}{'response'} eq 302) { $FoF{$ext}{'mode'} = "REDIR"; next; }
279        elsif ($FoF{$ext}{'response'} eq 303) { $FoF{$ext}{'mode'} = "REDIR"; next; }
280        elsif ($FoF{$ext}{'response'} eq 307) { $FoF{$ext}{'mode'} = "REDIR"; next; }
281        else                                  { $FoF{$ext}{'mode'} = "OTHER"; }
282
283        # if we've got an OK/OTHER response, look at content first
284        # blank content, or hash...
285        if (length($result{'whisker'}->{'data'}) eq 0) {
286            $FoF{$ext}{'type'}  = "BLANK";
287            $FoF{$ext}{'match'} = "";
288        }
289        else {
290            my $content = rm_active_content($result{'whisker'}->{'data'});
291            $FoF{$ext}{'match'} = LW2::md4($content);
292            $FoF{$ext}{'type'}  = "HASH";
293        }
294    }
295
296    # lastly, get a hash of index.php so we can cut down on some false positives...
297    $NIKTO{'totalrequests'}++;
298    $request{'whisker'}->{'uri'}    = "/index.php?";
299    $request{'whisker'}->{'method'} = GET;
300    LW2::http_close(\%request);    # force-close any old connections
301    delete $request{'whisker'}->{'data'};
302    delete $request{'Content-Encoding'};
303    delete $request{'Content-Length'};
304    LW2::http_fixup_request(\%request);
305
306    dump_var("Request Hash", \%request);
307    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
308    LW2::http_do_request_timeout(\%request, \%result);
309    $NIKTO{'totalrequests'}++;
310    dump_var("Result Hash", \%result);
311
312    my $content = rm_active_content($result{'whisker'}->{'data'});
313    $FoF{"index.php"}{'match'} = LW2::md4($content);
314    $FoF{"index.php"}{'type'}  = "HASH";
315
316# foreach $ext (keys %FoF)   { nprint "$ext: mode $FoF{$ext}{'mode'}, response $FoF{$ext}{'response'}, type $FoF{$ext}{'type'}\n"; }
317    return;
318}
319
320###############################################################################
321sub rm_active_content {
322
323    # Try to remove active content which could mess up the file's signature
324    my $cont = $_[0];
325    my $file = $_[1];
326
327    # filename
328    if ($file ne '') {
329        $file =~ s/([^a-zA-Z0-9\s])/\\$1/g;
330        $cont =~ s/$file//g;
331    }
332
333    # Dates
334    $cont =~ s/([0-9]{4}|[0-9]{1,2})(\-|\.|\/)[0-9]{1,2}(\-|\.|\/)([0-9]{4}|[0-9]{1,2})//g;
335    $cont =~ s/(([0-9]{2}:[0-9]{2}(:)?([0-9]{2})?)|([0-9]{8,14}|[0-9]{6}))//g;
336    $cont =~
337      s/(mon|tue|wed|thu|fri|sat|sun)(,)? [0-9]{1,2} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9]{4} ([0-9]{2}:[0-9]{2}(:)?([0-9]{2})?)?//ig;
338    $cont =~
339      s/([0-9]{2,4})? ?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)([0-9]{2,4})?(\/)?([0-9]{2})?([0-9]{2})?//gi;
340
341    $cont =~ s/[0-9\.]+ second//gi;    # page load time
342    $cont =~ s/[0-9]+ queries//gi;     # wordpress
343
344    # URI, if provided, plus encoded versions of it
345    # $_[1] has unescaped file name, and $file has escaped. use appropriate one!
346    if (defined $file) {
347
348        # match pages which link to themselves w/diff args
349        my $e = $file;
350        $e    =~ s/^\/$file\??//;
351        $cont =~ s/$e//gs;
352
353        # again but with the index in place
354        $e = $file;
355        $cont =~ s/$e//gs;
356
357        # base 64
358        $e = LW2::encode_base64($_[1]);
359        $cont =~ s/$e//gs;
360
361        # hex encoded
362        $e = LW2::encode_uri_hex($_[1]);
363        $cont =~ s/$e//gs;
364
365        # unicode encoded
366        $e = LW2::encode_unicode($_[1]);
367        $cont =~ s/$e//gs;
368
369        # url encoding, full url
370        $e = $_[1];
371        $e    =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
372        $cont =~ s/$e//gs;
373
374        # url encoding, query portion
375        if ($file =~ /\?/) {
376            $e = $file;
377            $e =~ s/\?(.*$)//;
378            my $qs = $1;
379            $qs =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
380            $e .= "?$qs";
381            $cont =~ s/$e//gs;
382        }
383    }
384
385    return $cont;
386}
387
388###############################################################################
389sub dump_target_info {
390    my ($mark) = @_;
391    my $sslprint = "";
392
393    if ($mark->{ssl}) {
394        my $sslcyphers = $result{'whisker'}->{'ssl_cipher'}       || "Unknown";
395        my $sslissuers = $result{'whisker'}->{'ssl_cert_issuer'}  || "Unknown";
396        my $sslinfo    = $result{'whisker'}->{'ssl_cert_subject'} || "Unknown";
397        $sslprint = "$NIKTO{'DIV'}\n";
398        $sslprint .=
399          "+ SSL Info:        Ciphers: $sslcyphers\n                   Info:    $sslissuers\n                   Subject: $sslinfo";
400
401        $mark->{sslcyphers} = $sslcyphers;
402        $mark->{sslissuers} = $sslissuers;
403        $mark->{sslinfo}    = $sslinfo;
404    }
405
406    if ($mark->{ip} =~ /[a-z]/i) {
407        nprint("+ Target IP:       (proxied)", "", $mark);
408    }
409    else {
410        nprint("+ Target IP:          $mark->{ip}", "", $mark);
411    }
412
413    nprint("+ Target Hostname:    $mark->{hostname}", "", $mark);
414    nprint("+ Target Port:        $mark->{port}");
415    if ((defined $CLI{'vhost'}) && ($CLI{'vhost'} ne $mark->{hostname})) {
416        nprint("+ Virtual Host:   $CLI{'vhost'}", "", $mark);
417    }
418    if (defined $request{'whisker'}->{'proxy_host'}) {
419        nprint(
420            "- Proxy:              $request{'whisker'}->{'proxy_host'}:$request{'whisker'}->{'proxy_port'}"
421            );
422    }
423    if (defined $NIKTO{'hostid'}) {
424        nprint(
425            "- Host Auth:       ID: $NIKTO{'hostid'}, PW: $NIKTO{'hostpw'}, Realm: $NIKTO{'hostdomain'}",
426            "v"
427            );
428    }
429    if ($mark->{ssl}) {
430        nprint($sslprint);
431    }
432
433    if (defined $NIKTO{'anti_ids'} && defined $CLI{'evasion'}) {
434        for (my $i = 1 ; $i <= (keys %{ $NIKTO{'anti_ids'} }) ; $i++) {
435            if ($CLI{'evasion'} =~ /$i/) {
436                nprint("+ Using IDS Evasion:  $NIKTO{'anti_ids'}{$i}");
437            }
438        }
439    }
440    if (defined $NIKTO{'mutate_opts'} && defined $CLI{'mutate'}) {
441        for (my $i = 1 ; $i <= (keys %{ $NIKTO{'mutate_opts'} }) ; $i++) {
442            if ($CLI{'mutate'} =~ /$i/) {
443                nprint("+ Using Mutation:     $NIKTO{'mutate_opts'}{$i}");
444            }
445        }
446    }
447    my $time = date_disp($mark->{start_time});
448    nprint("+ Start Time:         $time");
449    nprint($NIKTO{'DIV'});
450
451    if ($mark->{banner} ne "") {
452        nprint("+ Server: $mark->{banner}");
453    }
454    else {
455        nprint("+ Server: No banner retrieved");
456    }
457
458    return;
459}
460
461###############################################################################
462sub general_config {
463    ## gotta set these first
464    $|                    = 1;
465    $NIKTO{'anti_ids'}{1} = "Random URI encoding (non-UTF8)";
466    $NIKTO{'anti_ids'}{2} = "Directory self-reference (/./)";
467    $NIKTO{'anti_ids'}{3} = "Premature URL ending";
468    $NIKTO{'anti_ids'}{4} = "Prepend long random string";
469    $NIKTO{'anti_ids'}{5} = "Fake parameter";
470    $NIKTO{'anti_ids'}{6} = "TAB as request spacer";
471    $NIKTO{'anti_ids'}{7} = "Change the case of the URL";
472    $NIKTO{'anti_ids'}{8} = "Use Windows directory separator (\\)";
473    $NIKTO{'anti_ids'}{A} = "Use a carriage return (0x0d) as a request spacer";
474    $NIKTO{'anti_ids'}{B} = "Use binary value 0x0b as a request spacer";
475
476    $NIKTO{'mutate_opts'}{1} = "Test all files with all root directories";
477    $NIKTO{'mutate_opts'}{2} = "Guess for password file names";
478    $NIKTO{'mutate_opts'}{3} = "Enumerate user names via Apache (/~user type requests)";
479    $NIKTO{'mutate_opts'}{4} =
480      "Enumerate user names via cgiwrap (/cgi-bin/cgiwrap/~user type requests)";
481    $NIKTO{'mutate_opts'}{5} =
482      "Attempt to brute force sub-domain names, assume that the host name is the parent domain";
483    $NIKTO{'mutate_opts'}{6} = "Attempt to guess directory names from the supplied dictionary file";
484
485    $NIKTO{'display'}{1} = "Show redirects";
486    $NIKTO{'display'}{2} = "Show cookies received";
487    $NIKTO{'display'}{3} = "Show all 200/OK responses";
488    $NIKTO{'display'}{4} = "Show URLs which require authentication";
489    $NIKTO{'display'}{V} = "Verbose output";
490    $NIKTO{'display'}{D} = "Debug output";
491    $NIKTO{'display'}{E} = "Display all HTTP errors";
492
493    $NIKTO{'tuning'}{1} = "Interesting File / Seen in logs";
494    $NIKTO{'tuning'}{2} = "Misconfiguration / Default File";
495    $NIKTO{'tuning'}{3} = "Information Disclosure";
496    $NIKTO{'tuning'}{4} = "Injection (XSS/Script/HTML)";
497    $NIKTO{'tuning'}{5} = "Remote File Retrieval - Inside Web Root";
498    $NIKTO{'tuning'}{6} = "Denial of Service";
499    $NIKTO{'tuning'}{7} = "Remote File Retrieval - Server Wide";
500    $NIKTO{'tuning'}{8} = "Command Execution / Remote Shell";
501    $NIKTO{'tuning'}{9} = "SQL Injection";
502    $NIKTO{'tuning'}{0} = "File Upload";
503    $NIKTO{'tuning'}{a} = "Authentication Bypass";
504    $NIKTO{'tuning'}{b} = "Software Identification";
505    $NIKTO{'tuning'}{c} = "Remote Source Inclusion";
506    $NIKTO{'tuning'}{x} = "Reverse Tuning Options (i.e., include all except specified)";
507
508    $NIKTO{'options_short'} = "
509       -config+            Use this config file
510       -Cgidirs+           scan these CGI dirs: 'none', 'all', or values like \"/cgi/ /cgi-a/\"
511       -dbcheck            check database and other key files for syntax errors (cannot be abbreviated)
512       -evasion+           ids evasion technique
513       -Format+            save file (-o) format
514       -host+              target host
515       -Help               Extended help information
516       -id+                host authentication to use, format is userid:password
517       -list-plugins       List all available plugins
518       -mutate+            Guess additional file names
519       -mutate-options+    Provide extra information for mutations
520       -output+            Write output to this file
521       -nocache            Disables the URI cache
522       -nossl              Disables using SSL
523       -no404              Disables 404 checks
524       -Plugins            List of plugins to run (default ALL)
525       -port+              Port to use (default 80)
526       -Display+           Turn on/off display outputs
527       -ssl                Force ssl mode on port
528       -Single             Single request mode
529       -timeout+           Timeout (default 2 seconds)
530       -Tuning+            Scan tuning
531       -update             Update databases and plugins from cirt.net (cannot be abbreviated)
532       -Version            Print plugin and database versions
533       -vhost+             Virtual host (for Host header)
534   + requires a value
535   ";
536
537    $NIKTO{'options'} = "
538   Options:
539       -config+            Use this config file
540       -Cgidirs+           Scan these CGI dirs: 'none', 'all', or values like \"/cgi/ /cgi-a/\"
541       -Display+           Turn on/off display outputs:\n";
542    foreach my $k (sort keys %{ $NIKTO{'display'} }) {
543        $NIKTO{'options'} .= "                               $k     $NIKTO{'display'}{$k}\n";
544    }
545
546    $NIKTO{'options'} .=
547      "       -dbcheck           Check database and other key files for syntax errors (cannot be abbreviated)
548       -evasion+          IDS evasion technique:\n";
549    foreach my $k (sort keys %{ $NIKTO{'anti_ids'} }) {
550        $NIKTO{'options'} .= "                               $k     $NIKTO{'anti_ids'}{$k}\n";
551    }
552
553    $NIKTO{'options'} .=
554      "       -findonly          Find http(s) ports only, don't perform a full scan
555       -Format+           Save file (-o) format:
556                                csv   Comma-separated-value
557                                htm   HTML Format
558                                nbe   Nessus NBE format
559                                txt   Plain text (default if not specified)
560                                xml   XML Format
561       -host+             Target host
562       -Help              Extended help information
563       -id+               Host authentication to use, format is userid:password
564       -list-plugins      List all available plugins, perform no testing
565       -mutate+           Guess additional file names:\n";
566    foreach my $k (sort keys %{ $NIKTO{'mutate_opts'} }) {
567        $NIKTO{'options'} .= "                               $k     $NIKTO{'mutate_opts'}{$k}\n";
568    }
569
570    $NIKTO{'options'} .= "       -mutate-options    Provide information for mutates
571       -nocache           Disables the URI cache
572       -nossl             Disables using SSL
573       -no404             Disables nikto attempting to guess a 404 page
574       -output+           Write output to this file
575       -Plugins           List of plugins to run (default ALL)
576       -port+             Port to use (default 80)
577       -Pause+            Pause between tests (seconds)\n";
578
579    $NIKTO{'options'} .=
580      "       -root+             Prepend root value to all requests, format is /directory
581       -ssl               Force ssl mode on port
582       -Single            Single request mode
583       -timeout+          Timeout (default 2 seconds)
584       -Tuning+           Scan tuning:\n";
585    foreach my $k (sort keys %{ $NIKTO{'tuning'} }) {
586        $NIKTO{'options'} .= "                               $k     $NIKTO{'tuning'}{$k}\n";
587    }
588
589    $NIKTO{'options'} .= "       -useproxy          Use the proxy defined in config.txt
590       -update            Update databases and plugins from cirt.net (cannot be abbreviated)
591       -Version           Print plugin and database versions
592       -vhost+            Virtual host (for Host header)
593   + requires a value
594   ";
595
596    ### CLI STUFF
597    $CLI{'pause'}             = $CLI{'html'}     = $OUTPUT{'verbose'} = $CLI{'skiplookup'} =
598      $NIKTO{'totalrequests'} = $OUTPUT{'debug'} = $OUTPUT{'scrub'}   = $OUTPUT{'errors'} = 0;
599    $CLI{'all_options'} = join(" ", @ARGV);
600
601    # preprocess CLI options which cannot be abbreviated
602    for (my $i = 0 ; $i <= $#ARGV ; $i++) {
603        if    ($ARGV[$i] eq '-dbcheck') { dbcheck(); }
604        elsif ($ARGV[$i] eq '-update')  { check_updates(); }
605    }
606
607    GetOptions("nolookup"         => \$CLI{'skiplookup'},
608               "config=s"         => \$CLI{'config'},
609               "Cgidirs=s"        => \$CLI{'forcecgi'},
610               "mutate=s"         => \$CLI{'mutate'},
611               "mutate-options=s" => \$CLI{'mutate-options'},
612               "id=s"             => \$CLI{'hostauth'},
613               "evasion=s"        => \$CLI{'evasion'},
614               "port=s"           => \$CLI{'ports'},
615               "findonly"         => \$CLI{'findonly'},
616               "root=s"           => \$CLI{'root'},
617               "timeout=s"        => \$CLI{'timeout'},
618               "Pause=s"          => \$CLI{'pause'},
619               "ssl"              => \$CLI{'ssl'},
620               "nocache"          => \$CLI{'nocache'},
621               "nossl"            => \$CLI{'nossl'},
622               "no404"            => \$CLI{'nofof'},
623               "useproxy"         => \$CLI{'useproxy'},
624               "Help"             => \$CLI{'help'},
625               "vhost=s"          => \$CLI{'vhost'},
626               "host=s"           => \$CLI{'host'},
627               "output=s"         => \$CLI{'file'},
628               "Format=s"         => \$CLI{'format'},
629               "Display=s"        => \$CLI{'display'},
630               "Single"           => \$CLI{'Single'},
631               "Tuning=s"         => \$CLI{'tuning'},
632               "Version"          => \$CLI{'version'},
633               "Plugins=s"        => \$CLI{'plugins'},
634               "list-plugins"     => \$CLI{'list-plugins'},
635               ) or usage(0);
636
637    if    ($CLI{'help'})         { usage(2); }
638    elsif ($CLI{'version'})      { version(); }
639    elsif ($CLI{'Single'})       { single(); }
640    elsif ($CLI{'list-plugins'}) { list_plugins(); }
641
642    # output file
643    if (!defined $CLI{'format'}) {
644
645        # Check what output has
646        $CLI{'format'} = "none";
647        if (defined $CLI{'file'}) {
648            $CLI{'format'} = lc($CLI{'file'});
649            $CLI{'format'} =~ s/(^.*\.)([^.]*$)/$2/g;
650        }
651    }
652
653    if    ($CLI{'format'} =~ /te?xt/i) { $CLI{'format'} = "txt"; }
654    elsif ($CLI{'format'} =~ /html?/i) { $CLI{'format'} = "htm"; }
655    elsif ($CLI{'format'} =~ /csv/i)   { $CLI{'format'} = "csv"; }
656    elsif ($CLI{'format'} =~ /nbe/i)   { $CLI{'format'} = "nbe"; }
657    elsif ($CLI{'format'} =~ /xml/i)   { $CLI{'format'} = "xml"; }
658    elsif ($CLI{'format'} eq 'none') { }
659    else                             { nprint("+ ERROR: Invalid output format"); exit; }
660
661    if ((defined $CLI{'file'}) && ($CLI{'format'} eq "")) {
662        nprint("+ERROR: Output file specified without a format");
663        exit;
664    }
665
666    # verify readable dtd
667    if ($CLI{'format'} eq 'xml' && !-r $NIKTOCONFIG{NIKTODTD}) {
668        nprint("+ ERROR: reading DTD");
669        exit;
670    }
671
672    # screen output
673    if (defined $CLI{'display'}) {
674        if ($CLI{'display'} =~ /d/i) { $OUTPUT{'debug'}   = 1; }
675        if ($CLI{'display'} =~ /v/i) { $OUTPUT{'verbose'} = 1; }
676        if ($CLI{'display'} =~ /s/i) { $OUTPUT{'scrub'}   = 1; }
677        if ($CLI{'display'} =~ /e/i) { $OUTPUT{'errors'}  = 1; }
678    }
679
680    # port(s)
681    if (defined $CLI{'ports'}) {
682        $CLI{'ports'} =~ s/^\s+//;
683        $CLI{'ports'} =~ s/\s+$//;
684        if ($CLI{'ports'} =~ /[^0-9\-\, ]/) {
685            nprint("+ ERROR: Invalid port option '$CLI{'ports'}'");
686            exit;
687        }
688    }
689
690    # Fixup
691    if (defined $CLI{'root'}) {
692        $CLI{'root'} =~ s/\/$//;
693        if (($CLI{'root'} !~ /^\//) && ($CLI{'root'} ne "")) { $CLI{'root'} = "/$CLI{'root'}"; }
694    }
695
696    if (defined $CLI{'hostauth'}) {
697        my @x = split(/:/, $CLI{'hostauth'});
698        if (($#x > 2) || ($x[0] eq "")) {
699            nprint(
700                "+ ERROR: \'$CLI{'hostauth'}\' (-i option) syntax is 'user:password' or 'user:password:domain' for host authentication."
701                );
702            exit;
703        }
704    }
705
706    if (defined $CLI{'evasion'}) {
707        $CLI{'evasion'} =~ s/[^1-8AB]//g;
708    }
709
710    if (!defined $CLI{'plugins'} || $CLI{'plugins'} eq "") {
711        $CLI{'plugins'} = '@@DEFAULT';
712    }
713
714    # Mapping for mutate for plugins
715    if (defined $CLI{'mutate'}) {
716        nprint("- Mutate is deprecated, use -Plugins instead");
717        if ($CLI{'mutate'} =~ /1/ || $CLI{'mutate'} =~ /2/) {
718            my $parameters;
719            $parameters = "passfiles" if ($CLI{'mutate'} =~ /2/);
720            $parameters .= ",all" if ($CLI{'mutate'} =~ /1/);
721            $CLI{'plugins'} .= ';tests(' . $parameters . ')';
722        }
723        if ($CLI{'mutate'} =~ /3/ || $CLI{'mutate'} =~ /4/) {
724            my $parameters;
725            $parameters = "home" if ($CLI{'mutate'} =~ /3/);
726            $parameters .= ",cgiwrap" if ($CLI{'mutate'} =~ /4/);
727            $parameters .= ",dictionary:" . $CLI{'mutate-opts'} if (defined $CLI{'mutate-opts'});
728            $CLI{'plugins'} .= ';user_enum_apache(' . $parameters . ')';
729        }
730        if ($CLI{'mutate'} =~ /5/) {
731            $CLI{'plugins'} .= ";subdomain";
732        }
733        if ($CLI{'mutate'} =~ /6/) {
734            $CLI{'plugins'} .= ';dictionary(dictionary:' . $CLI{'mutate-opts'} . ')';
735        }
736    }
737
738    $NIKTO{'timeout'} = $CLI{'timeout'} || 10;
739
740    # Set up User-Agent
741    $NIKTO{'useragent'} = $NIKTOCONFIG{'USERAGENT'};
742    $NIKTO{'useragent'} =~ s/\@VERSION/$NIKTO{'version'}/g;
743    my $ev = $CLI{'evasion'} || "None";
744    $NIKTO{'useragent'} =~ s/\@EVASIONS/$ev/g;
745
746    # RFI URL -- push it to VARIABLES
747    if (defined $NIKTOCONFIG{'RFIURL'}) {
748        $VARIABLES{'@RFIURL'} = $NIKTOCONFIG{'RFIURL'};
749    }
750    else {
751        nprint(
752            "- ***** RFIURL is not defined in nikto.conf, which means no RFI tests will run *****");
753    }
754
755    # SSL Test
756    if (!LW2::ssl_is_available()) {
757        nprint("- ***** SSL support not available (see docs for SSL install instructions) *****");
758    }
759
760    # Notices
761    my $notice = "";
762    if (defined $CLI{'root'}) { $notice .= "Prepending \'$CLI{'root'}\' to requests"; }
763    if ($CLI{'pause'} > 0)    { $notice .= ", Pausing $CLI{'pause'} seconds per request"; }
764    $notice =~ s/^, //;
765    if ($notice ne '') { nprint("-***** $notice *****"); }
766
767    # get core version
768    open(FI, "<$NIKTOCONFIG{PLUGINDIR}/nikto_core.plugin");
769    my @F = <FI>;
770    close(FI);
771    my @VERS = grep(/^#VERSION/, @F);
772    $NIKTO{'core_version'} = $VERS[0];
773    $NIKTO{'core_version'} =~ s/\#VERSION,//;
774    chomp($NIKTO{'core_version'});
775    $NIKTO{'TMPL_HCTR'}    = 0;
776    $NIKTO{'TMPL_SUMMARY'} = 0;
777
778    return;
779}
780
781###############################################################################
782sub resolve {
783    my $ident = $_[0] || return;
784    my ($name, $ip, $dn) = "";
785
786    # ident is name, lookup IP
787    if ($ident =~ /[^0-9\.]/)    # not an IP, assume name
788    {
789        if ($CLI{'skiplookup'}) {
790            print("+ ERROR: -skiplookup set, but given name\n");
791            exit;
792        }
793        $ip = gethostbyname($ident);
794        if (   ($ip eq "")
795            && ($request{'whisker'}->{'proxy_host'} ne "")
796            )                    # can't resolve name to IP, but using proxy
797        {
798            $name = $ident;
799            $ip   = $name;
800        }
801        elsif (($ip eq "")
802            && ($request{'whisker'}->{'proxy_host'} eq "")) # can't resolve name to IP, no proxy set
803        {
804            nprint("+ ERROR: Cannot resolve hostname '$ident'\n");
805            return;
806        }
807        else {
808            use IO::Socket;
809            $ip = inet_ntoa($ip);
810            if (   ($ip !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/)
811                && ($ip ne ""))                             # trap for proxy...
812            {
813                nprint("+ ERROR: Invalid IP '$ip'\n\n");
814                return;
815            }
816            $name = $ident;
817        }
818    }
819    else                                                    # ident is IP, lookup name
820    {
821        if (   ($ident !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/)
822            && ($ident ne ""))                              # trap for proxy...
823        {
824            nprint("+ ERROR: Invalid IP '$ident'\n\n");
825            return;
826        }
827
828        $ip = $ident;
829        if (!$CLI{'skiplookup'}) {
830            use IO::Socket;
831            my $temp_ip = inet_aton($ip);
832            $name = gethostbyaddr($temp_ip, AF_INET);
833
834            # check reverse dns to avoid an inet_aton error
835            my $rdnsip = gethostbyname($name);
836            if ($rdnsip ne "") {
837                $rdnsip = inet_ntoa($rdnsip);
838                if ($ip ne $rdnsip) { $name = $ip; }    # Reverse DNS does not match
839            }
840            else { $name = $ip; }                       # Reverse DNS does not exist
841        }
842        if ($name eq "") { $name = $ip; }
843    }
844
845    # set displayname -- name takes precedence
846    if   ($name ne "") { $dn = $name; }
847    else               { $dn = $ip; }
848
849    return $name, $ip, $dn;
850}
851
852###############################################################################
853sub set_targets {
854    my ($hostlist, $portlist, $ssl, $root) = @_;
855    my $host_ctr = 1;
856    my @hosts    = split(/,/, $hostlist);
857    my @ports    = split(/,/, $portlist) if defined $portlist;
858    my (@checkhosts, @results, @marks);
859    my $defaultport = ($ssl) ? 443 : 80;
860
861    # Check for old style portlist and expand
862    my @newports;
863    foreach my $port (@ports) {
864        if ($port =~ /-/) {
865            my ($start, $end);
866            my @temp = split(/-/, $port);
867            $start = $temp[0];
868            $end   = $temp[1];
869            if ($start eq "") { $start = 0; }
870            if ($end   eq "") { $end   = 65535; }
871            if ($start > $end) {
872                nprint("+ ERROR port range $port doesn't make sense - assuming 80/tcp");
873                next;
874            }
875            for (my $i = $start ; $i <= $end ; $i++) {
876                push(@newports, $i);
877            }
878        }
879        else {
880            push(@newports, $port);
881        }
882    }
883    @ports = @newports;
884
885    nprint("- Getting targets", "v");
886    if (scalar(@ports) == 1) {
887
888        # Only one port is set, assume that is the default port
889        $defaultport = $ports[0];
890    }
891
892    # check whether it's a file or an entry
893    foreach my $host (@hosts) {
894        if (-e $host || $host eq "-") {
895            @results = parse_hostfile($host);
896            push(@checkhosts, @results);
897        }
898        else {
899            push(@checkhosts, $host);
900        }
901    }
902
903    # Now parse the list of checkhosts
904    foreach my $host (@checkhosts) {
905        my $defhost;
906        my $defport;
907        $host =~ s/\s+//g;
908        if (!defined $host) { next; }
909
910        # is it a URL?
911        if ($host =~ /^https?:\/\//) {
912            my @hostdata = LW2::uri_split($host);
913
914            $defhost = $hostdata[2];
915            $defport = $hostdata[3];
916
917            if ((!defined $root) && (defined $hostdata[0])) {
918                $root = $hostdata[0];
919                nprint("- Added -root value of '$root'", "d");
920            }
921        }
922        else {
923            my @h = split(/\:|\,/, $host);
924
925            $defhost = $h[0];
926            $defport = $h[1];
927        }
928
929        # Now skip through all ports if port hasn't been added
930        if ($defport eq "" && scalar(@ports) > 0) {
931
932            foreach $port (@ports) {
933                my $markhash = {};
934                $markhash->{ident} = $defhost;
935                $markhash->{port}  = $port;
936                nprint("- Target:$markhash->{ident} port:$markhash->{port}", "v", $markhash);
937                push(@marks, $markhash);
938            }
939        }
940        else {
941            if ($defport eq "") {
942                $defport = $defaultport;
943            }
944            my $markhash = {};
945            $markhash->{ident} = $defhost;
946            $markhash->{port}  = $defport;
947
948            nprint("- Target:$markhash->{ident} port:$markhash->{port}", "v", $markhash);
949            push(@marks, $markhash);
950        }
951    }
952
953    return @marks;
954}
955
956###############################################################################
957sub parse_hostfile {
958    my ($file) = @_;
959    my $nmap = 0;
960    my (@results, $hostdesc);
961
962    open(IN, $file) || die print STDERR "+ ERROR: Cannot open '$file':$@\n";
963    while (<IN>) {
964        my $found = 0;
965
966        # Check whether this is an nmap oG file
967        chomp;
968        if (/^# Nmap [0-9.]* scan initiated/) {
969            $nmap = 1;
970        }
971        s/\#.*$//;
972        if ($_ eq "") { next; }
973
974        # Parse for nmap files
975        if ($nmap == 1) {
976
977            # First get the host name
978            my @line = split(/ /);
979            my @name = split(/\(|\)/, $line[2]);
980            $hostdesc = ($name[1] ne "") ? $name[1] : $line[1];
981
982            # now parse the ports list
983            for (my $i = 3 ; $i <= $#line ; $i++) {
984                my @ports = split(/\//, $line[$i]);
985                if ($ports[1] eq "open" && $ports[4] eq "http") {
986                    $found = 1;
987                    $hostdesc .= ":" . $ports[0];
988                }
989            }
990        }
991        else {
992
993            # just add it to the list
994            $hostdesc = $_;
995            $found    = 1;
996        }
997        push(@results, $hostdesc) if ($found);
998    }
999    close(IN);
1000    return (@results);
1001}
1002
1003###############################################################################
1004sub load_databases {
1005    my @dbs = qw/db_404_strings  db_outdated  db_realms  db_tests  db_variables db_content_search/;
1006    my $prefix = $_[0];
1007
1008    unless (defined $prefix) {
1009        $prefix = "";
1010    }
1011
1012    # verify required files
1013    for my $file (@dbs) {
1014        if (!-r "$NIKTOCONFIG{PLUGINDIR}/$file") {
1015            die nprint("+ ERROR: Can't find/read required file \"$NIKTOCONFIG{PLUGINDIR}/$file\"");
1016        }
1017    }
1018
1019    for my $file (@dbs) {
1020        my $filename = $NIKTOCONFIG{PLUGINDIR} . "/" . $prefix . $file;
1021        if (!-r $filename) { next; }
1022        open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$!\n");
1023
1024        # db_tests
1025        if ($file eq 'db_tests') { push(@DBFILE, <IN>); next; }
1026
1027        # all the other files require per-line processing
1028        else {
1029            my @file;
1030
1031            # Cleanup
1032            while (<IN>) {
1033                chomp;
1034                $_ =~ s/#.*$//;
1035                $_ =~ s/\s+$//;
1036                $_ =~ s/^\s+//;
1037                if ($_ ne "") { push(@file, $_); }
1038            }
1039
1040            # db_variables
1041            if ($file eq 'db_variables') {
1042                foreach my $l (@file) {
1043                    if ($l =~ /^@/) {
1044                        my @temp = split(/=/, $l);
1045                        $VARIABLES{ $temp[0] } .= "$temp[1]";
1046                    }
1047                }
1048            }
1049
1050            # db_404_strings
1051            elsif ($file eq 'db_404_strings') {
1052                foreach my $l (@file) {
1053                    $ERRSTRINGS{$l} = 1;
1054                }
1055            }
1056
1057            # db_content_search
1058            elsif ($file eq 'db_content_search') {
1059                foreach my $l (@file) {
1060                    my @T = parse_csv($l);
1061                    $CONTENTSEARCH{ $T[0] }{'osvdb'}   = $T[1];
1062                    $CONTENTSEARCH{ $T[0] }{'string'}  = $T[2];
1063                    $CONTENTSEARCH{ $T[0] }{'message'} = $T[3];
1064                }
1065            }
1066
1067            # db_outdated
1068            elsif ($file eq 'db_outdated') {
1069                foreach my $l (@file) {
1070                    my @T = parse_csv($l);
1071                    $OVERS{ $T[1] }{ $T[2] } = $T[3];
1072                    $OVERS{ $T[1] }{tid} = $T[0];
1073                }
1074            }
1075
1076            # db_realms
1077            elsif ($file eq 'db_realms') {
1078                my $db_realms_ctr = keys %REALMS;
1079                foreach my $l (@file) {
1080                    my @t = parse_csv($l);
1081                    $REALMS{$db_realms_ctr}{tid}   = $t[0];
1082                    $REALMS{$db_realms_ctr}{realm} = $t[1];
1083                    $REALMS{$db_realms_ctr}{id}    = $t[2];
1084                    $REALMS{$db_realms_ctr}{pw}    = $t[3];
1085                    $REALMS{$db_realms_ctr}{msg}   = $t[4];
1086                    $db_realms_ctr++;
1087                }
1088
1089                # Cheat and add CLI directly to REALMS
1090                if (defined $CLI{'hostauth'}) {
1091                    my @x = split(/:/, $CLI{'hostauth'});
1092
1093                    $REALMS{$db_realms_ctr}{tid}   = "700500";
1094                    $REALMS{$db_realms_ctr}{realm} = (defined $x[2]) ? $x[2] : '@ANY';
1095                    $REALMS{$db_realms_ctr}{pw}    = $x[1];
1096                    $REALMS{$db_realms_ctr}{id}    = $x[0];
1097                    $REALMS{$db_realms_ctr}{msg}   = "Credentials provided by CLI.";
1098                }
1099            }
1100
1101            close(IN);
1102        }
1103    }
1104
1105    return;
1106}
1107
1108###############################################################################
1109sub dbcheck {
1110    my @dbs =
1111      qw/db_headers db_httpoptions db_multiple_index db_server_msgs db_subdomains db_favicon db_embedded db_404_strings  db_outdated  db_realms  db_tests  db_variables db_content_search/;
1112    my $prefix = $_[0];
1113    if ($prefix eq "")  { nprint "\n-->\tNikto Databases\n"; }
1114    if ($prefix eq "u") { nprint "\n-->\tUser Databases\n"; }
1115
1116    for my $file (@dbs) {
1117        my $filename = $NIKTOCONFIG{PLUGINDIR} . "/" . $prefix . $file;
1118        if (!-r $filename) { next; }
1119        open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$!\n");
1120        nprint "Syntax Check: $filename\n";
1121
1122        if ($file eq 'db_outdated') {
1123            foreach $line (<IN>) {
1124                $line =~ s/^\s+//;
1125                if ($line =~ /^\#/) { next; }
1126                chomp($line);
1127                if ($line eq "") { next; }
1128                my @L = parse_csv($line);
1129                if ($#L ne 3) { print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n"; next; }
1130                $ENTRIES{"$L[0]"}++;
1131            }
1132            foreach $entry (keys %ENTRIES) {
1133                if ($ENTRIES{$entry} > 1) {
1134                    print STDERR "\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry\n";
1135                }
1136            }
1137            nprint "\t" . keys(%ENTRIES) . " entries\n";
1138        }
1139        elsif ($file eq 'db_tests') {
1140            my %ENTRIES;
1141            foreach my $line (<IN>) {
1142                if ($line !~ /^\"/) { next; }
1143                my @L = parse_csv($line);
1144                if ($L[4] !~ /(GET|POST|TRACE|TRACK|OPTIONS|SEARCH|INDEX)/i) {
1145                    print STDERR "\t+ ERROR: Possibly invalid method: $L[4] on ($line)\n";
1146                }
1147                if ($L[5] eq "") { print STDERR "\t+ ERROR: blank conditional: $line"; next; }
1148                if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) {
1149                    print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n";
1150                    next;
1151                }
1152                if ($line !~ /^(\".*\",){11}\".*\"/) {
1153                    print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n";
1154                    next;
1155                }
1156                if (($L[3] =~ /^\@CG/) && ($L[3] !~ /^\@CGIDIRS/)) {
1157                    print STDERR "\t+ ERROR: Possible \@CGIDIRS misspelling: $line";
1158                }
1159                if ($L[1] =~ /[^0-9]/) { print STDERR "\t+ ERROR: Invalid OSVDB ID: $line"; }
1160                $ENTRIES{"$L[3],$L[4],$L[5],$L[6],$L[7],$L[8],$L[9],$L[11],$L[12]"}++;
1161                if ((count_fields($line, 1) ne 12) && (count_fields($line) ne '')) {
1162                    print STDERR "\t+ ERROR: Invalid syntax: $line\n";
1163                }
1164            }
1165            foreach $entry (keys %ENTRIES) {
1166                if ($ENTRIES{$entry} > 1) {
1167                    print STDERR "\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry\n";
1168                }
1169            }
1170            nprint "\t" . keys(%ENTRIES) . " entries\n";
1171        }
1172        elsif ($file eq 'db_variables') {
1173            my $ctr = 0;
1174            foreach $line (<IN>) {
1175                if ($line !~ /^\@/)         { next; }
1176                if ($line !~ /^\@.+\=.+$/i) { print STDERR "\t+ ERROR: Invalid syntax: $line\n"; }
1177                $ctr++;
1178            }
1179            nprint "\t$ctr entries\n";
1180        }
1181        elsif ($file eq 'db_realms') {
1182            my $ctr = 0;
1183            foreach $line (<IN>) {
1184                if ((count_fields($line, 1) ne 4) && (count_fields($line) ne '')) {
1185                    print STDERR "\t+ ERROR: Invalid syntax: $line";
1186                }
1187                $ctr++;
1188            }
1189            nprint "\t$ctr entries\n";
1190        }
1191        elsif ($file eq 'db_404_strings') {
1192            my $ctr = 0;
1193            foreach $line (<IN>) {
1194
1195                # not really any syntax to check
1196                $ctr++;
1197            }
1198            nprint "\t$ctr entries\n";
1199        }
1200        elsif ($file eq 'db_headers') {
1201            my $ctr = 0;
1202            foreach $line (<IN>) {
1203                if ((count_fields($line) ne 0) && (count_fields($line) ne '')) {
1204                    print STDERR "\t+ ERROR: Invalid syntax: $line";
1205                }
1206                $ctr++;
1207            }
1208            nprint "\t$ctr entries\n";
1209        }
1210        elsif ($file eq 'db_multiple_index') {
1211            my $ctr = 0;
1212            foreach $line (<IN>) {
1213                if ((count_fields($line) ne 0) && (count_fields($line) ne '')) {
1214                    print STDERR "\t+ ERROR: Invalid syntax: $line";
1215                }
1216                $ctr++;
1217            }
1218            nprint "\t$ctr entries\n";
1219        }
1220        else {
1221
1222            # It's a file of standard DB type, we can do this intelligently
1223            my @headers;
1224            my $ctr = 0, $fields = 0;
1225            foreach $line (<IN>) {
1226
1227                # first, grab the headers
1228                if ($fields == 0) {
1229                    $line =~ s/\#.*//;
1230                    next if ($line eq "");
1231                    @headers = parse_csv($line);
1232                    $fields  = $#headers;
1233                    next;
1234                }
1235                if (   (count_fields($line, 1) != $fields - 1)
1236                    && (count_fields($line) ne '')) {
1237                    print STDERR "\t+ ERROR: Invalid syntax: $line";
1238                }
1239                $ctr++;
1240            }
1241            nprint "\t$ctr entries\n";
1242        }
1243
1244        close(IN);
1245    }
1246
1247    if ($_[0] eq "") { dbcheck('u'); }    # do this once
1248
1249    nprint "\n";
1250    exit;
1251}
1252
1253###############################################################################
1254sub content_search {
1255    my $body   = shift;
1256    my $file   = shift;
1257    my $method = shift || "GET";
1258    my ($mark) = @_;
1259
1260    foreach my $testid (keys %CONTENTSEARCH) {
1261        if ($body =~ /$CONTENTSEARCH{$testid}{string}/) {
1262            my $outmessage = "$file: $CONTENTSEARCH{$testid}{'message'}";
1263            add_vulnerability($mark, $outmessage,
1264                              $CONTENTSEARCH{$testid}{testid},
1265                              $CONTENTSEARCH{$testid}{osvdb},
1266                              $method, $file);
1267        }
1268    }
1269
1270    return;
1271}
1272
1273###############################################################################
1274sub count_fields {
1275    my $line    = $_[0] || return;
1276    my $checkid = $_[1] || 0;
1277    if ($line !~ /^\"/) { return; }
1278    chomp($line);
1279    $line =~ s/\s+$//;
1280    if ($line eq '') { return; }
1281    my @L = parse_csv($line);
1282    if ($checkid && ($L[0] ne 'nikto_id') && (($L[0] =~ /[^0-9]/) || ($L[0] eq ''))) { return -1; }
1283    return $#L;
1284}
1285
1286###############################################################################
1287sub get_banner {
1288    my ($mark) = @_;
1289    my %headers;
1290
1291    (my $res, $CONTENT) = nfetch($mark, "/", "GET", "", \%headers, "", "Get Banner");
1292
1293    return $headers{server};
1294}
1295
1296###############################################################################
1297sub port_check {
1298    my ($hostname, $ip, $port) = @_;
1299    my (%m, %headers);
1300
1301    # Check SKIPPORTS
1302    if ($NIKTOCONFIG{'SKIPPORTS'} =~ /\b$port\b/) {
1303        nprint("+ ERROR: SKIPPORTS (nikto.conf) contains $port -- not checking");
1304        return 0;
1305    }
1306
1307    $m->{hostname} = $hostname;
1308    $m->{ip}       = $ip;
1309    $m->{port}     = $port;
1310    $m->{ssl}      = 0;
1311
1312    my @checktypes = ('HTTP', 'HTTPS');
1313    if ($CLI{'ssl'})   { shift(@checktypes); }
1314    if ($CLI{'nossl'}) { pop(@checktypes); }
1315
1316    foreach my $method (split(/ /, $NIKTOCONFIG{CHECKMETHODS})) {
1317        $request{'whisker'}->{'method'} = $method;
1318        foreach my $checkssl (@checktypes) {
1319            nprint("- Checking for $checkssl on port $ip:$port, using $method", "v", $m);
1320            $m->{ssl} = ($checkssl eq "HTTP") ? 0 : 1;
1321            proxy_check($m);
1322            my ($res, $content) = nfetch($m, "/", $method, "", \%headers, "", "Port Check");
1323
1324            if ($res) {
1325
1326      # this will fix for some Apaches that are smart enough to answer non ssl reqs on an ssl server
1327                if (defined $content
1328                    && $content =~ /speaking plain HTTP to an SSL/) {
1329                    dump_var("Result Hash", \%result);
1330                    next;
1331                }
1332                nprint("- $checkssl Server found: $ip:$port \t$headers{server}", "d", $m);
1333                return $m->{ssl} + 1;
1334            }
1335        }
1336    }
1337
1338    nprint("+ No web server found on $ip:$port");
1339    nprint("---------------------------------------------------------------------------");
1340
1341    return 0;
1342}
1343
1344###############################################################################
1345sub load_plugins {
1346    my @pluginlist = dirlist("$NIKTOCONFIG{PLUGINDIR}", '\.plugin$');
1347    my @all_names;
1348
1349    # populate plugin macros
1350    $NIKTOCONFIG{'@@NONE'} = "";
1351
1352    # Check if running plugins is NONE - if so, don't bother initalising
1353    # plugins
1354    if ($CLI{'plugins'} eq '@@NONE') {
1355        return;
1356    }
1357
1358    foreach my $plugin (@pluginlist) {
1359        my $plugin_name = $plugin;
1360        $plugin_name =~ s/\.plugin$//;
1361        my $plugin_init = $plugin_name . "_init";
1362        eval { require "$NIKTOCONFIG{PLUGINDIR}/$plugin"; };
1363        if ($@) {
1364            nprint("- Could not load or parse plugin: $plugin_name\n Error: ");
1365            warn $@;
1366            nprint("- The plugin could not be run.");
1367        }
1368        else {
1369            nprint("- Initialising plugin $plugin_name", "v");
1370
1371            # Call initialisation method
1372            if (defined &$plugin_init) {
1373                my $pluginhash = &$plugin_init;
1374
1375                # Add default weights if not already assigned
1376                $pluginhash->{recon_weight}  = 50 unless (defined $pluginhash->{recon_weight});
1377                $pluginhash->{scan_weight}   = 50 unless (defined $pluginhash->{scan_weight});
1378                $pluginhash->{report_weight} = 50 unless (defined $pluginhash->{report_weight});
1379                $pluginhash->{postfetch_weight} = 50 unless (defined $pluginhash->{postfetch_weight});
1380                $pluginhash->{postfetch_weight} = 50 unless (defined $pluginhash->{postfetch_weight});
1381                push(@all_names, $pluginhash->{name});
1382
1383                push(@PLUGINS, $pluginhash);
1384                nprint("- Loaded \"$pluginhash->{full_name}\" plugin.", "v");
1385            }
1386        }
1387    }
1388    $NIKTOCONFIG{'@@ALL'} = join(';', @all_names);
1389    my @torun = split(/;/, expand_pluginlist($CLI{'plugins'}, 0));
1390
1391    # Second pass to ensure that @@ALL is configured
1392    foreach my $plugin (@PLUGINS) {
1393
1394        # Check that the plugin is to be run
1395        # Perl doesn't allow us to use "in", pity
1396        foreach my $torun_plugin (@torun) {
1397
1398            # split up into parameters
1399            my $name = my $suffix = $torun_plugin;
1400            if ($torun_plugin =~ /\(/) {
1401                $name   =~ s/(.*)(\(.*\))/$1/;
1402                $suffix =~ s/(.*)(\(.*\))/$2/;
1403            }
1404            else {
1405                $name   = $torun_plugin;
1406                $suffix = "";
1407            }
1408            if ($plugin->{'name'} =~ /$name/i) {
1409                $plugin->{'run'} = 1;
1410
1411                # Create parameters
1412                if ($suffix ne "") {
1413                    my $parameters = {};
1414                    $suffix =~ s/(\()(.*[^\)])(\)?)/$2/;
1415                    foreach my $parameter (split(/,/, $suffix)) {
1416                        if ($parameter !~ /:/) {
1417                            $parameters->{$parameter} = 1;
1418                        }
1419                        else {
1420                            my $key = my $value = $parameter;
1421                            $key   =~ s/:.*//;
1422                            $value =~ s/.*://;
1423                            $parameters->{$key} = $value;
1424                        }
1425                    }
1426                    $plugin->{'parameters'} = $parameters;
1427                }
1428            }
1429        }
1430    }
1431   
1432    # For speed in future, create a hash of active plugins ordered by plugin weight, for
1433    # each type of plugin
1434    my (@rplugins, @splugins, @repplugins, @pfplugins, @prefplugins);
1435    my (@rsorted, @ssorted, @repsorted, @pfsorted, @prefsorted);
1436    foreach my $plugin (@PLUGINS) {
1437        if ($plugin->{'run'} == 1) {
1438            if (defined $plugin->{recon_method}) {
1439                push(@rplugins, $plugin);
1440            }
1441            if (defined $plugin->{scan_method}) {
1442                push(@splugins, $plugin);
1443            }
1444            if (defined $plugin->{report_item}) {
1445                push(@repplugins, $plugin);
1446            }
1447            if (defined $plugin->{prefetch_method}) {
1448                push(@prefplugins, $plugin);
1449            }
1450            if (defined $plugin->{postfetch_method}) {
1451                push(@pfplugins, $plugin);
1452            }
1453        }
1454    }
1455   
1456    # Now sort each array by weight
1457    @rsorted = sort { $a->{'weight'} <=> $b->{'weight'} } @rplugins;
1458    $PLUGINORDER{'recon'} = \@rsorted;
1459    @ssorted = sort { $a->{'weight'} <=> $b->{'weight'} } @splugins;
1460    $PLUGINORDER{'scan'} = \@ssorted;
1461    @repsorted = sort { $a->{'weight'} <=> $b->{'weight'} } @repplugins;
1462    $PLUGINORDER{'report'} = \@repsorted;
1463    @prefsorted = sort { $a->{'weight'} <=> $b->{'weight'} } @prefplugins;
1464    $PLUGINORDER{'prefetch'} = \@prefsorted;
1465    @pfsorted = sort { $a->{'weight'} <=> $b->{'weight'} } @pfplugins;
1466    $PLUGINORDER{'postfetch'} = \@pfsorted;
1467}
1468
1469sub run_hooks {
1470    my ($mark, $type, $request, $result) = @_;
1471   
1472    foreach my $plugin (@{$PLUGINORDER{$type}}) {
1473        my ($run) = 1;
1474                 
1475                # first check for conditionals
1476        my $condition = $plugin->{$type . '_cond'};
1477                if (defined $plugin->{$type . '_cond'}) { 
1478                    # Evaluate condition
1479                    $run = eval($condition);
1480                }
1481       
1482        next if ($run != 1) ;
1483       
1484        my $oldverbose = $OUTPUT{'verbose'};
1485        my $olddebug   = $OUTPUT{'debug'};
1486        my $olderrors  = $OUTPUT{'errors'};
1487        nprint("- Running $type for \"$plugin->{full_name}\" plugin", "v");
1488        if (defined $plugin->{'parameters'}->{'verbose'}
1489            && $plugin->{'parameters'}->{'verbose'} == 1) {
1490            $OUTPUT{'verbose'} = 1;
1491        }
1492        if (defined $plugin->{'parameters'}->{'debug'}
1493            && $plugin->{'parameters'}->{'debug'} == 1) {
1494            $OUTPUT{'debug'} = 1;
1495        }
1496        &{ $plugin->{$type . '_method'} }($mark, $request, $result, $plugin->{'parameters'});
1497        $OUTPUT{'verbose'} = $oldverbose;
1498        $OUTPUT{'debug'}   = $olddebug;
1499        $OUTPUT{'errors'}  = $olderrors;
1500    }
1501   
1502    return $request, $result;
1503}
1504
1505###############################################################################
1506sub report_head {
1507    my ($format, $file) = @_;
1508    nprint("- Opening reports", "v");
1509
1510    # For tuning set up a list of report methods, formats and handles
1511
1512    # This is a frig until I can think of a better way of achieving it
1513    foreach my $i (1 .. 100) {
1514        foreach my $plugin (@PLUGINS) {
1515            if ($plugin->{run} && defined $plugin->{report_item} && $plugin->{report_weight} == $i)
1516            {
1517                my $run = 1;
1518
1519                # first check for conditionals
1520                if (defined $plugin->{report_format}) {
1521
1522                    # Evaluate condition
1523                    $run = ($format eq $plugin->{report_format});
1524                }
1525                if ($run) {
1526                    nprint("- Opening report for \"$plugin->{full_name}\" plugin", "v");
1527                    my $handle;
1528                    if (defined $plugin->{report_head}) {
1529                        $handle = &{ $plugin->{report_head} }($file);
1530                    }
1531
1532                    # Now store this
1533                    my $report_entry = { host_start => $plugin->{report_host_start},
1534                                         host_end   => $plugin->{report_host_end},
1535                                         item       => $plugin->{report_item},
1536                                         close      => $plugin->{report_close},
1537                                         handle     => $handle,
1538                                         };
1539                    push(@REPORTS, $report_entry);
1540                }
1541            }
1542        }
1543    }
1544    return;
1545}
1546
1547###############################################################################
1548sub report_host_start {
1549    my ($mark) = @_;
1550
1551    # Go through all reporting modules
1552    foreach my $reporter (@REPORTS) {
1553        if (defined $reporter->{host_start}) {
1554            &{ $reporter->{host_start} }($reporter->{handle}, $mark);
1555        }
1556    }
1557}
1558
1559###############################################################################
1560sub report_host_end {
1561    my ($mark) = @_;
1562
1563    # Go through all reporting modules
1564    foreach my $reporter (@REPORTS) {
1565        if (defined $reporter->{host_end}) {
1566            &{ $reporter->{host_end} }($reporter->{handle}, $mark);
1567        }
1568    }
1569}
1570
1571###############################################################################
1572sub report_item {
1573    my ($mark, $item) = @_;
1574
1575    # Go through all reporting modules
1576    foreach my $reporter (@REPORTS) {
1577        if (defined $reporter->{item}) {
1578            &{ $reporter->{item} }($reporter->{handle}, $mark, $item);
1579        }
1580    }
1581}
1582
1583###############################################################################
1584sub report_close {
1585
1586    # Go through all reporting modules
1587    foreach my $reporter (@REPORTS) {
1588        if (defined $reporter->{close}) {
1589            &{ $reporter->{close} }($reporter->{handle});
1590        }
1591    }
1592}
1593
1594###############################################################################
1595sub check_updates {
1596    LW2::http_init_request(\%request);
1597    my (%REMOTE, %LOCAL, @DBTOGET) = ();
1598    my ($pluginmsg, $remotemsg) = "";
1599    my $code_updates = 0;
1600    my $serverdir    = "/nikto/UPDATES/$NIKTO{'version'}";
1601
1602    # set up our mark
1603    my %mark = ('ident' => 'www.cirt.net',
1604                'ssl'   => 0,
1605                'port'  => 80
1606                );
1607    ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('www.cirt.net');
1608
1609    for (my $i = 0 ; $i <= $#ARGV ; $i++) {
1610        if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) {
1611            $CLI{'useproxy'} = 1;
1612            last;
1613        }
1614    }
1615
1616    # retrieve versions file
1617    (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1618    if ($RES eq 407) {
1619        if ($NIKTOCONFIG{PROXYUSER} eq "") {
1620            $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
1621            $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
1622        }
1623
1624        # and try again
1625        ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1626    }
1627
1628    if ($RES eq "") {
1629        ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1630    }
1631
1632    if ($RES ne 200) {
1633        print STDERR
1634          "+ ERROR ($RES): Unable to get $request{'whisker'}->{'host'}$serverdir/versions.txt\n";
1635        exit;
1636    }
1637
1638    # make hash
1639    for (split(/\n/, $CONTENT)) {
1640        my @l = parse_csv($_);
1641        if ($_ =~ /^msg/) {
1642            $remotemsg = "$l[1]";
1643            next;
1644        }
1645        $REMOTE{ $l[0] } = $l[1];
1646    }
1647
1648    # get local versions of plugins/dbs
1649    my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, "");
1650
1651    foreach my $file (@NIKTOFILES) {
1652        my $v = "";
1653        open(LOCAL, "<$NIKTOCONFIG{PLUGINDIR}/$file")
1654          || print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$file' for read: $@\n";
1655        my @l = <LOCAL>;
1656        close(LOCAL);
1657
1658        my @VERS = grep(/^#VERSION/, @l);
1659        chomp($VERS[0]);
1660        $LOCAL{$file} = (parse_csv($VERS[0]))[1];
1661    }
1662
1663    # check main nikto versions
1664    foreach my $remotefile (keys %REMOTE) {
1665        if ($remotefile eq "nikto") {
1666            if ($REMOTE{$remotefile} > $NIKTO{'version'}) {
1667                nprint
1668                  "+ Nikto has been updated to $REMOTE{$remotefile}, local copy is $NIKTO{'version'}\n";
1669                nprint
1670                  "+ No update has taken place. Please upgrade Nikto by visiting http://$server/\n";
1671                if ($remotemsg ne "") {
1672                    nprint "+ $server message: $remotemsg\n";
1673                }
1674                exit;
1675            }
1676            next;
1677        }
1678
1679        if (   ($LOCAL{$remotefile} eq "")
1680            || ($REMOTE{$remotefile} > $LOCAL{$remotefile})) {
1681            push(@DBTOGET, $remotefile);
1682            if ($remotefile !~ /^db_/) { $code_updates = 1; }
1683        }
1684        elsif ($REMOTE{$remotefile} < $LOCAL{$remotefile}) {
1685            print STDERR
1686              "+ ERROR: Local '$remotefile' (ver $LOCAL{$remotefile}) is NEWER than remote (ver $REMOTE{$remotefile}).\n";
1687        }
1688    }
1689
1690    # replace local files if updated
1691    foreach my $toget (@DBTOGET) {
1692        nprint "+ Retrieving '$toget'\n";
1693        (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/$toget", "GET");
1694        if ($RES ne 200) {
1695            print STDERR "+ ERROR: Unable to get $server$serverdir/$toget\n";
1696            exit;
1697        }
1698        if ($CONTENT ne "") {
1699            open(OUT, ">$NIKTOCONFIG{PLUGINDIR}/$toget")
1700              || die print STDERR
1701              "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$toget' for write: $@\n";
1702            print OUT $CONTENT;
1703            close(OUT);
1704        }
1705    }
1706
1707    # CHANGES file
1708    if ($code_updates) {
1709        nprint "+ Retrieving 'CHANGES.txt'\n";
1710        (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/CHANGES.txt", "GET");
1711        if (($CONTENT ne "") && ($RES eq 200)) {
1712            open(OUT, ">$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt")
1713              || die print STDERR
1714              "+ ERROR: Unable to open '$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt' for write: $@\n";
1715            print OUT $CONTENT;
1716            close(OUT);
1717        }
1718    }
1719
1720    if ($#DBTOGET < 0) { nprint "+ No updates required.\n"; }
1721    if ($remotemsg ne "") { nprint "+ $server message: $remotemsg\n"; }
1722    exit;
1723}
1724
1725###############################################################################
1726# do_auth
1727# Inputs: $request, $result
1728# Returns: $request
1729sub do_auth {
1730    my $request  = $_[0];
1731    my $result   = $_[1];
1732    my $mark     = $_[2];
1733    my $authtype = "basic";
1734    my $realm    = "";
1735
1736    unless (defined $result{'www-authenticate'}) {
1737        nprint("+ ERROR: No authentication header defined");
1738    }
1739
1740    # Split up www-authenticate to realm and method
1741    my @authenticate = split(/ /, $result{'www-authenticate'});
1742    if ($#authenticate == 0) {    # Only one parameter: realm
1743        $realm = $authenticate[0];
1744        if ($realm =~ /^ntlm/i) {
1745            $realm    = "";
1746            $authtype = $authenticate[0];
1747        }
1748    }
1749    else {
1750        $authtype = $authenticate[0];
1751        $realm    = $authenticate[1];
1752        $realm =~ s/^realm=//;
1753    }
1754
1755    # Now we have this we can try guessing the password
1756    foreach my $REALM (keys %REALMS) {
1757        next unless ($realm =~ /$REALMS{$REALM}{realm}/i || $REALMS{$REALM}{realm} eq '@ANY');
1758        if (($REALMS{$REALM}{id} eq "") && ($REALMS{$REALM}{pw} eq "")) {
1759            my $uridir = $request{'whisker'}->{'uri'};
1760            $uridir =~ s#/[^/]*$#/#g;
1761            add_vulnerability(
1762                $mark,
1763                "Blank credentials found at $uridir ($request{whisker}->{uri}), $REALMS{$REALM}{realm}: $REALMS{$REALM}{msg}",
1764                $REALMS{$REALM}{tid},
1765                0,
1766                "GET",
1767                $uridir,
1768                \%result
1769                );
1770        }
1771        else {
1772            my $save_auth = $result{'www-authenticate'};
1773            LW2::http_close(\%request);    # force-close any old connections
1774            LW2::auth_set($authtype, \%request, $REALMS{$REALM}{id}, $REALMS{$REALM}{pw});
1775
1776            # Path to fix short reads
1777            $request{'whisker'}->{allow_short_reads} = 1;
1778            LW2::http_fixup_request(\%request);
1779            if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1780            LW2::http_do_request_timeout(\%request, \%result);    # test auth
1781            $NIKTO{'totalrequests'}++;
1782            dump_var("Auth Request",  \%request);
1783            dump_var("Auth Response", \%result);
1784            if ($result{'www-authenticate'} =~ /^ntlm/i) {
1785
1786                # Deal with ntlm
1787                my @ntlm_x = split(/ /, $result{'www-authenticate'});
1788                if ($#ntlm_x == 1) {
1789                    LW2::http_do_request_timeout(\%request, \%result);
1790                    $NIKTO{'totalrequests'}++;
1791                }
1792            }
1793            if ($result{'www-authenticate'} eq ""
1794                && !defined $result{'whisker'}->{'error'}) {
1795                unless (   $REALMS{$REALM}{tid} == 700500
1796                        || $REALMS{$REALM}{checked} == 1) {
1797                    my $uridir = $request{'whisker'}->{'uri'};
1798                    $uridir =~ s#/[^/]*$#/#g;
1799                    add_vulnerability(
1800                        $mark,
1801                        "Default account found for '$realm' at $uridir ($request{whisker}->{uri}) (ID '$REALMS{$REALM}{id}', PW '$REALMS{$REALM}{pw}'). $REALMS{$REALM}{msg}",
1802                        $REALMS{$REALM}{tid},
1803                        0,
1804                        "GET",
1805                        $uridir,
1806                        \%result
1807                        );
1808                    $REALMS{$REALM}{checked} = 1;
1809                }
1810
1811                # Finally repeat the check
1812                LW2::http_do_request_timeout(\%request, \%result);    # test auth
1813                $NIKTO{'totalrequests'}++;
1814
1815                # and leave
1816                last;
1817            }
1818            else { $result->{'www-authenticate'} = $save_auth; }
1819        }
1820    }
1821    LW2::auth_unset(\%request);
1822    return $result;
1823}
1824
1825###############################################################################
1826# read_data ( prompt, mode )
1827# read STDIN data from the user
1828# portions of this (POSIX code) were taken from the
1829# Term::ReadPassword module by Tom Phoenix <rootbeer@redcat.com> (many thanks).
1830# it has been modified to not require Term::ReadLine, but still requires
1831# POSIX::Termios of it's a POSIX machine
1832###############################################################################
1833sub read_data {
1834    if ($NIKTOCONFIG{PROMPTS} =~ /no/i) { return; }
1835    my ($prompt, $mode, $POSIX) = @_;
1836    my $input = "";
1837
1838    if   ($^O =~ /Win32/) { $POSIX = 0; }
1839    else                  { $POSIX = 1; }
1840
1841    my %SPECIAL = ("\x03" => 'INT',    # Control-C, Interrupt
1842                   "\x08" => 'DEL',    # Backspace
1843                   "\x7f" => 'DEL',    # Delete
1844                   "\x0d" => 'ENT',    # CR, Enter
1845                   "\x0a" => 'ENT',    # LF, Enter
1846                   );
1847
1848    # if we're on a non-POSIX machine we can't not-echo the
1849    # characters, so just use getc to avoid the dependency on
1850    # POSIX::Termios. We would be best to get rid of this
1851    # entirely and use another way...
1852
1853    if ($POSIX) {
1854        local (*TTY, *TTYOUT);
1855        open TTY,    "<&STDIN"   or return;
1856        open TTYOUT, ">>&STDOUT" or return;
1857
1858        # Don't buffer it!
1859        select((select(TTYOUT), $| = 1)[0]);
1860        print TTYOUT $prompt;
1861
1862        # Remember where everything was
1863        my $fd_tty = fileno(TTY);
1864        my $term   = POSIX::Termios->new();
1865        $term->getattr($fd_tty);
1866        my $original_flags = $term->getlflag();
1867
1868        if ($mode eq "noecho") {
1869            my $new_flags = $original_flags & ~(ISIG | ECHO | ICANON);
1870            $term->setlflag($new_flags);
1871        }
1872        $term->setattr($fd_tty, TCSAFLUSH);
1873      KEYSTROKE:
1874        while (1) {
1875            my $new_keys = '';
1876            my $count = sysread(TTY, $new_keys, 99);
1877            if ($count) {
1878                for my $new_key (split //, $new_keys) {
1879                    if (my $meaning = $SPECIAL{$new_key}) {
1880                        if    ($meaning eq 'ENT') { last KEYSTROKE; }
1881                        elsif ($meaning eq 'DEL') { chop $input; }
1882                        elsif ($meaning eq 'INT') { last KEYSTROKE; }
1883                        else                      { $input .= $new_key; }
1884                    }
1885                    else { $input .= $new_key; }
1886                }
1887            }
1888            else { last KEYSTROKE; }
1889        }
1890
1891        # Done with waiting for input. Let's not leave the cursor sitting
1892        # there, after the prompt.
1893        print TTY "\n";
1894        nprint "\n";
1895
1896        # Let's put everything back where we found it.
1897        $term->setlflag($original_flags);
1898        $term->setattr($fd_tty, TCSAFLUSH);
1899        close(TTY);
1900        close(TTYOUT);
1901    }
1902    else    # non-POSIX
1903    {
1904        print $prompt;
1905        $input = <STDIN>;
1906        chomp($input);
1907    }
1908
1909    return $input;
1910}
1911
1912###############################################################################
1913sub proxy_setup {
1914    if (!$CLI{'useproxy'}) { return; }
1915
1916    # HTTP proxy
1917    $request{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{PROXYHOST};
1918    $request{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{PROXYPORT};
1919
1920    return;
1921}
1922
1923###############################################################################
1924sub proxy_check {
1925    my ($mark) = @_;
1926
1927    if (defined $request{'whisker'}->{'proxy_host'} && $CLI{'useproxy'})    # proxy is set up
1928    {
1929        LW2::http_close(\%request);    # force-close any old connections
1930        setup_hash(\%request, $mark, "Proxy Check");
1931        $request{'whisker'}->{'method'} = "GET";
1932        $request{'whisker'}->{'uri'}    = "/";
1933
1934        LW2::http_fixup_request(\%request);
1935
1936        if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1937        LW2::http_do_request_timeout(\%request, \%result);
1938        $NIKTO{'totalrequests'}++;
1939        dump_var("Request Hash", \%request);
1940        dump_var("Result Hash",  \%result);
1941
1942        # First check that we can connect to the proxy
1943        if (exists $result{'whisker'}{'error'}) {
1944            if ($result{'whisker'}{'error'} =~ /Transport endpoint is not connected/) {
1945                nprint("+ ERROR: Could not connect to the defined proxy $NIKTOCONFIG{PROXYHOST}");
1946                exit 1;
1947            }
1948            nprint("+ ERROR: Proxy error: $result{'whisker'}{'error'}", "v");
1949        }
1950        if ($result{'whisker'}{'code'} eq "407")    # proxy requires auth
1951        {
1952
1953            # have id/pw?
1954            if ($NIKTOCONFIG{PROXYUSER} eq "") {
1955                $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
1956                $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
1957            }
1958            if ($result{'proxy-authenticate'} !~ /Basic/i) {
1959                my @x = split(/ /, $result{'proxy-authenticate'});
1960                nprint(
1961                    "+ Proxy server uses '$x[0]' rather than 'Basic' authentication. $NIKTO{'name'} $NIKTO{'version'} can't do that."
1962                    );
1963                exit;
1964            }
1965
1966            # test it...
1967            LW2::http_close(\%request);    # force-close any old connections
1968            LW2::auth_set("proxy-basic", \%request, $NIKTOCONFIG{PROXYUSER},
1969                          $NIKTOCONFIG{PROXYPASS});    # set auth
1970            LW2::http_fixup_request(\%request);
1971            if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1972            LW2::http_do_request_timeout(\%request, \%result);
1973            $NIKTO{'totalrequests'}++;
1974            dump_var("Request Hash", \%request);
1975            dump_var("Result Hash",  \%result);
1976
1977            if ($result{'proxy-authenticate'} ne "") {
1978                my @pauthinfo  = split(/ /, $result{'proxy-authenticate'});
1979                my @pauthinfo2 = split(/=/, $result{'proxy-authenticate'});
1980                $pauthinfo2[1] =~ s/^\"//;
1981                $pauthinfo2[1] =~ s/\"$//;
1982                nprint(
1983                    "+ Proxy requires authentication for '$pauthinfo[0]' realm '$pauthinfo2[1]', unable to authenticate."
1984                    );
1985                exit;
1986            }
1987            else { nprint("- Successfully authenticated to proxy.", "v"); }
1988        }
1989    }
1990
1991    return;
1992}
1993
1994###############################################################################
1995sub dirlist {
1996    my $DIR     = $_[0] || return;
1997    my $PATTERN = $_[1] || "";
1998    my @FILES_TMP = ();
1999
2000    opendir(DIRECTORY, $DIR) || die print STDERR "+ ERROR: Can't open directory '$DIR': $@";
2001    foreach my $file (readdir(DIRECTORY)) {
2002        if ($file =~ /^\./) { next; }    # skip hidden files, '.' and '..'
2003        if ($PATTERN ne "") {
2004            if ($file =~ /$PATTERN/) { push(@FILES_TMP, $file); }
2005        }
2006        else { push(@FILES_TMP, $file); }
2007    }
2008    closedir(DIRECTORY);
2009
2010    return @FILES_TMP;
2011}
2012
2013#######################################################################
2014sub dump_var {
2015    return if !$OUTPUT{'debug'};
2016    my $msg     = $_[0];
2017    my %hash_in = %{ $_[1] };
2018
2019    my $display = LW2::dump('', \%hash_in);
2020    $display =~ s/^\$/'$msg'/;
2021    if ($OUTPUT{'scrub'}) {
2022        $display =~ s/'host' => '.*',/'host' => 'example.com',/g;
2023        $display =~ s/'Host' => '.*'/'host' => 'example.com'/g;
2024    }
2025    nprint($display, "d");
2026    return;
2027}
2028
2029######################################################################
2030sub content_present {
2031    my $result = FALSE;
2032    my $res    = $_[0];
2033
2034    # perform an extra check just in case the web server lies about finds
2035    # basically assume that the value for a non-extension is the true
2036    # code for "File not Found".
2037    if ($res ne $FoF{NONE}{response}) {
2038        foreach $found (split(' ', $VARIABLES{"\@HTTPFOUND"})) {
2039            if ($res eq $found) {
2040                $result = TRUE;
2041            }
2042        }
2043    }
2044
2045    return $result;
2046}
2047
2048#######################################################################
2049sub fetch {
2050    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
2051    LW2::http_close(\%request);    # force-close any old connections
2052    $request{'whisker'}->{'uri'} = $_[0];
2053    if (defined $CLI{'root'}) {
2054        $request{'whisker'}->{'uri'} = $CLI{'root'} . $_[0];    # prepend -root option value
2055    }
2056
2057    # Set testid in UA
2058    my $temp_ua = $request{'User-Agent'};
2059    my $testid  = $_[3];
2060    $request{'User-Agent'} =~ s/\@TESTID/$testid/g;
2061
2062    $request{'whisker'}->{'method'} = $_[1];
2063    delete $request{'whisker'}->{'data'};
2064    delete $request{'Content-Encoding'};
2065    delete $request{'Content-Length'};
2066    my $header_hash = $_[3];
2067
2068    if (defined $_[2]) {
2069        my $r = $_[2];
2070        $r =~ s/\\\"/\"/g;
2071        $request{'whisker'}->{'data'} = $r;
2072    }
2073
2074    # check for extra HTTP headers
2075    if (defined $_[3]) {
2076
2077        # loop through the hash ref passed and add each header to request
2078        while (my ($key, $value) = each(%$header_hash)) {
2079            $request{$key} = $value;
2080        }
2081    }
2082    unless ($_[4]) { LW2::http_fixup_request(\%request); }
2083
2084    # Cache
2085    if (   defined($CACHE{ $request{'whisker'}->{'uri'} })
2086        && !defined($CLI{'nocache'})
2087        && ($CACHE{ $request{'whisker'}->{'uri'} }{'method'} eq $request{'whisker'}->{'method'})) {
2088
2089        # Get from cache
2090        nprint("- Retrieving $request{'whisker'}->{'uri'} from cache.", "v");
2091        $result{'whisker'}->{'code'} = $CACHE{ $request{'whisker'}->{'uri'} }{'res'};
2092        $result{'whisker'}->{'data'} = $CACHE{ $request{'whisker'}->{'uri'} }{'content'};
2093    }
2094    else {
2095        LW2::http_do_request_timeout(\%request, \%result);
2096        $NIKTO{'totalrequests'}++;
2097        unless (defined $CLI{'nocache'}) {
2098            $CACHE{ $request{'whisker'}->{'uri'} }{'method'}  = $request{'whisker'}->{'method'};
2099            $CACHE{ $request{'whisker'}->{'uri'} }{'res'}     = $result{'whisker'}->{'code'};
2100            $CACHE{ $request{'whisker'}->{'uri'} }{'content'} = $result{'whisker'}->{'data'};
2101        }
2102        if ($OUTPUT{'debug'}) {
2103            dump_var("Request Hash", \%request);
2104            dump_var("Result Hash",  \%result);
2105        }
2106    }
2107
2108    if (   defined $CLI{'display'}
2109        && ($CLI{'display'} =~ /2/)
2110        && (defined($result{'whisker'}->{'cookies'}))) {
2111        foreach my $c (@{ $result{'whisker'}->{'cookies'} }) {
2112            nprint("+ $request{'whisker'}->{'uri'} sent cookie: $c");
2113        }
2114    }
2115
2116    # Clean up extra headers
2117    if (defined $_[3]) {
2118        while (my ($key, $value) = each(%$header_hash)) {
2119            delete $request{$key};
2120        }
2121    }
2122
2123    # content search
2124    content_search($result{'whisker'}->{'data'},
2125                   $request{'whisker'}->{'uri'},
2126                   $request{'whisker'}->{'method'});
2127
2128    $request{'User-Agent'} = $temp_ua;    # reset UA
2129    return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'};
2130}
2131
2132#######################################################################
2133sub setup_hash {
2134    my ($reqhash, $mark, $testid) = @_;
2135
2136    # Do the standard set up for the hash
2137    LW2::http_init_request($reqhash);
2138    $reqhash->{'whisker'}->{'ssl_save_info'}              = 1;
2139    $reqhash->{'whisker'}->{'lowercase_incoming_headers'} = 1;
2140    $reqhash->{'whisker'}->{'timeout'}                    = $NIKTO{'timeout'};
2141    if (defined $CLI{'evasion'}) {
2142        $reqhash->{'whisker'}->{'encode_anti_ids'} = $CLI{'evasion'};
2143    }
2144    $reqhash->{'User-Agent'} = $NIKTO{'useragent'};
2145    $reqhash->{'User-Agent'} =~ s/\@TESTID/$testid/g;
2146    $reqhash->{'whisker'}->{'retry'} = 0;
2147    $reqhash->{'whisker'}->{'host'} = $mark->{'hostname'} || $mark->{'ip'};
2148
2149    if ($mark->{'vhost'}) {
2150        $request{'Host'} = $mark->{'vhost'};
2151    }
2152    $reqhash->{'whisker'}->{'port'} = $mark->{'port'};
2153    $reqhash->{'whisker'}->{'ssl'}  = $mark->{'ssl'};
2154
2155    # Proxy stuff
2156    if (defined $NIKTOCONFIG{PROXYHOST} && defined $CLI{'useproxy'}) {
2157        $reqhash->{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{'PROXYHOST'};
2158        $reqhash->{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{'PROXYPORT'};
2159        LW2::auth_set("proxy-basic", $reqhash, $NIKTOCONFIG{'PROXYUSER'},
2160                      $NIKTOCONFIG{'PROXYPASS'});
2161    }
2162
2163    return $reqhash;
2164}
2165
2166#######################################################################
2167sub nfetch {
2168    my ($mark, $uri, $method, $data, $headers, $flags, $testid) = @_;
2169    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
2170    my (%request, %result);
2171    setup_hash(\%request, $mark, $testid);
2172
2173    $request{'whisker'}->{'uri'} = $uri;
2174    if (defined $CLI{'root'}) {
2175        $request{'whisker'}->{'uri'} = $CLI{'root'} . $uri;    # prepend -root option value
2176    }
2177    $request{'whisker'}->{'method'} = $method;
2178    my $header_hash = $headers;
2179
2180    if (defined $data) {
2181        my $r = $data;
2182        $r =~ s/\\\"/\"/g;
2183        $request{'whisker'}->{'data'} = $r;
2184    }
2185
2186    # check for extra HTTP headers
2187    if (defined $headers) {
2188
2189        # loop through the hash ref passed and add each header to request
2190        while (my ($key, $value) = each(%$header_hash)) {
2191            $request{$key} = $value;
2192        }
2193    }
2194    LW2::http_fixup_request(\%request) unless ($flags->{'noclean'});
2195   
2196    # Run pre hooks
2197    (%$request, %$result) = run_hooks($mark, "prefetch", \%request, \%result);
2198   
2199    # Cache
2200    if (   defined $CACHE{$uri}
2201        && !defined $CLI{'nocache'}
2202        && !defined $flags->{'nocache'}
2203        && $CACHE{$uri}{method} eq $method
2204        && $CACHE{$uri}{mark}   eq $mark) {
2205
2206        # Get from cache
2207        nprint("- Retrieving $uri from cache.", "v");
2208        $result{'whisker'}->{'code'} = $CACHE{$uri}{'res'};
2209        $result{'whisker'}->{'data'} = $CACHE{$uri}{'content'};
2210    }
2211    else {
2212        LW2::http_do_request_timeout(\%request, \%result);
2213        $NIKTO{'totalrequests'}++;
2214        unless (defined $CLI{'nocache'}) {
2215            $CACHE{$uri}{'method'}  = $request{'whisker'}->{'method'};
2216            $CACHE{$uri}{'res'}     = $result{'whisker'}->{'code'};
2217            $CACHE{$uri}{'content'} = $result{'whisker'}->{'data'};
2218            $CACHE{$uri}{'mark'}    = $mark;
2219        }
2220        if ($OUTPUT{'debug'}) {
2221            dump_var("Request Hash", \%request);
2222            dump_var("Result Hash",  \%result);
2223        }
2224    }
2225
2226    if (   defined $CLI{'display'}
2227        && ($CLI{'display'} =~ /2/)
2228        && (defined($result{'whisker'}->{'cookies'}))) {
2229        foreach my $c (@{ $result{'whisker'}->{'cookies'} }) {
2230            nprint("+ $uri sent cookie: $c");
2231        }
2232    }
2233
2234    # If headers is defined, copy the whisker headers to the hash
2235    if (defined $headers) {
2236
2237        # First clear the hash
2238        foreach my $header (keys %$headers) {
2239            delete($headers->{$header});
2240        }
2241        while (my ($key, $value) = each(%result)) {
2242            if ($key ne "whisker" && $key ne "connection") {
2243                $headers->{$key} = $value;
2244            }
2245        }
2246    }
2247
2248    # Run post hooks
2249    (%$request, %$result) = run_hooks($mark, "postfetch", \%request, \%result);
2250
2251    return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'},
2252      $result{'whisker'}->{'error'};
2253}
2254
2255#######################################################################
2256sub set_scan_items {
2257    my ($mark) = @_;
2258
2259    # load the tests
2260    my $shname = $mark->{hostname} || $mark->{ip};
2261    %TESTS = ();
2262    my @SKIPLIST = ();
2263    if (defined $NIKTOCONFIG{SKIPIDS}) {
2264        @SKIPLIST = split(/ /, $NIKTOCONFIG{SKIPIDS});
2265    }
2266
2267    # now load checks
2268    foreach my $line (@DBFILE) {
2269        if ($line =~ /^\"/)    # check
2270        {
2271            chomp($line);
2272            my @item = parse_csv($line);
2273            my $add  = 1;
2274
2275            # check tuning options
2276            if ((defined $CLI{'tuning'}) && (defined $item[2])) {
2277
2278                # Work out the required tuning from the CLI string
2279                my $exclude = 0;
2280                foreach my $tune (split(//, $CLI{'tuning'})) {
2281                    if ($tune eq "x") {
2282                        $exclude = 1;
2283                        next;
2284                    }
2285                    if ($exclude == 0) {
2286                        if ($item[2] !~ /$tune/) { $add = 0; }
2287                        next;
2288                    }
2289                    if ($exclude == 1) {
2290                        if ($item[2] =~ /$tune/) { $add = 0; }
2291                    }
2292                }
2293            }
2294
2295            # Skip list
2296            foreach my $id (@SKIPLIST) {
2297                if ($id eq $item[0]) { $add = 0; }
2298            }
2299
2300            # RFI URL Defined?
2301            if (($item[2] =~ /c/) && ($VARIABLES{'@RFIURL'} eq '')) {
2302                $add = 0;
2303            }
2304
2305            if ($add) {
2306                my $ext = get_ext($item[3]);
2307                $db_extensions{$ext} = 1;
2308
2309# This escapes regex characters in the conditionals. This will have to change if regex is ever allowed in the db
2310                for (my $y = 5 ; $y <= 9 ; $y++) { $item[$y] =~ s/([^a-zA-Z0-9\s])/\\$1/g; }
2311
2312                $mark->{total_checks}++;
2313                $TESTS{ $item[0] }{uri}         = $item[3];
2314                $TESTS{ $item[0] }{osvdb}       = $item[1];
2315                $TESTS{ $item[0] }{method}      = $item[4];
2316                $TESTS{ $item[0] }{match_1}     = $item[5];
2317                $TESTS{ $item[0] }{match_1_or}  = $item[6];
2318                $TESTS{ $item[0] }{match_1_and} = $item[7];
2319                $TESTS{ $item[0] }{fail_1}      = $item[8];
2320                $TESTS{ $item[0] }{fail_2}      = $item[9];
2321                $TESTS{ $item[0] }{message}     = $item[10];
2322                $TESTS{ $item[0] }{data}        = $item[11];
2323                $TESTS{ $item[0] }{headers}     = $item[12];
2324            }
2325        }
2326    }
2327
2328    undef @DBFILE;    # this memory hog is no longer needed!
2329    nprint("- $mark->{total_checks} server checks loaded", "v");
2330    if ($mark->{total_checks} eq 0 && !defined $CLI{'tuning'}) {
2331        nprint("+ Unable to load valid checks!");
2332        exit;
2333    }
2334    return;
2335}
2336
2337#######################################################################
2338sub max_test_id {
2339    return (sort { $a <=> $b } keys %TESTS)[-1];
2340}
2341
2342#######################################################################
2343sub char_escape {
2344    $_[0] =~ s/([^a-zA-Z0-9 ])/\\$1/g;
2345    return $_[0];
2346}
2347
2348#######################################################################
2349sub parse_csv {
2350    my $text = $_[0] || return;
2351    my @new = ();
2352    push(@new, $+) while $text =~ m{
2353      "([^\"\\]*(?:\\.[^\"\\]*)*)",?
2354       |  ([^,]+),?
2355       | ,
2356   }gx;
2357    push(@new, undef) if substr($text, -1, 1) eq ',';
2358    return @new;
2359}
2360
2361#######################################################################
2362sub version {
2363    my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, "(^nikto|^db_)");
2364    nprint "$NIKTO{'DIV'}\n";
2365    nprint "$NIKTO{'name'} Versions\n";
2366    nprint "$NIKTO{'DIV'}\n";
2367    nprint "File                               Version      Last Mod\n";
2368    nprint "-----------------------------      --------     ----------\n";
2369    nprint "Nikto main                         $NIKTO{'version'}\n";
2370    nprint "LibWhisker                         $LW2::VERSION\n";
2371
2372    foreach my $FILE (sort @NIKTOFILES) {
2373        open(FI, "<$NIKTOCONFIG{PLUGINDIR}/$FILE")
2374          || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$FILE': $!\n";
2375        my @F = <FI>;
2376        close(FI);
2377        my @VERS = grep(/^#VERSION/, @F);
2378        my @MODS = grep(/^# \$Id:/,  @F);
2379        chomp($VERS[0]);
2380        chomp($MODS[0]);
2381        my @modification = split(/ /, $MODS[0]);
2382        $VERS[0] =~ s/^#VERSION,//;
2383        my $ws1 = (35 - length($FILE));
2384        my $ws2 = (13 - length($VERS[0]));
2385        nprint "$FILE" . " " x $ws1 . "$VERS[0]" . " " x $ws2 . "$modification[4]\n";
2386    }
2387    nprint "$NIKTO{'DIV'}\n";
2388
2389    exit;
2390}
2391
2392#######################################################################
2393sub send_updates {
2394    return if ($NIKTOCONFIG{UPDATES} !~ /yes|auto/i);
2395
2396    my $have_updates = 0;
2397    my ($updated_version, $answer, $RES);
2398    foreach my $ver (keys %UPDATES) {
2399
2400        # ignore useless ones...
2401        if ($ver !~ /[0-9]/) { next; }
2402        elsif ($ver eq "Win32")          { next; }
2403        elsif ($ver eq "(Win32)")        { next; }
2404        elsif ($ver eq "Linux-Mandrake") { next; }
2405        $have_updates = 1;
2406        $updated_version .= "$ver ";
2407    }
2408
2409    if ((!$have_updates) || ($updated_version eq "")) { return; }
2410
2411    # make sure the db_outdatedb isn't *too* old
2412    open(OD, "<$NIKTOCONFIG{PLUGINDIR}/db_outdated")
2413      || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/db_outdated': $!\n";
2414    @F = <OD>;
2415    close(OD);
2416
2417    my @LASTUPDATED = grep(/^\# \$Id: db_outdated/, @F);
2418    $LASTUPDATED[0] =~ /([0-9]{4}\-[0-9]{2})/;
2419    $lm = $1;
2420    $lm =~ s/\-//g;
2421
2422    my @NOW = localtime(time);
2423    $NOW[5] += 1900;
2424    $NOW[4]++;
2425    if ($NOW[4] < 10) { $NOW[4] = "0$NOW[4]"; }
2426    my $now = "$NOW[5]$NOW[4]";
2427    if (($now - $lm) > 120) { return; }    # DB is 4 months old... ignore the updates!
2428
2429    $updated_version =~ s/\s+$//;
2430    $updated_version =~ s/^\s+//;
2431
2432    if ($NIKTOCONFIG{UPDATES} eq "auto") {
2433        $answer = "y";
2434    }
2435    else {
2436        $answer = read_data(
2437            "\n
2438      *********************************************************************
2439      Portions of the server's ident string ($updated_version) are not in
2440      the Nikto database or is newer than the known string. Would you like
2441      to submit this information (*no server specific data*) to CIRT.net
2442      for a Nikto update (or you may email to sullo\@cirt.net) (y/n)? ", ""
2443            );
2444    }
2445
2446    if ($answer !~ /y/i) { return; }
2447
2448    # set up our mark
2449    my %mark = ('ident' => 'www.cirt.net',
2450                'ssl'   => 0,
2451                'port'  => 80
2452                );
2453    ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('www.cirt.net');
2454
2455    for (my $i = 0 ; $i <= $#ARGV ; $i++) {
2456        if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) {
2457            $CLI{'useproxy'} = 1;
2458            last;
2459        }
2460    }
2461
2462    ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2463
2464    if ($RES eq 407) {
2465        if ($NIKTOCONFIG{PROXYUSER} eq "") {
2466            $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
2467            $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
2468        }
2469        ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2470    }
2471
2472    if ($RES eq "") {
2473        LW2::http_close(\%request);    # force-close any old connections
2474        $mark{'ip'} = $NIKTOCONFIG{CIRT};
2475        ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2476    }
2477
2478    if ($CONTENT !~ /SUCCESS/) {
2479        print STDERR "+ ERROR: ($RES, $CONTENT): Unable to send update info to CIRT.net\n";
2480    }
2481    else {
2482        nprint "- Sent updated info to CIRT.net -- Thank you!\n";
2483    }
2484
2485    return;
2486}
2487
2488#######################################################################
2489sub usage {
2490    if ($_[0] eq 2) {
2491        nprint($NIKTO{'options'});
2492    }
2493    else {
2494        nprint($NIKTO{'options_short'});
2495    }
2496    exit;
2497}
2498
2499#######################################################################
2500sub init_db {
2501    my $dbname   = $_[0];
2502    my $filename = "$NIKTOCONFIG{PLUGINDIR}/" . $dbname;
2503    my (@dbarray, @headers);
2504    my $hashref = {};
2505
2506    # Check that the database exists
2507    unless (open(IN, "<$filename")) {
2508        nprint("+ ERROR: Unable to open database file $dbname: $!.");
2509        return $dbarray;
2510    }
2511
2512    # Now read the header values
2513    while (<IN>) {
2514        chomp;
2515        s/\#.*$//;
2516        if ($_ eq "") { next }
2517        unless (@headers) {
2518            @headers = parse_csv($_);
2519        }
2520        else {
2521
2522            # contents; so split them up and apply to hash
2523            my @contents = parse_csv($_);
2524            my $hashref  = {};
2525            for (my $i = 0 ; $i <= $#contents ; $i++) {
2526                $hashref->{ $headers[$i] } = $contents[$i];
2527            }
2528            push(@dbarray, $hashref);
2529        }
2530    }
2531    return \@dbarray;
2532}
2533
2534#######################################################################
2535sub add_vulnerability {
2536    my ($mark, $message, $nikto_id, $osvdb, $method, $uri) = @_;
2537    $uri    = "/"   unless (defined $uri);
2538    $method = "GET" unless (defined $method);
2539    $osvdb  = "0"   unless (defined $osvdb);
2540
2541    my $result = "";
2542    if (defined $_[7]) {
2543        $result = $_[7]->{'whisker'}->{'data'};
2544    }
2545    my $outmessage = $message;
2546    my $resulthash = {};
2547
2548    unless ($osvdb eq "0") {
2549        $outmessage = "OSVDB-$osvdb: $message";
2550    }
2551    nprint("+ $outmessage");
2552    %$resulthash = (mark     => $mark,
2553                    message  => $message,
2554                    nikto_id => $nikto_id,
2555                    osvdb    => $osvdb,
2556                    method   => $method,
2557                    uri      => $uri,
2558                    result   => $result,
2559                    );
2560    $mark->{total_vulns}++;
2561    push(@RESULTS, $resulthash);
2562
2563    # Now report it
2564    report_item($mark, $resulthash);
2565}
2566
2567###############################################################################
2568sub list_plugins {
2569
2570    # Just do a load_plugins, then loop through the array and print out name,
2571    # description and copyright
2572
2573    load_plugins();
2574
2575    foreach my $plugin (@PLUGINS) {
2576        nprint("Plugin: $plugin->{'name'}");
2577        push(@all_names, $plugin->{'name'});
2578        nprint(" $plugin->{'full_name'} - $plugin->{'description'}");
2579        nprint(" Written by $plugin->{'author'}, Copyright (C) $plugin->{'copyright'}");
2580        nprint("\n");
2581    }
2582
2583    # Plugin macros
2584    nprint("Defined plugin macros:");
2585
2586    foreach my $macro (keys %NIKTOCONFIG) {
2587        if ($macro =~ /^@@/) {
2588            nprint(" $macro = \"" . $NIKTOCONFIG{$macro} . "\"");
2589            if ($NIKTOCONFIG{$macro} =~ /@@/) {
2590                nprint("  (expanded) = \"" . expand_pluginlist($NIKTOCONFIG{$macro}, 0) . "\"");
2591            }
2592        }
2593    }
2594
2595    exit(0);
2596}
2597
2598###############################################################################
2599# This is overly complicated and jumps a lot between scalars and arrays. The REs are
2600# probably dodgy, but it works! W00!
2601sub expand_pluginlist {
2602    my ($pluginlist, $parent) = @_;
2603
2604    my @macros;
2605    foreach my $config (keys %NIKTOCONFIG) {
2606        if ($config =~ /^@@/) {
2607            push(@macros, $config);
2608        }
2609    }
2610
2611    # Now loop through each member of the list and expand it
2612    my $count       = 0;
2613    my $npluginlist = $pluginlist;
2614    do {
2615        $count++;
2616        my @raw = split(/;/, $npluginlist);
2617
2618        # cooked contains the processed list
2619        my @cooked;
2620        foreach my $entry (@raw) {
2621
2622            # Is it +; if so remap to @@DEFAULT
2623            if ($entry eq "+") {
2624                $entry = '@@DEFAULT';
2625            }
2626
2627            # result contains the processed entry
2628            my $result = $original = $entry;
2629
2630            # Is it a macro
2631            if ($entry =~ /^-?@@/) {
2632
2633                # break up into components
2634                $prefix = ($entry =~ /^-/) ? "-" : "";
2635                $name = $suffix = $entry;
2636                $name   =~ s/(^-?)(@@[[:alpha:]]+)(\(?.*\)?$)/$2/;
2637                $suffix =~ s/(.*)(\(.*\))/$2/;
2638                if ($suffix eq $entry) {
2639                    $suffix = "";
2640                }
2641                foreach my $macro (@macros) {
2642                    if ($entry =~ /-?$macro/) {
2643
2644                        # It's a macro, so replace the contents with the macro
2645                        # Add prefix and suffix to each member of the macro
2646                        my @temp;
2647                        foreach my $child (split(/;/, $NIKTOCONFIG{$macro})) {
2648                            push(@temp, "$prefix$child$suffix");
2649                        }
2650                        $result = join(';', @temp);
2651
2652                        # stop an infinite loop
2653                        last;
2654                    }
2655                }
2656            }
2657            if ($result =~ /^-?@@/ && $result eq $original) {
2658
2659                # macro not found or is itself - ignore
2660                $result = "";
2661            }
2662            if ($count > 100) {
2663
2664                # check for recurstion
2665                nprint("ERROR: Recursion found whilst expanding macros");
2666                $result = "";
2667                last;
2668            }
2669            push(@cooked, $result);
2670        }
2671        $npluginlist = join(';', @cooked);
2672    } while ($npluginlist =~ /@@/ && $count <= 100);
2673
2674    #use re 'debug';
2675    # Now we've expanded out macros, deal with duplicates and -
2676    my @raw = split(/;/, $npluginlist);
2677
2678    # hash so we don't have to mess with duplicates
2679    my %cooked;
2680    foreach my $plugin (@raw) {
2681
2682        # break out components
2683        my $minus;
2684        my $name = my $suffix = $plugin;
2685        $minus = (substr($plugin, 0, 1) eq '-');
2686        $name   =~ s/(^-?)([^\(]+)(\(?.*\)?$)/$2/;
2687        $suffix =~ s/(.*)(\(.*\))/$2/;
2688        if ($suffix eq $plugin) {
2689            $suffix = "";
2690        }
2691
2692        #nprint("P:$plugin M:$minus N:$name S:$suffix");
2693        if ($minus) {
2694
2695            # it's a minus - remove any previous entry
2696            if (exists $cooked{$name}) {
2697                delete $cooked{$name};
2698            }
2699        }
2700        else {
2701
2702            # else add it with the parameters as the value of the hash
2703            $cooked{$name} = $suffix;
2704        }
2705    }
2706
2707    # Now rejoin into one happy whole
2708    my $output;
2709    foreach my $plugin (keys %cooked) {
2710        $output .= "$plugin" . $cooked{$plugin} . ";";
2711    }
2712
2713    # remove the last ;
2714    $output =~ s/;$//g;
2715
2716    return $output;
2717}
2718#######################################################################
2719sub nikto_core { return; }    # trap for this plugin being called to run. lame.
2720#######################################################################
2721
27221;
Note: See TracBrowser for help on using the repository browser.