source: trunk/plugins/nikto_core.plugin @ 356

Revision 356, 93.5 KB checked in by deity, 3 years ago (diff)

Changes to move auth to a postfetch plugin

  • 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 ($run) = 1;
1466                 
1467                # first check for conditionals
1468        my $condition = $plugin->{$type . '_cond'};
1469                if (defined $plugin->{$type . '_cond'}) { 
1470                    # Evaluate condition
1471                    $run = eval($condition);
1472                }
1473       
1474        next if ($run != 1) ;
1475       
1476        my $oldverbose = $OUTPUT{'verbose'};
1477        my $olddebug   = $OUTPUT{'debug'};
1478        nprint("- Running $type for \"$plugin->{full_name}\" plugin", "v");
1479        if (defined $plugin->{'parameters'}->{'verbose'}
1480            && $plugin->{'parameters'}->{'verbose'} == 1) {
1481            $OUTPUT{'verbose'} = 1;
1482        }
1483        if (defined $plugin->{'parameters'}->{'debug'}
1484            && $plugin->{'parameters'}->{'debug'} == 1) {
1485            $OUTPUT{'debug'} = 1;
1486        }
1487        &{ $plugin->{$type . '_method'} }($mark, $request, $result, $plugin->{'parameters'});
1488        $OUTPUT{'verbose'} = $oldverbose;
1489        $OUTPUT{'debug'}   = $olddebug;
1490    }
1491   
1492    return $request, $result;
1493}
1494
1495###############################################################################
1496sub report_head {
1497    my ($format, $file) = @_;
1498    nprint("- Opening reports", "v");
1499
1500    # For tuning set up a list of report methods, formats and handles
1501
1502    # This is a frig until I can think of a better way of achieving it
1503    foreach my $i (1 .. 100) {
1504        foreach my $plugin (@PLUGINS) {
1505            if ($plugin->{run} && defined $plugin->{report_item} && $plugin->{report_weight} == $i)
1506            {
1507                my $run = 1;
1508
1509                # first check for conditionals
1510                if (defined $plugin->{report_format}) {
1511
1512                    # Evaluate condition
1513                    $run = ($format eq $plugin->{report_format});
1514                }
1515                if ($run) {
1516                    nprint("- Opening report for \"$plugin->{full_name}\" plugin", "v");
1517                    my $handle;
1518                    if (defined $plugin->{report_head}) {
1519                        $handle = &{ $plugin->{report_head} }($file);
1520                    }
1521
1522                    # Now store this
1523                    my $report_entry = { host_start => $plugin->{report_host_start},
1524                                         host_end   => $plugin->{report_host_end},
1525                                         item       => $plugin->{report_item},
1526                                         close      => $plugin->{report_close},
1527                                         handle     => $handle,
1528                                         };
1529                    push(@REPORTS, $report_entry);
1530                }
1531            }
1532        }
1533    }
1534    return;
1535}
1536
1537###############################################################################
1538sub report_host_start {
1539    my ($mark) = @_;
1540
1541    # Go through all reporting modules
1542    foreach my $reporter (@REPORTS) {
1543        if (defined $reporter->{host_start}) {
1544            &{ $reporter->{host_start} }($reporter->{handle}, $mark);
1545        }
1546    }
1547}
1548
1549###############################################################################
1550sub report_host_end {
1551    my ($mark) = @_;
1552
1553    # Go through all reporting modules
1554    foreach my $reporter (@REPORTS) {
1555        if (defined $reporter->{host_end}) {
1556            &{ $reporter->{host_end} }($reporter->{handle}, $mark);
1557        }
1558    }
1559}
1560
1561###############################################################################
1562sub report_item {
1563    my ($mark, $item) = @_;
1564
1565    # Go through all reporting modules
1566    foreach my $reporter (@REPORTS) {
1567        if (defined $reporter->{item}) {
1568            &{ $reporter->{item} }($reporter->{handle}, $mark, $item);
1569        }
1570    }
1571}
1572
1573###############################################################################
1574sub report_close {
1575
1576    # Go through all reporting modules
1577    foreach my $reporter (@REPORTS) {
1578        if (defined $reporter->{close}) {
1579            &{ $reporter->{close} }($reporter->{handle});
1580        }
1581    }
1582}
1583
1584###############################################################################
1585sub check_updates {
1586    LW2::http_init_request(\%request);
1587    my (%REMOTE, %LOCAL, @DBTOGET) = ();
1588    my ($pluginmsg, $remotemsg) = "";
1589    my $code_updates = 0;
1590    my $serverdir    = "/nikto/UPDATES/$NIKTO{'version'}";
1591
1592    # set up our mark
1593    my %mark = ('ident' => 'www.cirt.net',
1594                'ssl'   => 0,
1595                'port'  => 80
1596                );
1597    ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('www.cirt.net');
1598
1599    for (my $i = 0 ; $i <= $#ARGV ; $i++) {
1600        if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) {
1601            $CLI{'useproxy'} = 1;
1602            last;
1603        }
1604    }
1605
1606    # retrieve versions file
1607    (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1608    if ($RES eq 407) {
1609        if ($NIKTOCONFIG{PROXYUSER} eq "") {
1610            $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
1611            $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
1612        }
1613
1614        # and try again
1615        ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1616    }
1617
1618    if ($RES eq "") {
1619        ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET");
1620    }
1621
1622    if ($RES ne 200) {
1623        print STDERR
1624          "+ ERROR ($RES): Unable to get $request{'whisker'}->{'host'}$serverdir/versions.txt\n";
1625        exit;
1626    }
1627
1628    # make hash
1629    for (split(/\n/, $CONTENT)) {
1630        my @l = parse_csv($_);
1631        if ($_ =~ /^msg/) {
1632            $remotemsg = "$l[1]";
1633            next;
1634        }
1635        $REMOTE{ $l[0] } = $l[1];
1636    }
1637
1638    # get local versions of plugins/dbs
1639    my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, "");
1640
1641    foreach my $file (@NIKTOFILES) {
1642        my $v = "";
1643        open(LOCAL, "<$NIKTOCONFIG{PLUGINDIR}/$file")
1644          || print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$file' for read: $@\n";
1645        my @l = <LOCAL>;
1646        close(LOCAL);
1647
1648        my @VERS = grep(/^#VERSION/, @l);
1649        chomp($VERS[0]);
1650        $LOCAL{$file} = (parse_csv($VERS[0]))[1];
1651    }
1652
1653    # check main nikto versions
1654    foreach my $remotefile (keys %REMOTE) {
1655        if ($remotefile eq "nikto") {
1656            if ($REMOTE{$remotefile} > $NIKTO{'version'}) {
1657                nprint
1658                  "+ Nikto has been updated to $REMOTE{$remotefile}, local copy is $NIKTO{'version'}\n";
1659                nprint
1660                  "+ No update has taken place. Please upgrade Nikto by visiting http://$server/\n";
1661                if ($remotemsg ne "") {
1662                    nprint "+ $server message: $remotemsg\n";
1663                }
1664                exit;
1665            }
1666            next;
1667        }
1668
1669        if (   ($LOCAL{$remotefile} eq "")
1670            || ($REMOTE{$remotefile} > $LOCAL{$remotefile})) {
1671            push(@DBTOGET, $remotefile);
1672            if ($remotefile !~ /^db_/) { $code_updates = 1; }
1673        }
1674        elsif ($REMOTE{$remotefile} < $LOCAL{$remotefile}) {
1675            print STDERR
1676              "+ ERROR: Local '$remotefile' (ver $LOCAL{$remotefile}) is NEWER than remote (ver $REMOTE{$remotefile}).\n";
1677        }
1678    }
1679
1680    # replace local files if updated
1681    foreach my $toget (@DBTOGET) {
1682        nprint "+ Retrieving '$toget'\n";
1683        (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/$toget", "GET");
1684        if ($RES ne 200) {
1685            print STDERR "+ ERROR: Unable to get $server$serverdir/$toget\n";
1686            exit;
1687        }
1688        if ($CONTENT ne "") {
1689            open(OUT, ">$NIKTOCONFIG{PLUGINDIR}/$toget")
1690              || die print STDERR
1691              "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$toget' for write: $@\n";
1692            print OUT $CONTENT;
1693            close(OUT);
1694        }
1695    }
1696
1697    # CHANGES file
1698    if ($code_updates) {
1699        nprint "+ Retrieving 'CHANGES.txt'\n";
1700        (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/CHANGES.txt", "GET");
1701        if (($CONTENT ne "") && ($RES eq 200)) {
1702            open(OUT, ">$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt")
1703              || die print STDERR
1704              "+ ERROR: Unable to open '$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt' for write: $@\n";
1705            print OUT $CONTENT;
1706            close(OUT);
1707        }
1708    }
1709
1710    if ($#DBTOGET < 0) { nprint "+ No updates required.\n"; }
1711    if ($remotemsg ne "") { nprint "+ $server message: $remotemsg\n"; }
1712    exit;
1713}
1714
1715###############################################################################
1716# do_auth
1717# Inputs: $request, $result
1718# Returns: $request
1719sub do_auth {
1720    my $request  = $_[0];
1721    my $result   = $_[1];
1722    my $mark     = $_[2];
1723    my $authtype = "basic";
1724    my $realm    = "";
1725
1726    unless (defined $result{'www-authenticate'}) {
1727        nprint("+ ERROR: No authentication header defined");
1728    }
1729
1730    # Split up www-authenticate to realm and method
1731    my @authenticate = split(/ /, $result{'www-authenticate'});
1732    if ($#authenticate == 0) {    # Only one parameter: realm
1733        $realm = $authenticate[0];
1734        if ($realm =~ /^ntlm/i) {
1735            $realm    = "";
1736            $authtype = $authenticate[0];
1737        }
1738    }
1739    else {
1740        $authtype = $authenticate[0];
1741        $realm    = $authenticate[1];
1742        $realm =~ s/^realm=//;
1743    }
1744
1745    # Now we have this we can try guessing the password
1746    foreach my $REALM (keys %REALMS) {
1747        next unless ($realm =~ /$REALMS{$REALM}{realm}/i || $REALMS{$REALM}{realm} eq '@ANY');
1748        if (($REALMS{$REALM}{id} eq "") && ($REALMS{$REALM}{pw} eq "")) {
1749            my $uridir = $request{'whisker'}->{'uri'};
1750            $uridir =~ s#/[^/]*$#/#g;
1751            add_vulnerability(
1752                $mark,
1753                "Blank credentials found at $uridir ($request{whisker}->{uri}), $REALMS{$REALM}{realm}: $REALMS{$REALM}{msg}",
1754                $REALMS{$REALM}{tid},
1755                0,
1756                "GET",
1757                $uridir,
1758                \%result
1759                );
1760        }
1761        else {
1762            my $save_auth = $result{'www-authenticate'};
1763            LW2::http_close(\%request);    # force-close any old connections
1764            LW2::auth_set($authtype, \%request, $REALMS{$REALM}{id}, $REALMS{$REALM}{pw});
1765
1766            # Path to fix short reads
1767            $request{'whisker'}->{allow_short_reads} = 1;
1768            LW2::http_fixup_request(\%request);
1769            if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1770            LW2::http_do_request_timeout(\%request, \%result);    # test auth
1771            $NIKTO{'totalrequests'}++;
1772            dump_var("Auth Request",  \%request);
1773            dump_var("Auth Response", \%result);
1774            if ($result{'www-authenticate'} =~ /^ntlm/i) {
1775
1776                # Deal with ntlm
1777                my @ntlm_x = split(/ /, $result{'www-authenticate'});
1778                if ($#ntlm_x == 1) {
1779                    LW2::http_do_request_timeout(\%request, \%result);
1780                    $NIKTO{'totalrequests'}++;
1781                }
1782            }
1783            if ($result{'www-authenticate'} eq ""
1784                && !defined $result{'whisker'}->{'error'}) {
1785                unless (   $REALMS{$REALM}{tid} == 700500
1786                        || $REALMS{$REALM}{checked} == 1) {
1787                    my $uridir = $request{'whisker'}->{'uri'};
1788                    $uridir =~ s#/[^/]*$#/#g;
1789                    add_vulnerability(
1790                        $mark,
1791                        "Default account found for '$realm' at $uridir ($request{whisker}->{uri}) (ID '$REALMS{$REALM}{id}', PW '$REALMS{$REALM}{pw}'). $REALMS{$REALM}{msg}",
1792                        $REALMS{$REALM}{tid},
1793                        0,
1794                        "GET",
1795                        $uridir,
1796                        \%result
1797                        );
1798                    $REALMS{$REALM}{checked} = 1;
1799                }
1800
1801                # Finally repeat the check
1802                LW2::http_do_request_timeout(\%request, \%result);    # test auth
1803                $NIKTO{'totalrequests'}++;
1804
1805                # and leave
1806                last;
1807            }
1808            else { $result->{'www-authenticate'} = $save_auth; }
1809        }
1810    }
1811    LW2::auth_unset(\%request);
1812    return $result;
1813}
1814
1815###############################################################################
1816# read_data ( prompt, mode )
1817# read STDIN data from the user
1818# portions of this (POSIX code) were taken from the
1819# Term::ReadPassword module by Tom Phoenix <rootbeer@redcat.com> (many thanks).
1820# it has been modified to not require Term::ReadLine, but still requires
1821# POSIX::Termios of it's a POSIX machine
1822###############################################################################
1823sub read_data {
1824    if ($NIKTOCONFIG{PROMPTS} =~ /no/i) { return; }
1825    my ($prompt, $mode, $POSIX) = @_;
1826    my $input = "";
1827
1828    if   ($^O =~ /Win32/) { $POSIX = 0; }
1829    else                  { $POSIX = 1; }
1830
1831    my %SPECIAL = ("\x03" => 'INT',    # Control-C, Interrupt
1832                   "\x08" => 'DEL',    # Backspace
1833                   "\x7f" => 'DEL',    # Delete
1834                   "\x0d" => 'ENT',    # CR, Enter
1835                   "\x0a" => 'ENT',    # LF, Enter
1836                   );
1837
1838    # if we're on a non-POSIX machine we can't not-echo the
1839    # characters, so just use getc to avoid the dependency on
1840    # POSIX::Termios. We would be best to get rid of this
1841    # entirely and use another way...
1842
1843    if ($POSIX) {
1844        local (*TTY, *TTYOUT);
1845        open TTY,    "<&STDIN"   or return;
1846        open TTYOUT, ">>&STDOUT" or return;
1847
1848        # Don't buffer it!
1849        select((select(TTYOUT), $| = 1)[0]);
1850        print TTYOUT $prompt;
1851
1852        # Remember where everything was
1853        my $fd_tty = fileno(TTY);
1854        my $term   = POSIX::Termios->new();
1855        $term->getattr($fd_tty);
1856        my $original_flags = $term->getlflag();
1857
1858        if ($mode eq "noecho") {
1859            my $new_flags = $original_flags & ~(ISIG | ECHO | ICANON);
1860            $term->setlflag($new_flags);
1861        }
1862        $term->setattr($fd_tty, TCSAFLUSH);
1863      KEYSTROKE:
1864        while (1) {
1865            my $new_keys = '';
1866            my $count = sysread(TTY, $new_keys, 99);
1867            if ($count) {
1868                for my $new_key (split //, $new_keys) {
1869                    if (my $meaning = $SPECIAL{$new_key}) {
1870                        if    ($meaning eq 'ENT') { last KEYSTROKE; }
1871                        elsif ($meaning eq 'DEL') { chop $input; }
1872                        elsif ($meaning eq 'INT') { last KEYSTROKE; }
1873                        else                      { $input .= $new_key; }
1874                    }
1875                    else { $input .= $new_key; }
1876                }
1877            }
1878            else { last KEYSTROKE; }
1879        }
1880
1881        # Done with waiting for input. Let's not leave the cursor sitting
1882        # there, after the prompt.
1883        print TTY "\n";
1884        nprint "\n";
1885
1886        # Let's put everything back where we found it.
1887        $term->setlflag($original_flags);
1888        $term->setattr($fd_tty, TCSAFLUSH);
1889        close(TTY);
1890        close(TTYOUT);
1891    }
1892    else    # non-POSIX
1893    {
1894        print $prompt;
1895        $input = <STDIN>;
1896        chomp($input);
1897    }
1898
1899    return $input;
1900}
1901
1902###############################################################################
1903sub proxy_setup {
1904    if (!$CLI{'useproxy'}) { return; }
1905
1906    # HTTP proxy
1907    $request{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{PROXYHOST};
1908    $request{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{PROXYPORT};
1909
1910    return;
1911}
1912
1913###############################################################################
1914sub proxy_check {
1915    my ($mark) = @_;
1916
1917    if (defined $request{'whisker'}->{'proxy_host'} && $CLI{'useproxy'})    # proxy is set up
1918    {
1919        LW2::http_close(\%request);    # force-close any old connections
1920        setup_hash(\%request, $mark, "Proxy Check");
1921        $request{'whisker'}->{'method'} = "GET";
1922        $request{'whisker'}->{'uri'}    = "/";
1923
1924        LW2::http_fixup_request(\%request);
1925
1926        if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1927        LW2::http_do_request_timeout(\%request, \%result);
1928        $NIKTO{'totalrequests'}++;
1929        dump_var("Request Hash", \%request);
1930        dump_var("Result Hash",  \%result);
1931
1932        # First check that we can connect to the proxy
1933        if (exists $result{'whisker'}{'error'}) {
1934            if ($result{'whisker'}{'error'} =~ /Transport endpoint is not connected/) {
1935                nprint("+ ERROR: Could not connect to the defined proxy $NIKTOCONFIG{PROXYHOST}");
1936                exit 1;
1937            }
1938            nprint("+ ERROR: Proxy error: $result{'whisker'}{'error'}", "v");
1939        }
1940        if ($result{'whisker'}{'code'} eq "407")    # proxy requires auth
1941        {
1942
1943            # have id/pw?
1944            if ($NIKTOCONFIG{PROXYUSER} eq "") {
1945                $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
1946                $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
1947            }
1948            if ($result{'proxy-authenticate'} !~ /Basic/i) {
1949                my @x = split(/ /, $result{'proxy-authenticate'});
1950                nprint(
1951                    "+ Proxy server uses '$x[0]' rather than 'Basic' authentication. $NIKTO{'name'} $NIKTO{'version'} can't do that."
1952                    );
1953                exit;
1954            }
1955
1956            # test it...
1957            LW2::http_close(\%request);    # force-close any old connections
1958            LW2::auth_set("proxy-basic", \%request, $NIKTOCONFIG{PROXYUSER},
1959                          $NIKTOCONFIG{PROXYPASS});    # set auth
1960            LW2::http_fixup_request(\%request);
1961            if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
1962            LW2::http_do_request_timeout(\%request, \%result);
1963            $NIKTO{'totalrequests'}++;
1964            dump_var("Request Hash", \%request);
1965            dump_var("Result Hash",  \%result);
1966
1967            if ($result{'proxy-authenticate'} ne "") {
1968                my @pauthinfo  = split(/ /, $result{'proxy-authenticate'});
1969                my @pauthinfo2 = split(/=/, $result{'proxy-authenticate'});
1970                $pauthinfo2[1] =~ s/^\"//;
1971                $pauthinfo2[1] =~ s/\"$//;
1972                nprint(
1973                    "+ Proxy requires authentication for '$pauthinfo[0]' realm '$pauthinfo2[1]', unable to authenticate."
1974                    );
1975                exit;
1976            }
1977            else { nprint("- Successfully authenticated to proxy.", "v"); }
1978        }
1979    }
1980
1981    return;
1982}
1983
1984###############################################################################
1985sub dirlist {
1986    my $DIR     = $_[0] || return;
1987    my $PATTERN = $_[1] || "";
1988    my @FILES_TMP = ();
1989
1990    opendir(DIRECTORY, $DIR) || die print STDERR "+ ERROR: Can't open directory '$DIR': $@";
1991    foreach my $file (readdir(DIRECTORY)) {
1992        if ($file =~ /^\./) { next; }    # skip hidden files, '.' and '..'
1993        if ($PATTERN ne "") {
1994            if ($file =~ /$PATTERN/) { push(@FILES_TMP, $file); }
1995        }
1996        else { push(@FILES_TMP, $file); }
1997    }
1998    closedir(DIRECTORY);
1999
2000    return @FILES_TMP;
2001}
2002
2003#######################################################################
2004sub dump_var {
2005    return if !$OUTPUT{debug};
2006    my $msg     = $_[0];
2007    my %hash_in = %{ $_[1] };
2008
2009    my $display = LW2::dump('', \%hash_in);
2010    $display =~ s/^\$/'$msg'/;
2011    if ($OUTPUT{'scrub'}) {
2012        $display =~ s/'host' => '.*',/'host' => 'example.com',/g;
2013        $display =~ s/'Host' => '.*'/'host' => 'example.com'/g;
2014    }
2015    nprint($display, "d");
2016    return;
2017}
2018
2019######################################################################
2020sub content_present {
2021    my $result = FALSE;
2022    my $res    = $_[0];
2023
2024    # perform an extra check just in case the web server lies about finds
2025    # basically assume that the value for a non-extension is the true
2026    # code for "File not Found".
2027    if ($res ne $FoF{NONE}{response}) {
2028        foreach $found (split(' ', $VARIABLES{"\@HTTPFOUND"})) {
2029            if ($res eq $found) {
2030                $result = TRUE;
2031            }
2032        }
2033    }
2034
2035    return $result;
2036}
2037
2038#######################################################################
2039sub fetch {
2040    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
2041    LW2::http_close(\%request);    # force-close any old connections
2042    $request{'whisker'}->{'uri'} = $_[0];
2043    if (defined $CLI{'root'}) {
2044        $request{'whisker'}->{'uri'} = $CLI{'root'} . $_[0];    # prepend -root option value
2045    }
2046
2047    # Set testid in UA
2048    my $temp_ua = $request{'User-Agent'};
2049    my $testid  = $_[3];
2050    $request{'User-Agent'} =~ s/\@TESTID/$testid/g;
2051
2052    $request{'whisker'}->{'method'} = $_[1];
2053    delete $request{'whisker'}->{'data'};
2054    delete $request{'Content-Encoding'};
2055    delete $request{'Content-Length'};
2056    my $header_hash = $_[3];
2057
2058    if (defined $_[2]) {
2059        my $r = $_[2];
2060        $r =~ s/\\\"/\"/g;
2061        $request{'whisker'}->{'data'} = $r;
2062    }
2063
2064    # check for extra HTTP headers
2065    if (defined $_[3]) {
2066
2067        # loop through the hash ref passed and add each header to request
2068        while (my ($key, $value) = each(%$header_hash)) {
2069            $request{$key} = $value;
2070        }
2071    }
2072    unless ($_[4]) { LW2::http_fixup_request(\%request); }
2073
2074    # Cache
2075    if (   defined($CACHE{ $request{'whisker'}->{'uri'} })
2076        && !defined($CLI{'nocache'})
2077        && ($CACHE{ $request{'whisker'}->{'uri'} }{'method'} eq $request{'whisker'}->{'method'})) {
2078
2079        # Get from cache
2080        nprint("- Retrieving $request{'whisker'}->{'uri'} from cache.", "v");
2081        $result{'whisker'}->{'code'} = $CACHE{ $request{'whisker'}->{'uri'} }{'res'};
2082        $result{'whisker'}->{'data'} = $CACHE{ $request{'whisker'}->{'uri'} }{'content'};
2083    }
2084    else {
2085        LW2::http_do_request_timeout(\%request, \%result);
2086        $NIKTO{'totalrequests'}++;
2087        unless (defined $CLI{'nocache'}) {
2088            $CACHE{ $request{'whisker'}->{'uri'} }{'method'}  = $request{'whisker'}->{'method'};
2089            $CACHE{ $request{'whisker'}->{'uri'} }{'res'}     = $result{'whisker'}->{'code'};
2090            $CACHE{ $request{'whisker'}->{'uri'} }{'content'} = $result{'whisker'}->{'data'};
2091        }
2092        if ($OUTPUT{debug}) {
2093            dump_var("Request Hash", \%request);
2094            dump_var("Result Hash",  \%result);
2095        }
2096    }
2097
2098    if (   defined $CLI{'display'}
2099        && ($CLI{'display'} =~ /2/)
2100        && (defined($result{'whisker'}->{'cookies'}))) {
2101        foreach my $c (@{ $result{'whisker'}->{'cookies'} }) {
2102            nprint("+ $request{'whisker'}->{'uri'} sent cookie: $c");
2103        }
2104    }
2105
2106    # Clean up extra headers
2107    if (defined $_[3]) {
2108        while (my ($key, $value) = each(%$header_hash)) {
2109            delete $request{$key};
2110        }
2111    }
2112
2113    # content search
2114    content_search($result{'whisker'}->{'data'},
2115                   $request{'whisker'}->{'uri'},
2116                   $request{'whisker'}->{'method'});
2117
2118    $request{'User-Agent'} = $temp_ua;    # reset UA
2119    return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'};
2120}
2121
2122#######################################################################
2123sub setup_hash {
2124    my ($reqhash, $mark, $testid) = @_;
2125
2126    # Do the standard set up for the hash
2127    LW2::http_init_request($reqhash);
2128    $reqhash->{'whisker'}->{'ssl_save_info'}              = 1;
2129    $reqhash->{'whisker'}->{'lowercase_incoming_headers'} = 1;
2130    $reqhash->{'whisker'}->{'timeout'}                    = $NIKTO{'timeout'};
2131    if (defined $CLI{'evasion'}) {
2132        $reqhash->{'whisker'}->{'encode_anti_ids'} = $CLI{'evasion'};
2133    }
2134    $reqhash->{'User-Agent'} = $NIKTO{'useragent'};
2135    $reqhash->{'User-Agent'} =~ s/\@TESTID/$testid/g;
2136    $reqhash->{'whisker'}->{'retry'} = 0;
2137    $reqhash->{'whisker'}->{'host'} = $mark->{'hostname'} || $mark->{'ip'};
2138
2139    if ($mark->{'vhost'}) {
2140        $request{'Host'} = $mark->{'vhost'};
2141    }
2142    $reqhash->{'whisker'}->{'port'} = $mark->{'port'};
2143    $reqhash->{'whisker'}->{'ssl'}  = $mark->{'ssl'};
2144
2145    # Proxy stuff
2146    if (defined $NIKTOCONFIG{PROXYHOST} && defined $CLI{'useproxy'}) {
2147        $reqhash->{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{'PROXYHOST'};
2148        $reqhash->{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{'PROXYPORT'};
2149        LW2::auth_set("proxy-basic", $reqhash, $NIKTOCONFIG{'PROXYUSER'},
2150                      $NIKTOCONFIG{'PROXYPASS'});
2151    }
2152
2153    return $reqhash;
2154}
2155
2156#######################################################################
2157sub nfetch {
2158    my ($mark, $uri, $method, $data, $headers, $flags, $testid) = @_;
2159    if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; }
2160    my (%request, %result);
2161    setup_hash(\%request, $mark, $testid);
2162
2163    $request{'whisker'}->{'uri'} = $uri;
2164    if (defined $CLI{'root'}) {
2165        $request{'whisker'}->{'uri'} = $CLI{'root'} . $uri;    # prepend -root option value
2166    }
2167    $request{'whisker'}->{'method'} = $method;
2168    my $header_hash = $headers;
2169
2170    if (defined $data) {
2171        my $r = $data;
2172        $r =~ s/\\\"/\"/g;
2173        $request{'whisker'}->{'data'} = $r;
2174    }
2175
2176    # check for extra HTTP headers
2177    if (defined $headers) {
2178
2179        # loop through the hash ref passed and add each header to request
2180        while (my ($key, $value) = each(%$header_hash)) {
2181            $request{$key} = $value;
2182        }
2183    }
2184    LW2::http_fixup_request(\%request) unless ($flags->{'noclean'});
2185   
2186    # Run pre hooks
2187    (%$request, %$result) = run_hooks($mark, "prefetch", \%request, \%result);
2188   
2189    # Cache
2190    if (   defined $CACHE{$uri}
2191        && !defined $CLI{'nocache'}
2192        && !defined $flags->{'nocache'}
2193        && $CACHE{$uri}{method} eq $method
2194        && $CACHE{$uri}{mark}   eq $mark) {
2195
2196        # Get from cache
2197        nprint("- Retrieving $uri from cache.", "v");
2198        $result{'whisker'}->{'code'} = $CACHE{$uri}{'res'};
2199        $result{'whisker'}->{'data'} = $CACHE{$uri}{'content'};
2200    }
2201    else {
2202        LW2::http_do_request_timeout(\%request, \%result);
2203        $NIKTO{'totalrequests'}++;
2204        unless (defined $CLI{'nocache'}) {
2205            $CACHE{$uri}{'method'}  = $request{'whisker'}->{'method'};
2206            $CACHE{$uri}{'res'}     = $result{'whisker'}->{'code'};
2207            $CACHE{$uri}{'content'} = $result{'whisker'}->{'data'};
2208            $CACHE{$uri}{'mark'}    = $mark;
2209        }
2210        if ($OUTPUT{debug}) {
2211            dump_var("Request Hash", \%request);
2212            dump_var("Result Hash",  \%result);
2213        }
2214    }
2215
2216    if (   defined $CLI{'display'}
2217        && ($CLI{'display'} =~ /2/)
2218        && (defined($result{'whisker'}->{'cookies'}))) {
2219        foreach my $c (@{ $result{'whisker'}->{'cookies'} }) {
2220            nprint("+ $uri sent cookie: $c");
2221        }
2222    }
2223
2224    # If headers is defined, copy the whisker headers to the hash
2225    if (defined $headers) {
2226
2227        # First clear the hash
2228        foreach my $header (keys %$headers) {
2229            delete($headers->{$header});
2230        }
2231        while (my ($key, $value) = each(%result)) {
2232            if ($key ne "whisker" && $key ne "connection") {
2233                $headers->{$key} = $value;
2234            }
2235        }
2236    }
2237
2238    # Run post hooks
2239    (%$request, %$result) = run_hooks($mark, "postfetch", \%request, \%result);
2240
2241    return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'},
2242      $result{'whisker'}->{'error'};
2243}
2244
2245#######################################################################
2246sub set_scan_items {
2247    my ($mark) = @_;
2248
2249    # load the tests
2250    my $shname = $mark->{hostname} || $mark->{ip};
2251    %TESTS = ();
2252    my @SKIPLIST = ();
2253    if (defined $NIKTOCONFIG{SKIPIDS}) {
2254        @SKIPLIST = split(/ /, $NIKTOCONFIG{SKIPIDS});
2255    }
2256
2257    # now load checks
2258    foreach my $line (@DBFILE) {
2259        if ($line =~ /^\"/)    # check
2260        {
2261            chomp($line);
2262            my @item = parse_csv($line);
2263            my $add  = 1;
2264
2265            # check tuning options
2266            if ((defined $CLI{'tuning'}) && (defined $item[2])) {
2267
2268                # Work out the required tuning from the CLI string
2269                my $exclude = 0;
2270                foreach my $tune (split(//, $CLI{'tuning'})) {
2271                    if ($tune eq "x") {
2272                        $exclude = 1;
2273                        next;
2274                    }
2275                    if ($exclude == 0) {
2276                        if ($item[2] !~ /$tune/) { $add = 0; }
2277                        next;
2278                    }
2279                    if ($exclude == 1) {
2280                        if ($item[2] =~ /$tune/) { $add = 0; }
2281                    }
2282                }
2283            }
2284
2285            # Skip list
2286            foreach my $id (@SKIPLIST) {
2287                if ($id eq $item[0]) { $add = 0; }
2288            }
2289
2290            # RFI URL Defined?
2291            if (($item[2] =~ /c/) && ($VARIABLES{'@RFIURL'} eq '')) {
2292                $add = 0;
2293            }
2294
2295            if ($add) {
2296                my $ext = get_ext($item[3]);
2297                $db_extensions{$ext} = 1;
2298
2299# This escapes regex characters in the conditionals. This will have to change if regex is ever allowed in the db
2300                for (my $y = 5 ; $y <= 9 ; $y++) { $item[$y] =~ s/([^a-zA-Z0-9\s])/\\$1/g; }
2301
2302                $mark->{total_checks}++;
2303                $TESTS{ $item[0] }{uri}         = $item[3];
2304                $TESTS{ $item[0] }{osvdb}       = $item[1];
2305                $TESTS{ $item[0] }{method}      = $item[4];
2306                $TESTS{ $item[0] }{match_1}     = $item[5];
2307                $TESTS{ $item[0] }{match_1_or}  = $item[6];
2308                $TESTS{ $item[0] }{match_1_and} = $item[7];
2309                $TESTS{ $item[0] }{fail_1}      = $item[8];
2310                $TESTS{ $item[0] }{fail_2}      = $item[9];
2311                $TESTS{ $item[0] }{message}     = $item[10];
2312                $TESTS{ $item[0] }{data}        = $item[11];
2313                $TESTS{ $item[0] }{headers}     = $item[12];
2314            }
2315        }
2316    }
2317
2318    undef @DBFILE;    # this memory hog is no longer needed!
2319    nprint("- $mark->{total_checks} server checks loaded", "v");
2320    if ($mark->{total_checks} eq 0 && !defined $CLI{'tuning'}) {
2321        nprint("+ Unable to load valid checks!");
2322        exit;
2323    }
2324    return;
2325}
2326
2327#######################################################################
2328sub max_test_id {
2329    return (sort { $a <=> $b } keys %TESTS)[-1];
2330}
2331
2332#######################################################################
2333sub char_escape {
2334    $_[0] =~ s/([^a-zA-Z0-9 ])/\\$1/g;
2335    return $_[0];
2336}
2337
2338#######################################################################
2339sub parse_csv {
2340    my $text = $_[0] || return;
2341    my @new = ();
2342    push(@new, $+) while $text =~ m{
2343      "([^\"\\]*(?:\\.[^\"\\]*)*)",?
2344       |  ([^,]+),?
2345       | ,
2346   }gx;
2347    push(@new, undef) if substr($text, -1, 1) eq ',';
2348    return @new;
2349}
2350
2351#######################################################################
2352sub version {
2353    my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, "(^nikto|^db_)");
2354    nprint "$NIKTO{'DIV'}\n";
2355    nprint "$NIKTO{'name'} Versions\n";
2356    nprint "$NIKTO{'DIV'}\n";
2357    nprint "File                               Version      Last Mod\n";
2358    nprint "-----------------------------      --------     ----------\n";
2359    nprint "Nikto main                         $NIKTO{'version'}\n";
2360    nprint "LibWhisker                         $LW2::VERSION\n";
2361
2362    foreach my $FILE (sort @NIKTOFILES) {
2363        open(FI, "<$NIKTOCONFIG{PLUGINDIR}/$FILE")
2364          || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$FILE': $!\n";
2365        my @F = <FI>;
2366        close(FI);
2367        my @VERS = grep(/^#VERSION/, @F);
2368        my @MODS = grep(/^# \$Id:/,  @F);
2369        chomp($VERS[0]);
2370        chomp($MODS[0]);
2371        my @modification = split(/ /, $MODS[0]);
2372        $VERS[0] =~ s/^#VERSION,//;
2373        my $ws1 = (35 - length($FILE));
2374        my $ws2 = (13 - length($VERS[0]));
2375        nprint "$FILE" . " " x $ws1 . "$VERS[0]" . " " x $ws2 . "$modification[4]\n";
2376    }
2377    nprint "$NIKTO{'DIV'}\n";
2378
2379    exit;
2380}
2381
2382#######################################################################
2383sub send_updates {
2384    return if ($NIKTOCONFIG{UPDATES} !~ /yes|auto/i);
2385
2386    my $have_updates = 0;
2387    my ($updated_version, $answer, $RES);
2388    foreach my $ver (keys %UPDATES) {
2389
2390        # ignore useless ones...
2391        if ($ver !~ /[0-9]/) { next; }
2392        elsif ($ver eq "Win32")          { next; }
2393        elsif ($ver eq "(Win32)")        { next; }
2394        elsif ($ver eq "Linux-Mandrake") { next; }
2395        $have_updates = 1;
2396        $updated_version .= "$ver ";
2397    }
2398
2399    if ((!$have_updates) || ($updated_version eq "")) { return; }
2400
2401    # make sure the db_outdatedb isn't *too* old
2402    open(OD, "<$NIKTOCONFIG{PLUGINDIR}/db_outdated")
2403      || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/db_outdated': $!\n";
2404    @F = <OD>;
2405    close(OD);
2406
2407    my @LASTUPDATED = grep(/^\# \$Id: db_outdated/, @F);
2408    $LASTUPDATED[0] =~ /([0-9]{4}\-[0-9]{2})/;
2409    $lm = $1;
2410    $lm =~ s/\-//g;
2411
2412    my @NOW = localtime(time);
2413    $NOW[5] += 1900;
2414    $NOW[4]++;
2415    if ($NOW[4] < 10) { $NOW[4] = "0$NOW[4]"; }
2416    my $now = "$NOW[5]$NOW[4]";
2417    if (($now - $lm) > 120) { return; }    # DB is 4 months old... ignore the updates!
2418
2419    $updated_version =~ s/\s+$//;
2420    $updated_version =~ s/^\s+//;
2421
2422    if ($NIKTOCONFIG{UPDATES} eq "auto") {
2423        $answer = "y";
2424    }
2425    else {
2426        $answer = read_data(
2427            "\n
2428      *********************************************************************
2429      Portions of the server's ident string ($updated_version) are not in
2430      the Nikto database or is newer than the known string. Would you like
2431      to submit this information (*no server specific data*) to CIRT.net
2432      for a Nikto update (or you may email to sullo\@cirt.net) (y/n)? ", ""
2433            );
2434    }
2435
2436    if ($answer !~ /y/i) { return; }
2437
2438    # set up our mark
2439    my %mark = ('ident' => 'www.cirt.net',
2440                'ssl'   => 0,
2441                'port'  => 80
2442                );
2443    ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('www.cirt.net');
2444
2445    for (my $i = 0 ; $i <= $#ARGV ; $i++) {
2446        if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) {
2447            $CLI{'useproxy'} = 1;
2448            last;
2449        }
2450    }
2451
2452    ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2453
2454    if ($RES eq 407) {
2455        if ($NIKTOCONFIG{PROXYUSER} eq "") {
2456            $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ",   "");
2457            $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho");
2458        }
2459        ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2460    }
2461
2462    if ($RES eq "") {
2463        LW2::http_close(\%request);    # force-close any old connections
2464        $mark{'ip'} = $NIKTOCONFIG{CIRT};
2465        ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET");
2466    }
2467
2468    if ($CONTENT !~ /SUCCESS/) {
2469        print STDERR "+ ERROR: ($RES, $CONTENT): Unable to send update info to CIRT.net\n";
2470    }
2471    else {
2472        nprint "- Sent updated info to CIRT.net -- Thank you!\n";
2473    }
2474
2475    return;
2476}
2477
2478#######################################################################
2479sub usage {
2480    if ($_[0] eq 2) {
2481        nprint($NIKTO{'options'});
2482    }
2483    else {
2484        nprint($NIKTO{'options_short'});
2485    }
2486    exit;
2487}
2488
2489#######################################################################
2490sub init_db {
2491    my $dbname   = $_[0];
2492    my $filename = "$NIKTOCONFIG{PLUGINDIR}/" . $dbname;
2493    my (@dbarray, @headers);
2494    my $hashref = {};
2495
2496    # Check that the database exists
2497    unless (open(IN, "<$filename")) {
2498        nprint("+ ERROR: Unable to open database file $dbname: $!.");
2499        return $dbarray;
2500    }
2501
2502    # Now read the header values
2503    while (<IN>) {
2504        chomp;
2505        s/\#.*$//;
2506        if ($_ eq "") { next }
2507        unless (@headers) {
2508            @headers = parse_csv($_);
2509        }
2510        else {
2511
2512            # contents; so split them up and apply to hash
2513            my @contents = parse_csv($_);
2514            my $hashref  = {};
2515            for (my $i = 0 ; $i <= $#contents ; $i++) {
2516                $hashref->{ $headers[$i] } = $contents[$i];
2517            }
2518            push(@dbarray, $hashref);
2519        }
2520    }
2521    return \@dbarray;
2522}
2523
2524#######################################################################
2525sub add_vulnerability {
2526    my ($mark, $message, $nikto_id, $osvdb, $method, $uri) = @_;
2527    $uri    = "/"   unless (defined $uri);
2528    $method = "GET" unless (defined $method);
2529    $osvdb  = "0"   unless (defined $osvdb);
2530
2531    my $result = "";
2532    if (defined $_[7]) {
2533        $result = $_[7]->{'whisker'}->{'data'};
2534    }
2535    my $outmessage = $message;
2536    my $resulthash = {};
2537
2538    unless ($osvdb eq "0") {
2539        $outmessage = "OSVDB-$osvdb: $message";
2540    }
2541    nprint("+ $outmessage");
2542    %$resulthash = (mark     => $mark,
2543                    message  => $message,
2544                    nikto_id => $nikto_id,
2545                    osvdb    => $osvdb,
2546                    method   => $method,
2547                    uri      => $uri,
2548                    result   => $result,
2549                    );
2550    $mark->{total_vulns}++;
2551    push(@RESULTS, $resulthash);
2552
2553    # Now report it
2554    report_item($mark, $resulthash);
2555}
2556
2557###############################################################################
2558sub list_plugins {
2559
2560    # Just do a load_plugins, then loop through the array and print out name,
2561    # description and copyright
2562
2563    load_plugins();
2564
2565    foreach my $plugin (@PLUGINS) {
2566        nprint("Plugin $plugin->{'name'}");
2567        push(@all_names, $plugin->{'name'});
2568        nprint(" $plugin->{'full_name'} - $plugin->{'description'}");
2569        nprint(" Written by $plugin->{'author'}, Copyright (C) $plugin->{'copyright'}");
2570        nprint("\n");
2571    }
2572
2573    # Plugin macros
2574    nprint("Defined plugin macros:");
2575
2576    foreach my $macro (keys %NIKTOCONFIG) {
2577        if ($macro =~ /^@@/) {
2578            nprint(" $macro = \"" . $NIKTOCONFIG{$macro} . "\"");
2579            if ($NIKTOCONFIG{$macro} =~ /@@/) {
2580                nprint("  (expanded) = \"" . expand_pluginlist($NIKTOCONFIG{$macro}, 0) . "\"");
2581            }
2582        }
2583    }
2584
2585    exit(0);
2586}
2587
2588###############################################################################
2589# This is overly complicated and jumps a lot between scalars and arrays. The REs are
2590# probably dodgy, but it works! W00!
2591sub expand_pluginlist {
2592    my ($pluginlist, $parent) = @_;
2593
2594    my @macros;
2595    foreach my $config (keys %NIKTOCONFIG) {
2596        if ($config =~ /^@@/) {
2597            push(@macros, $config);
2598        }
2599    }
2600
2601    # Now loop through each member of the list and expand it
2602    my $count       = 0;
2603    my $npluginlist = $pluginlist;
2604    do {
2605        $count++;
2606        my @raw = split(/;/, $npluginlist);
2607
2608        # cooked contains the processed list
2609        my @cooked;
2610        foreach my $entry (@raw) {
2611
2612            # Is it +; if so remap to @@DEFAULT
2613            if ($entry eq "+") {
2614                $entry = '@@DEFAULT';
2615            }
2616
2617            # result contains the processed entry
2618            my $result = $original = $entry;
2619
2620            # Is it a macro
2621            if ($entry =~ /^-?@@/) {
2622
2623                # break up into components
2624                $prefix = ($entry =~ /^-/) ? "-" : "";
2625                $name = $suffix = $entry;
2626                $name   =~ s/(^-?)(@@[[:alpha:]]+)(\(?.*\)?$)/$2/;
2627                $suffix =~ s/(.*)(\(.*\))/$2/;
2628                if ($suffix eq $entry) {
2629                    $suffix = "";
2630                }
2631                foreach my $macro (@macros) {
2632                    if ($entry =~ /-?$macro/) {
2633
2634                        # It's a macro, so replace the contents with the macro
2635                        # Add prefix and suffix to each member of the macro
2636                        my @temp;
2637                        foreach my $child (split(/;/, $NIKTOCONFIG{$macro})) {
2638                            push(@temp, "$prefix$child$suffix");
2639                        }
2640                        $result = join(';', @temp);
2641
2642                        # stop an infinite loop
2643                        last;
2644                    }
2645                }
2646            }
2647            if ($result =~ /^-?@@/ && $result eq $original) {
2648
2649                # macro not found or is itself - ignore
2650                $result = "";
2651            }
2652            if ($count > 100) {
2653
2654                # check for recurstion
2655                nprint("ERROR: Recursion found whilst expanding macros");
2656                $result = "";
2657                last;
2658            }
2659            push(@cooked, $result);
2660        }
2661        $npluginlist = join(';', @cooked);
2662    } while ($npluginlist =~ /@@/ && $count <= 100);
2663
2664    #use re 'debug';
2665    # Now we've expanded out macros, deal with duplicates and -
2666    my @raw = split(/;/, $npluginlist);
2667
2668    # hash so we don't have to mess with duplicates
2669    my %cooked;
2670    foreach my $plugin (@raw) {
2671
2672        # break out components
2673        my $minus;
2674        my $name = my $suffix = $plugin;
2675        $minus = (substr($plugin, 0, 1) eq '-');
2676        $name   =~ s/(^-?)([^\(]+)(\(?.*\)?$)/$2/;
2677        $suffix =~ s/(.*)(\(.*\))/$2/;
2678        if ($suffix eq $plugin) {
2679            $suffix = "";
2680        }
2681
2682        #nprint("P:$plugin M:$minus N:$name S:$suffix");
2683        if ($minus) {
2684
2685            # it's a minus - remove any previous entry
2686            if (exists $cooked{$name}) {
2687                delete $cooked{$name};
2688            }
2689        }
2690        else {
2691
2692            # else add it with the parameters as the value of the hash
2693            $cooked{$name} = $suffix;
2694        }
2695    }
2696
2697    # Now rejoin into one happy whole
2698    my $output;
2699    foreach my $plugin (keys %cooked) {
2700        $output .= "$plugin" . $cooked{$plugin} . ";";
2701    }
2702
2703    # remove the last ;
2704    $output =~ s/;$//g;
2705
2706    return $output;
2707}
2708#######################################################################
2709sub nikto_core { return; }    # trap for this plugin being called to run. lame.
2710#######################################################################
2711
27121;
Note: See TracBrowser for help on using the repository browser.