source: trunk/plugins/nikto_core.plugin @ 399

Revision 399, 88.8 KB checked in by sullo, 3 years ago (diff)

Don't add content-type and content-length headers if there is no data, and avoid temp variable usage.

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