source: trunk/plugins/nikto_core.plugin @ 413

Revision 413, 90.3 KB checked in by sullo, 3 years ago (diff)

Check for disabled cache on add to cache as well as fetch, simplify logic a little

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