1#VERSION,2.04
2###############################################################################
3#  Copyright (C) 2007 CIRT, Inc.
4#
5#  This program is free software; you can redistribute it and/or
6#  modify it under the terms of the GNU General Public License
7#  as published by the Free Software Foundation; version 2
8#  of the License only.
9#
10#  This program is distributed in the hope that it will be useful,
11#  but WITHOUT ANY WARRANTY; without even the implied warranty of
12#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#  GNU General Public License for more details.
14#
15#  You should have received a copy of the GNU General Public License
16#  along with this program; if not, write to
17#  Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18###############################################################################
19# PURPOSE:
20# Perform the full database of nikto tests against a target
21###############################################################################
22sub nikto_tests_init {
23    my $id = { name        => "tests",
24               full_name   => "Nikto Tests",
25               author      => "Sullo, Tautology",
26               description => "Test host with the standard Nikto tests",
27               copyright   => "2008 CIRT Inc.",
28               hooks       => {
29                          scan => { method => \&nikto_tests,
30                                    weight => 99,
31                                    },
32                            },
33               options => {
34                         passfiles => "Flag to indicate whether to check for common password files",
35                         all => "Flag to indicate whether to check all files with all directories",
36                         report => "Report a status after the passed number of tests",
37						 tids => "A range of testids that will only be run",
38                         }
39                 };
40    return $id;
41}
42
43sub nikto_tests {
44    return if $mark->{'terminate'};
45    my ($mark, $parameters) = @_;
46	my @tids;
47
48	if (defined $parameters->{'tids'}) {
49		@tids = expand_range($parameters->{'tids'});
50	}
51
52    # this is the actual the looped code for all the checks
53    foreach my $checkid (sort keys %TESTS) {
54        return if $mark->{'terminate'};
55        if ($checkid >= 500000) { next; }    # skip TESTS added manually during run (for reports)
56                                             # replace variables in the uri
57	my $intcheck=int($checkid);
58	if ((scalar(@tids) > 0) && !grep(/^$intcheck$/, @tids)) { next; }
59        my @urilist = change_variables($TESTS{$checkid}{'uri'});
60
61        # Now repeat for each uri
62        foreach my $uri (@urilist) {
63            return if $mark->{'terminate'};
64            my %headrs;
65            my %flags;
66            if ($TESTS{$checkid}{'headers'}) {
67                %headrs=split /: /, $TESTS{$checkid}{'headers'};
68                $headrs{'host'} = $mark->{'hostname'} unless ($headrs{'host'}); # Kludge not to override host injection vectors
69                $flags{'noclean'} = 1;
70            }
71            my ($res, $content, $error, $request, $response) =
72              nfetch($mark, $uri,
73                     $TESTS{$checkid}{'method'},
74                     $TESTS{$checkid}{'data'},
75                     \%headrs,
76                     \%flags,
77                     $checkid);
78
79            # NOTE: auth is now done in nfetch
80            if ($OUTPUT{'show_ok'} && ($res eq 200)) {
81                nprint("+ $mark->{'root'}$uri - 200/OK Response could be $TESTS{$checkid}{'message'}");
82            }
83            elsif ($OUTPUT{'show_redirects'} && ($res =~ /30(?:[0-3]|7)/)) {
84                nprint(  "+ $mark->{'root'}$uri - Redirects ($res) to "
85                       . $response->{'location'} . " , " . $TESTS{$checkid}{'message'});
86            }
87
88            # If user puts a @CODE= string in db_404_strings, let it take precedence
89            foreach my $code (keys %{ $VARIABLES->{'ERRCODES'} }) {
90                if ($res =~ /$code/) {
91                    return;
92                }
93            }
94
95            my $m1_method = my $m1o_method = my $m1a_method = my $f2_method = my $f1_method =
96              "content";
97            my ($positive, $hashme) = 0;
98            my ($hash, $reason) = '';
99
100            # how to check each conditional
101            if ($TESTS{$checkid}{'match_1'} =~ /^[0-9]{3}$/) { $m1_method = "code"; }
102            elsif ($TESTS{$checkid}{'match_1'} =~ /^\@MD5/) {
103                $m1_method = "md5";
104                $hashme    = 1;
105                $TESTS{$checkid}{'match_1'} =~ s/^\@MD5//;
106            }
107
108            if ($TESTS{$checkid}{'match_1_or'} =~ /^[0-9]{3}$/) { $m1o_method = "code"; }
109            elsif ($TESTS{$checkid}{'match_1_or'} =~ /^\@MD5/) {
110                $m1o_method = "md5";
111                $hashme     = 1;
112                $TESTS{$checkid}{'match_1_or'} =~ s/^\@MD5//;
113            }
114
115            if ($TESTS{$checkid}{'match_1_and'} =~ /^[0-9]{3}$/) { $m1a_method = "code"; }
116            elsif ($TESTS{$checkid}{'match_1_and'} =~ /^\@MD5/) {
117                $m1a_method = "md5";
118                $hashme     = 1;
119                $TESTS{$checkid}{'match_1_and'} =~ s/^\@MD5//;
120            }
121
122            if ($TESTS{$checkid}{'fail_1'} =~ /^[0-9]{3}$/) { $f1_method = "code"; }
123            elsif ($TESTS{$checkid}{'fail_1'} =~ /^\@MD5/) {
124                $f1_method = "md5";
125                $hashme    = 1;
126                $TESTS{$checkid}{'fail_1'} =~ s/^\@MD5//;
127            }
128
129            if ($TESTS{$checkid}{'fail_2'} =~ /^[0-9]{3}$/) { $f2_method = "code"; }
130            elsif ($TESTS{$checkid}{'fail_2'} =~ /^\@MD5/) {
131                $f2_method = "md5";
132                $hashme    = 1;
133                $TESTS{$checkid}{'fail_2'} =~ s/^\@MD5//;
134            }
135
136            if ($hashme) {
137                $hash = LW2::md5($content);
138            }
139
140            # basic match for positive response
141            if ($m1_method eq "content") {
142                if ($content =~ /$TESTS{$checkid}{'match_1'}/) {
143                    $positive = 1;
144                    $reason = 'Content Match';
145                }
146            }
147            elsif ($m1_method eq "md5") {
148                if ($hash eq $TESTS{$checkid}{'match_1'}) {
149                    $positive = 1;
150                    $reason = 'MD5 Hash';
151                }
152            }
153            else {
154                if ($res eq $TESTS{$checkid}{'match_1'}) {
155                    $positive = 1;
156                    $reason = 'Response Code Match';
157                }
158                elsif ($res eq $FoF{'okay'}{'response'}) {
159                    $positive = 1;
160                    $reason = 'Response Code Match - FoF OK Response)';
161		}
162            }
163
164            # no match, check optional match
165            if ((!$positive) && ($TESTS{$checkid}{'match_1_or'} ne "")) {
166                if ($m1o_method eq "content") {
167                    if ($content =~ /$TESTS{$checkid}{'match_1_or'}/) {
168                        $positive = 1;
169                    	$reason = 'Content Match - Match 1 (Or)';
170                    }
171                }
172                elsif ($m1o_method eq "md5") {
173                    if ($hash eq $TESTS{$checkid}{'match_1_or'}) {
174                        $positive = 1;
175                    	$reason = 'MD5 Hash - Match 1 (Or)';
176                    }
177                }
178                else {
179                    if ($res eq $TESTS{$checkid}{'match_1_or'}) {
180                        $positive = 1;
181                        $reason = 'Response Code Match - Match 1 (Or)';
182			}
183                   elsif ($res eq $FoF{'okay'}{'response'}) {
184                        $positive = 1;
185                        $reason = 'Response Code Match - FoF OK Response / Match 1 (Or)';
186                    }
187                }
188            }
189
190            # matched on something, check fails/ands
191            if ($positive) {
192                if ($TESTS{$checkid}{'fail_1'} ne "") {
193                    if ($f1_method eq "content") {
194                        if ($content =~ /$TESTS{$checkid}{'fail_1'}/) { next; }
195                    }
196                    elsif ($f1_method eq "md5") {
197                        if ($hash eq $TESTS{$checkid}{'fail_1'}) {
198                            next;
199                        }
200                    }
201                    else {
202                        if ($res eq $TESTS{$checkid}{'fail_1'}) { next; }
203                    }
204                }
205                if ($TESTS{$checkid}{'fail_2'} ne "") {
206                    if ($f2_method eq "content") {
207                        if ($content =~ /$TESTS{$checkid}{'fail_2'}/) { next; }
208                    }
209                    elsif ($f2_method eq "md5") {
210                        if ($hash eq $TESTS{$checkid}{'fail_2'}) {
211                            next;
212                        }
213                    }
214                    else {
215                        if ($res eq $TESTS{$checkid}{'fail_2'}) { next; }
216                    }
217                }
218                if ($TESTS{$checkid}{'match_1_and'} ne "") {
219                    if ($m1a_method eq "content") {
220                        if ($content !~ /$TESTS{$checkid}{'match_1_and'}/) { next; }
221			else { $reason .= ' and content Match1 (And)'; }
222                    }
223                    elsif ($m1a_method eq "md5") {
224                        if ($hash ne $TESTS{$checkid}{'match_1_and'}) {
225                            next;
226                        }
227			else { $reason .= ' and MD5 Match1 (And)'; }
228                    }
229                    else {
230                        if ($res ne $TESTS{$checkid}{'match_1_and'}) { next; }
231			else { $reason .= ' and Response Code Match1 (And)'; }
232                    }
233                }
234
235                # if it's an index.php, check for normal /index.php to see if it's a FP
236                if ($uri =~ /^\/index.php\?/) {
237                    my $content = rm_active_content($content, $mark->{'root'} . $uri);
238                    if (LW2::md4($content) eq $FoF{'index.php'}{'match'}) { next; }
239                }
240
241                # lastly check for a false positive based on file extension or type
242                if (($m1_method eq "code") || ($m1o_method eq "code")) {
243                    if (is_404($mark->{'root'} . $uri, $content, $res, $response->{'location'}))
244                    {
245                        next;
246                    }
247                }
248
249                $TESTS{$checkid}{'osvdb'} =~ s/\s+/ OSVDB\-/g;
250		if ($positive) {
251                	add_vulnerability($mark,
252                                  "$mark->{'root'}$uri: $TESTS{$checkid}{'message'}",
253                                  $checkid,
254                                  $TESTS{$checkid}{'osvdb'},
255                                  $TESTS{$checkid}{'method'},
256                                  $mark->{'root'} . $uri,
257                                  $request,
258                                  $response,
259                                  $reason
260                                  );
261		}
262            }
263        }
264
265        # Percentages
266        if (($OUTPUT{'progress'}) && ($parameters->{'report'})) {
267            if (($COUNTERS{'totalrequests'} % $parameters->{'report'}) == 0) {
268                status_report();
269            }
270        }
271    }    # end check loop
272
273    # Perform mutation tests
274    if ($parameters->{'passfiles'}) {
275        passchecks($mark);
276    }
277    if ($parameters->{'all'}) {
278        allchecks($mark);
279    }
280
281    return;
282}
283
284sub passchecks {
285    my ($mark) = @_;
286    my @DIRS   = (split(/ /, $VARIABLES{"\@PASSWORDDIRS"}));
287    my @PFILES = (split(/ /, $VARIABLES{"\@PASSWORDFILES"}));
288    my @EXTS = qw(asp bak dat data dbc dbf exe htm html htx ini lst txt xml php php3 phtml);
289
290    nprint("- Performing passfiles mutation", "v");
291
292    # Update total requests for status reports
293    my @CGIS = split(/ /, $VARIABLES{'@CGIDIRS'});
294    $COUNTERS{'total_checks'} =
295      $COUNTERS{'total_checks'} +
296      (scalar(@DIRS) * scalar(@PFILES)) +
297      (scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS)) +
298      ((scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS) * scalar(@CGIS)) * 2);
299
300    foreach my $dir (@DIRS) {
301        return if $mark->{'terminate'};
302        foreach my $file (@PFILES) {
303            next if ($file eq "");
304
305            # dir/file
306            testfile($mark, "$dir$file", "passfiles", "299998");
307
308            foreach my $ext (@EXTS) {
309                return if $mark->{'terminate'};
310
311                # dir/file.ext
312                testfile($mark, "$dir$file.$ext", "passfiles", "299998");
313
314                foreach my $cgi (@CGIS) {
315                    $cgi =~ s/\/$//;
316
317                    # dir/file.ext
318                    testfile($mark, "$cgi$dir$file.$ext", "passfiles", "299998");
319
320                    # dir/file
321                    testfile($mark, "$cgi$dir$file", "passfiles", "299998");
322                }
323            }
324        }
325    }
326}
327
328sub allchecks {
329    my ($mark) = @_;
330
331    # Hashes to temporarily store files/dirs in
332    # We're using hashes to ensure that duplicates are removed
333    my (%FILES, %DIRS);
334
335    # build the arrays
336    nprint("- Loading root level files", "v");
337    foreach my $checkid (keys %TESTS) {
338
339        # Expand out vars so we get full matches
340        my @uris = change_variables($TESTS{$checkid}{'uri'});
341
342        foreach my $uri (@uris) {
343            my $dir  = LW2::uri_get_dir($uri);
344            my $file = $uri;
345
346            if ($dir ne "") {
347                $DIRS{$dir} = "";
348                $dir  =~ s/([^a-zA-Z0-9])/\\$1/g;
349                $file =~ s/$dir//;
350            }
351            if (($file ne "") && ($file !~ /^\?/)) {
352                $FILES{$file} = "";
353            }
354        }
355    }
356
357    # Update total requests for status reports
358    $COUNTERS{'total_checks'} = $COUNTERS{'total_checks'} + (keys(%DIRS) * keys(%FILES));
359
360    # Now do a check for each item - just check the return status, nothing else
361    foreach my $dir (keys %DIRS) {
362        foreach my $file (keys %FILES) {
363            return if $mark->{'terminate'};
364            testfile($mark, "$dir$file", "all checks", 299999);
365        }
366    }
367}
368
369sub testfile {
370    return if $mark->{'terminate'};
371    my ($mark, $uri, $name, $tid) = @_;
372    my ($res, $content, $error, $request, $response) =
373      nfetch($mark, $uri, "GET", "", "", "", "Tests: $name");
374    nprint("- $res for $uri (error: $error)", "v");
375    if ($error) {
376        $mark->{'total_errors'}++;
377        nprint("+ ERROR: $uri returned an error: $error", "e");
378        return;
379    }
380    if ($res == 200) {
381        add_vulnerability($mark, "$uri: file found during $name mutation", "$tid", "0", "GET", $uri, $request, $response);
382    }
383}
384
3851;
386