8.4. Building the ScannerNext we begin crafting the code for our scanner. The first thing we need to do is open our script and set up our command-line options. We use the Getopt::Std Perl module to parse the three command-line options outlined in Table 8-2.
We also need to check whether at least two arguments have been passed to the script (the two mandatory arguments of the input filename and hostname). If two arguments have not been passed, the script dies and prints out some basic syntax info: #!/usr/bin/perl use LWP::UserAgent; use strict; use Getopt::Std; my %args; getopts('c:o:v', \%args); printReport("\n** Simple Web Application Scanner **\n"); unless (@ARGV) { die "\nsimpleScanner.pl [-o <file>] [-c <cookie data>] [-v] inputfile http://hostname\n\n-c: Use HTTP Cookie\n-o: Output File\n-v: Be Verbose\n"; } Notice in the preceding code that we already called a custom subroutine, printReport. This subroutine is an extremely simple routine for printing output to the screen and/or log file. Let's jump down and take a look at it. 8.4.1. Printing OutputWe have developed a custom subroutine that our script uses for printing output. We have done this because we have a command-line option (-o) that allows all output to be sent to an output file, so we can send everything through one subroutine that handles output to both the screen and a file, if necessary. 8.4.1.1 printReport subroutineAs we just mentioned, we use the printReport subroutine to manage the printing of output to both the screen and output file, if necessary. Let's take a quick look at this routine's contents: sub printReport { my ($printData) = @_; if ($args{o}) { open(REPORT, ">>", $args{o}) or die "ERROR => Can't write to file $args{o}\n"; print REPORT $printData; close(REPORT); } print $printData; } As we mentioned, this routine is pretty simple. It takes one parameter as input (the data to be printed), appends the data to a file if the user specified the -o option ($args{o}), and prints the data to the screen. If the script cannot open the log file for writing, it dies and prints the error to the screen. Now all we have to do when we want to print something is send it to printReport, and we know it ends up printing in the right place(s). Now that we have finished the first subroutine, let's go back to the main body of the script. 8.4.2. Parsing the Input FileIf we have made it this far in the execution cycle, we know the user has provided two arguments, so we assume the first one is the input file and we attempt to open it. If the open fails, the script dies and prints the error to the screen. If the open succeeds, we populate the @requestArray array with the contents of the input file: # Open input file open(IN, "<", $ARGV[0]) or die"ERROR => Can't open file $ARGV[0].\n"; my @requestArray = <IN>; Now that we have opened our input file, the @requestArray array contains all the requests that were extracted from the input file. At this point, we can begin to process each request in the array by performing a foreach loop on the array members.
GET /public/content/jsp/news.jsp?id=2&view=F At this point in the script, we also declare a few other variables: specifically, $oResponse and $oStatus (the response content and status code generated by our request), and two hashes for storing a log of all directory- and parameter-based test combinations we perform. We use the log hashes primarily to ensure that we do not make duplicate test requests (we discuss this in greater detail later in the chapter). As we perform each loop, we assign the original request from the input file to the $oRequest variable: my ($oRequest,$oResponse, $oStatus, %dirLog, %paramLog); printReport("\n** Beginning Scan **\n\n"); # Loop through each of the input file requests foreach $oRequest (@requestArray) { Once we start the loop, the first thing we do is to remove any line-break characters from the input entry and ensure that we are dealing with a GET or a POST request; otherwise, there is no need to continue. Although every line in our input file should contain only one of these two request types, because we are accepting an external input file we need to validate this fact: # Remove line breaks and carriage returns $oRequest =~ s/\n|\r//g; # Only process GETs and POSTs if ($oRequest =~ /^(GET|POST)/) { Next, we determine whether the request contains input parameters (either in the query string of a GET request or in a POST request) by inspecting the line for the presence of a question mark (?). If we find one, we need to parse the parameters and perform input parameter testing; otherwise, we skip parameter testing and move directly to directory testing: # Check for request data if ($oRequest =~ /\?/) { For requests that contain parameter data, we perform parameter-based testing to identify a couple of common input-based vulnerabilities. Within the parameter-based testing block, the first action we perform on the request is to replay the original request (without altering any data): # Issue the original request for reference purposes ($oStatus, $oResponse) = makeRequest($oRequest); The reason we do this, although perhaps not immediately obvious, is quite simple. Our scanning tool is testing the application based on a series of specific "test requests" made to the application. The responses generated by each test request are analyzed for particular signatures indicating whether the specific vulnerability we are testing for is present. Because our findings are based on the output generated by each test request, we must be sure the presence of the vulnerability signature we are using is a direct result of our test request and not merely an attribute of a normal response. For example, let's say we are looking for the string SQL Server in the test response to identify the presence of a database error message. However, the page we are testing contains a product description for software that is "designed to integrate with SQL Server." If we aren't careful, we might mistakenly identify this page as being vulnerable simply because the string SQL Server was contained in every response. To mitigate this risk, we preserve the original "valid" responses for each page before we begin our testing to validate that our signature matches are a result of the test we are performing and not a result of the scenario just described. This helps to ensure that we do not report false positives based on the content of the page or application we are testing. 8.4.3. Making an HTTP RequestThis brings us to our next subroutine, makeRequest, which is responsible for making the actual requests during our scanning. As you can see in the last piece of code, the makeRequest subroutine is called to make the request, and it returns two variables (the status code and the response content). Let's jump down to this subroutine and take a closer look at exactly what is happening. 8.4.3.1 makeRequest subroutineThis subroutine is used to make each request we want to generate while testing the application. Keep in mind that this routine is not responsible for manipulating the request for testing purposes; it merely accepts a request and returns the response. Manipulating data for testing occurs outside of this subroutine, depending on the test being performed. We need to consider several things here, specifically the inputs and outputs of the routine. Because we have already developed a fairly simple and consistent format for storing requests in our input file, it makes sense to pass off requests to this routine using the same syntax. As such, this subroutine expects one variable to be passed to it that contains an HTTP request in the same format as our input log entries. The output requirements for this routine will directly depend on the information we need to identify, regardless of whether the test is successful. At a minimum, the request body (typically HTML) is returned so that we can analyze the contents of the response output. In addition to the response body, we need to check the status code returned by the server to determine whether certain tests resulted on success or failure. Another feature we discussed earlier was the ability for our scanner to use HTTP cookies when making test requests. Most web applications use HTTP cookies as a means of authenticating requests once the user has logged in (using a Session ID, for example). To effectively test the application, our tool needs to send these cookie(s) with each test request. To keep things simple, we assume these cookie values remain static throughout the testing session. Now we can take a close look at this subroutine. The first thing it does is declare some variables and accept one input variable (the request): sub makeRequest { my ($request, $lwp, $method, $uri, $data, $req, $status, $content); ($request)=@_; if ($args{v}) { printReport("Making Request: $request\n"); } else { print "."; } You can see we are also printing some output based on the presence of the -v (verbose) option. Note, however, that for nonverbose output we are using print instead of printReport. This is because we are printing consecutive periods (.) to the screen each time a request is made to indicate the script's progress during nonverbose execution. Although we want the verbose message to appear in the output file, we do not want these periods to appear there. Next, we set up a new instance of LWP to make the HTTP request: # Setup LWP UserAgent $lwp = LWP::UserAgent->new(env_proxy => 1, keep_alive => 1, timeout => 30, ); Now we need to parse the request data. Because we plan on performing upload testing via the HTTP PUT method, we need to support the GET, POST, and PUT methods. Both the POST and PUT methods need to pass some data in the body of the request, and as such, we need to perform a bit more processing for these two request methods. First, we split the input variable ($request) on the first space to parse out the method ($method) from the actual request data ($uri). For the POST and PUT requests, we can go ahead and parse out the data portion of the request ($data) as well by splitting the $uri variable based on a question mark: # Method should always precede the request with a space ($method, $uri) = split(/ /, $request); # PUTS and POSTS should have data appended to the request if (($method eq "POST") || ($method eq "PUT")) { ($uri, $data) = split(/\?/, $uri); } Now that we have our essential request data parsed into separate variables, we can set up the actual HTTP request. We know the hostname and cookie values being used for testing are available via the $ARGV[1] and $args{c} values, respectively (both of these are provided as inputs to the script). You'll notice here that we manually add our own custom "cookie" header value only if the $args{c} variable is populated because this is an optional switch. Although LWP does have an additional module designed specifically for handling HTTP cookies (LWP::Cookies), we don't really need the robust level of functionality this module provides because our cookie values remain static across all test requests. # Append the uri to the hostname and set up the request $req = new HTTP::Request $method => $ARGV[1].$uri; # Add request content for POST and PUTs if ($data) { $req->content_type('application/x-www-form-urlencoded'); $req->content($data); } # If cookies are defined, add a Cookie: header if ($args{c}) { $req->header(Cookie => $args{c}); } Now that the request has been constructed, we pass it to LWP and parse the response that is sent back. We already decided the two pieces of the response we are most interested in are the status code and the response content, so we extract those two pieces of the response and assign them to the $status and $content variables accordingly: my $response = $lwp->request($req); # Extract the HTTP status code and HTML content from the response $status = $response->status_line; $content = $response->content; It should be noted that the hostname or IP address ($ARGV[1]) supplied to LWP must be preceded with http:// or https:// and can optionally be followed by a nonstandard port number appended with a colon (i.e., http://www.myhost.com:81). Note in the next and final piece of this subroutine that we check for a 400 response status code. LWP returns a 400 (Bad Request) response when it is passed an invalid URL, so this response likely indicates the user did not supply a well-formed hostname. If this error occurs, the script dies and prints the error to the screen. Provided this is not the case, we return the $status and $content variables and close the subroutine: if ($status =~ /^400/) { die "Error: Invalid URL or HostName\n\n"; } return ($status, $content); } As you can see, the routine accepts one input parameter, the request, and returns two output parameters, the response status code and the response content. 8.4.4. Parameter-Based TestingNow let's go back to where we left off before we dove into makeRequest. You recall that we had just started our loop through the input file requests and had checked to see if the requests contained parameters. Now that we have replayed the original unaltered request, let's start dicing up the input file entry and generate our parameter-based test requests. Because we are within the if statement that checks for the presence of request parameters, we know any request that hits this area of the code has input parameters. As such, we perform a split on the first question mark to separate the data from the method and resource name. We assign the method and resource name (typically a web server script or file) to the $methodAndFile variable and the parameter data to the $reqData variable: #Populate methodAndFile and reqData variables my ($methodAndFile, $reqData) = split(/\?/, $oRequest, 2); Next, we split the $reqData variable into an array based on an ampersand (&). Because this character is used to join parameter name/value pairs, we should be left with an array containing each parameter name/value pair: my @reqParams = split(/\&/, $reqData); Now that @reqParams is populated with our parameter name/value pairs, we are ready to start testing individual parameters. For efficiency, our scanner tests only unique page/parameter combinations that have not yet been tested. This is important if we have a large application that makes multiple requests to a common page throughout a user's session using the same parameters. As such, the first thing we do is craft a log entry for %paramLog and add it to the hash. Because we are interested in only the page and parameter names, and not the parameter values, we loop through the parameter name/value pairs and add only the parameter name(s) to our log entry ($pLogEntry): my $pLogEntry = $methodAndFile; # Build parameter log entry my $parameter; foreach $parameter (@reqParams) { my ($pName) = split("=", $parameter); $pLogEntry .= "+".$pName; } $paramLog{$pLogEntry}++; Notice that in the last line of the preceding code, we are incrementing the value of the %paramLog hash member. If the hash member does not exist, it is added with a value of 1. If a subsequent page/parameter combination is identical, the value is incremented to 2, and so forth. To ensure that no duplicate requests are made, we test this page/parameter combination only if the log entry is equal to 1. Table 8-3 shows the current value of $pLogEntry and other key variables at this point in the script.
Once we verify that the page/parameter combination has not already been tested, we must perform two nested loops through the @reqparams array. The first loop cycles through and tests each parameter. The second loop loops through the parameter/value list and reassembles it back into a query string while replacing the value of the parameter to be tested with a placeholder value. We use the counter variable from the first loop to determine the current array member to be altered in the second loop. We use the placeholder string "--PLACEHOLDER--" in the parameter to be tested because we have more than one input validation test to perform. This allows our individual testing routines to substitute the placeholder based on their individual testing needs. At the end of each inner loop we can call the input validation testing routines. We also chop the last character off of the request because it always consists of an unnecessary ampersand (&): if ($paramLog{$pLogEntry} eq 1) { # Loop to perform test on each parameter for (my $i = 0; $i <= $#reqParams; $i++) { my $testData; # Loop to reassemble the request parameters for (my $j = 0; $j <= $#reqParams; $j++) { if ($j == $i) { my ($varName, $varValue) = split("=",$reqParams[$j],2); $testData .= $varName."="."---PLACEHOLDER---"."&"; } else { $testData .= $reqParams[$j]."&"; } } # Remove the extra & chop($testData); my $paramRequest = $methodAndFile."?".$testData; ## Perform input validation tests At this point in our loop, we can insert the individual input parameter testing routines we want to perform. As you can see, we have one test request for each request parameter, and we have replaced the parameter value to be tested with our placeholder.
Now that we have our parameter parsing logic in place, we can call whichever specific input validation tests we want to perform. The first of these tests, called sqlTest, detects potential SQL injection points. This subroutine accepts one variable (the request to be used for testing) and returns 1 if the test detects a potential vulnerability or 0 if no vulnerability is detected. We assign the output of sqlTest (the 0 or 1) to a variable called $sqlVuln: my $sqlVuln = sqlTest($paramRequest); 8.4.4.1 sqlTest subroutineBefore we start building the SQL injection testing routine, we must decide what the test should consist of. The most common technique for SQL injection testing involves the use of a single quote (') character inserted into a parameter value. In the absence of any input validation, a single quote, when passed to a database server within a query, typically generates an SQL syntax error unless it is properly escaped. The ability to invoke a database syntax error by inserting a single quote into an application parameter is a very good indication that an SQL injection point might exist. From a testing perspective, any database error message that the user can invoke is something that should be followed up on. As such, our SQL injection test consists of passing a single quote within the parameter being tested to see if the application returns a database error. Recall that the specific parameter value to be tested in each request is prepopulated with a placeholder string before the parameter parsing logic calls the test routine. This saves us some effort because the subroutine automatically knows which parameter value to test based on the presence of the placeholder string. The first thing this subroutine does is accept an input variable (the request) and substitute the placeholder string with our SQL injection string. Because all we need to do is to pass in a single quote, our test string can be something simple, such as te'st: sub sqlTest { my ($sqlRequest, $sqlStatus, $sqlResults, $sqlVulnerable); ($sqlRequest) = @_; # Replace the "---PLACEHOLDER---" string with our test string $sqlRequest =~ s/---PLACEHOLDER---/te'st/; Now that the SQL injection test request is ready, we can hand it off to the makeRequest subroutine and inspect the response. We must define the criteria used to determine whether the response indicates the presence of a vulnerability. We previously decided that the ability to invoke a database error message using our test string is a good indicator that a potential injection point might exist. As such, the easiest way to test the response is to develop a regular expression designed to identify common database errors. We must ensure that the regular expression can identify database error messages from a variety of common database servers. Figure 8-3 shows what one of these error messages typically looks like. Figure 8-3. Common SQL server error messageThe regular expression used in the following code was designed to match common database server error messages. As you can see, if the response matches our regular expression, we consider the page vulnerable and report the finding: # Make the request and get the response data ($sqlStatus, $sqlResults) = makeRequest($sqlRequest); # Check to see if the output matches our vulnerability signature. my $sqlRegEx = qr /(OLE DB|SQL Server|Incorrect Syntax|ODBC Driver|ORA-|SQL command not|Oracle Error Code|CFQUERY|MySQL|Sybase| DB2 |Pervasive|Microsoft Access|MySQL|CLI Driver|The string constant beginning with|does not have an ending string delimiter|JET Database Engine error)/i; if (($sqlResults =~ $sqlRegEx) && ($oResponse !~ $sqlRegEx)) { $sqlVulnerable = 1; printReport("\n\nALERT: Database Error Message Detected:\n=> $sqlRequest\n\n"); } else { $sqlVulnerable = 0; } Additionally, note that we are also ensuring that the original response, made before we started testing (the $oResponse variable), does not match our regular expression. This helps to reduce the likelihood of reporting a false positive, because the normal request content matches our regular expression (recall the scenario involving the product description page for software "designed to integrate with SQL Server"). Now that we have performed our test, we assign a value to the $sqlVulnerable variable to indicate whether the request detected a database error message. The final action for our subroutine is to return this variable. Returning 1 indicates that the request is potentially vulnerable; 0 indicates it is not: # Return the test result indicator return $sqlVulnerable; } Now that our SQL injection testing has been performed, we continue with our per-variable tests. Turning back to our main script routine, you'll recall we are in the midst of looping through each request variable, so we must perform the remaining per-variable tests before we continue. The next and last per-variable test to be performed is designed to detect possible XSS exposures. The subroutine for this test is called xssTest and it is structured in a way that is very similar to sqlTest. As before, we declare a new variable ($xssVuln) to assign the value returned (0 or 1) by xssTest: my $xssVuln = xssTest($paramRequest); 8.4.4.2 xssTest subroutineTo test for XSS, we inject a test string containing JavaScript into every test variable and check to see if the string gets returned in the HTTP response. A simple JavaScript alert such as the one shown here produces an easily visible result in the web browser if successful: <script>alert('Vulnerable');</script> One thing we must consider is that many XSS exposures result from HTML form fields that are populated with request parameter values. These values are typically embedded within an existing HTML form control, so any effective exploit string needs to "break out" of the existing HTML tag. To compensate for this, we modify our test string as follows: "><script>alert('Vulnerable');</script> Now that we have designed our test string, we can build the XSS testing routine. Like the other parameter test routines, it accepts a request containing a placeholder that must be replaced by our test string: sub xssTest { my ($xssRequest, $xssStatus, $xssResults, $xssVulnerable); ($xssRequest) = @_; # Replace the "---PLACEHOLDER---" string with our test string $xssRequest =~ s/---PLACEHOLDER---/"><script>alert('Vulnerable');<\/script>/; # Make the request and get the response data ($xssStatus, $xssResults) = makeRequest($xssRequest); Once again, we hand off the test request to makeRequest and inspect the HTTP response data for the presence of our test string. If the application returns the entire string (unencoded), an exploitable XSS vulnerability is likely to be present. If that is the case we assign a value of 1 to the $xssVulnerable variable and report the finding; otherwise, we set it to 0: # Check to see if the output matches our vulnerability signature. if ($xssResults =~ /"><script>alert\('Vulnerable'\);<\/script>/i) { $xssVulnerable = 1; # If vulnerable, print something to the user printReport("\n\nALERT: Cross-Site Scripting Vulnerability Detected:\n=> $xssRequest\n\n"); } else { $xssVulnerable = 0; } Note that for this test, we did not check to see whether the original response contained our test string. This is because we want to flag any page that contains this test string because there is a chance it could be the result of a previous test request made by our scanner. Additionally, unlike the SQL injection test, the odds of generating a false hit using this string are fairly low. Now that we have performed our test, the final action for our subroutine is to return the value of $xssVulnerable. Returning 1 indicates that the request is vulnerable; 0 indicates it is not: # Return the test results return $xssVulnerable; } Turning back to our main script routine, we now have completed all our parameter-based testing for the current request. We can close out the loop for each parameter value, as well as the if statements checking for unique parameter combos and request data: } # End of loop for each request parameter } # End if statement for unique parameter combos } # Close if statement checking for request data 8.4.5. Directory-Based TestingNow it's time to move on to directory-based testing. You'll recall that we had previously determined the scanner tests would consist of parameter-based and directory-based testing routines. To perform directory-based testing, we must develop some logic that loops through each directory level within the test request and calls the appropriate testing subroutines at each level. Because we want to test every directory regardless of its content, we do not discriminate against any attributes of the test request (i.e., request method, presence of parameter data, etc.). The first thing we do is isolate the path and file information from the rest of the test entry. Specifically, we strip out the request method at the beginning of the current test request ($oRequest) and any parameter data appended to it. For simplicity, we declare a trash variable ($trash) for allocating unnecessary data and keep the portion of the test request to be used in the $oRequest variable: my $trash; ($trash, $oRequest, $trash) = split(/\ |\?/, $oRequest); Now that we have isolated our path and file data, we create an array containing each directory and subdirectory from the $oRequest variable. We can do this by performing a split using a forward slash (/): my @directories = split(m{/}, $oRequest); Before we start looping through each directory level, we need to determine whether the last member of our @directories array is a filename. If the request was to a directory containing a default web server document, there is a good chance the request won't contain a filename. It is also likely that most of our requests will, in fact, contain a filename, so we need to determine this up front so that we do not confuse the two. Because most web servers require a trailing forward slash (/) when making a request to a directory with no document, we can check the last character in the test request to see if it is a forward slash. If it is, we know no filename is in the request. If it is not, we assume the last portion of the request includes a file or servlet name, and this value is the last member of our @directories array. To check the last character, we break out each character in the request to an array (@checkSlash) and refer to the last member of the array: my @checkSlash = split(//, $oRequest); my $totalDirs = $#directories; # Start looping through each directory level for (my $d = 0; $d <= $totalDirs; $d++) { if ((($checkSlash[(-1)] ne "/") && ($d == 0)) || ($d != 0)) { pop(@directories); } As you can see in the preceding code, we assign the member count from the @directories array to the $totalDirs variable, then we perform a loop starting with a counter variable ($d) at 0 and continually increment the counter by 1 until it and the $totalDirs variable are equal. Each time we loop, we remove the last member of the @directories array, effectively truncating up one level every time. The exception to this is on the first loop ($d = 0), where the last member of the $checkSlash array is equal to a forward slash (/). This condition indicates that the test request did not contain a filename (the request ended with a forward slash), thus the last member is not removed. Subsequent requests ($d != 0), however, always result in the removal of the last array member. We assigned the member count from the @directories array to the $totalDirs variable because this number changes after each loop iteration. Now that we have our directory truncation loop in place, we can create the actual request to be used by our testing subroutines. We are not particularly interested in the original request method, so we reassemble the current members of the @directories array into a GET request as follows: my $dirRequest = "GET ".join("/", @directories)."\/"; At this point in the loop, we can insert the individual directory testing routines we want to perform. For our sample request, any code placed here is hit three times, with the values in Example 8-7 assigned to the $dirRequest variable. Example 8-7. Values assigned to $dirRequestGET /public/content/jsp/ GET /public/content/ GET /public/ As you can see, we have one test request for each directory level. Just as we did with the parameter-based test requests, we keep track of each request we make to ensure that we do not make duplicate requests. We had previously declared the %dirLog hash with this specific purpose in mind, so we can use the same technique we used with %paramLog to determine if the request is unique: # Add directory log entry $dirLog{$dirRequest}++; if ($dirLog{$dirRequest} eq 1) { Now we call whichever specific directory-based tests we want to perform. The first of these testing subroutines, dirList, is used to detect whether directory listings are permitted when requesting the directory without a document: my $dListVuln = dirList($dirRequest); Let's jump down and take a peek at the dirList subroutine. 8.4.5.1 dirList subroutineBecause this subroutine is called once at each directory level, it accepts a request that is already properly formed with no default document. This makes this routine relatively simple because all it needs to do is make the request and decide whether the response contains a directory listing: sub dirList { my ($dirRequest, $dirStatus, $dirResults, $dirVulnerable); ($dirRequest) = @_; # Make the request and get the response data ($dirStatus, $dirResults) = makeRequest($dirRequest); # Check to see if it looks like a listing if ($dirResults =~ /(<TITLE>Index of \/|(<h1>|<title>)Directory Listing For|<title>Directory of|\"\?N=D\"|\"\?S=A\"|\"\?M=A\"|\"\?D=A\"| - \/<\/title>|<dir>| - \/<\/H1><hr>|\[To Parent Directory\])/i) { $dirVulnerable = 1; # If vulnerable, print something to the user printReport("\n\nALERT: Directory Listing Detected:\n=> $dirRequest\n\n"); } else { $dirVulnerable = 0; } The regular expression used in the preceding code was designed to detect IIS, Apache, and Tomcat directory listings. As with the other testing routines, we assign a value of 1 to the $dirVulnerable variable and report the finding if the expression matches; otherwise, we assign a 0 to the variable. Finally, we return this value and close the subroutine: # Return the test results. return $dirVulnerable; } Let's jump back up to our main script routine and move on to our next and final testing subroutine, dirPut, to determine if the directory permits uploading of files using the HTTP PUT method: my $dPutVuln = dirPut($dirRequest); 8.4.5.2 dirPut subroutineThe last of our testing routines is responsible for determining whether files can be uploaded using the HTTP PUT method. Like dirList, this subroutine accepts a request that is already properly formed with no default document: sub dirPut { my ($putRequest, $putStatus, $putResults, $putVulnerable); ($putRequest) = @_; Unlike the dirList routine, we need to format our request a bit more before handing it off to makeRequest. Specifically, we need to change the request method from GET to PUT, and add request data to the end of the request. Once we have done that we issue the request: # Format the test request to upload the file $putRequest =~ s/^GET/PUT/; $putRequest .= "uploadTest.txt?ThisIsATest"; # Make the request and get the response data ($putStatus, $putResults) = makeRequest($putRequest); Now that we have issued the PUT request we reformat the request to check whether the new document is in the directory. The reformatting includes changing the request method back to GET, and removing the request parameter data: # Format the request to check for the new file $putRequest =~ s/^PUT/GET/; $putRequest =~ s/\?ThisIsATest//; # Check for the uploaded file ($putStatus, $putResults) = makeRequest($putRequest); Once we issue the second request, we can check to see if our test string was returned in the content. If so, we can be sure the file was created successfully, so we set the $dirVulnerable variable to 1 and report the finding; otherwise, we set this variable to 0: if ($putResults =~ /ThisIsATest/) { $putVulnerable = 1; # If vulnerable, print something to the user printReport("\n\nALERT: Writeable Directory Detected:\n=> $putRequest\n\n"); } else { $putVulnerable = 0; } Last but not least, we return the $dirVulnerable value and close the subroutine: # Return the test results. return $putVulnerable; } At this point, we have completed all our directory-level testing routines, so we jump back up to our main script routine and close out all our loops as follows: } # End check for unique directory } # End loop for each directory level } # End check for GET or POST request } # End loop on each input file entry printReport("\n\n** Scan Complete **\n\n"); Finally, we report a message stating that testing is complete. With that, we have completed our simple web application vulnerability scanner. |