source: trunk/plugins/nikto_core.plugin @ 623

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