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