source: trunk/plugins/nikto_core.plugin @ 351

Revision 351, 93.2 KB checked in by deity, 3 years ago (diff)

Loads of changes, including a fix for 126 and more hooks for plugins

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