source: trunk/plugins/nikto_core.plugin @ 384

Revision 384, 93.7 KB checked in by sullo, 3 years ago (diff)

minor cleanup

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