source: trunk/plugins/nikto_core.plugin @ 447

Revision 447, 87.2 KB checked in by sullo, 3 years ago (diff)

Some regex and get_ext optimizations

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