Detect Attacks with PHP and .htaccess

Blackhole Pro: Block Bad Bots

This tutorial explains how to detect and block security threats via .htaccess, and then pass that information to a PHP script for further processing. This is a powerful technique that combines the power of Apache with the flexibility of PHP. Enabling you to do things like log all unwanted traffic, send email reports for blocked requests, create a UI to display logged data, and just about anything else you can imagine. It’s an excellent way to keep a close eye on any malicious activity happening on your site. And a great tool to have in your belt.

Part 1: Detecting attacks

For this technique, detection happens at the server level, using Apache’s rewrite module, mod_rewrite. So for detecting and logging attacks, we can use any techniques that use mod_rewrite to handle HTTP requests. For example, we can log traffic from any of the following .htaccess techniques:

In general, each of these techniques executes the following logic:

  1. Check each HTTP request against a set of criteria
  2. Evaluate whether or not to deny access based on that criteria
  3. Redirect or block the request based on the evaluation
  4. Send the appropriate server response header

The trick for this tutorial happens in step 3. That’s where we can connect Apache/.htaccess to PHP by redirecting the request to a PHP script. For example, say we are using the following .htaccess technique to control request methods:

<IfModule mod_rewrite.c>
	RewriteRule (.*) - [F,L]

Using mod_rewrite, step 3 always happens in the RewriteRule directive. In this example, we simply are blocking the request via 403 “Forbidden” response. To better understand this, consider the logic used by the RewriteRule:

  1. (.*) – If the request matches any character
  2. - – Deny the request
  3. [F] – Send the default server 403 response
  4. [L] – Instruct Apache to stop processing the rule set

Now, here is where it gets interesting. Instead of denying the request via the RewriteRule, we can redirect the request to a custom PHP script. Our PHP script will then do whatever we want. And then after we do whatever we want, we can deny access to the request at the end of our PHP script. This way, we are using Apache to detect unwanted requests, and then using PHP to further process and log the request as desired.

Continuing with our example, let’s modify the RewriteRule so that it redirects each matched request to our custom PHP file:

<IfModule mod_rewrite.c>
	RewriteRule (.*) /path/to/custom.php [R=302,L]

That is the magic bullet for this tutorial. In the RewriteRule, we are redirecting all unwanted requests to our custom PHP script, located at /path/to/custom.php. We also can pass variables from Apache to PHP. For example, to pass the matching REQUEST_METHOD, we can append a query-string, like so:

<IfModule mod_rewrite.c>
	RewriteRule (.*) /path/to/custom.php?request_method=%1 [R=302,L]

Notice that we appended ?request_method=%1 to the URL of our PHP script. The %1 represents the pattern that was matched inside of the parentheses () in the previous RewriteCond. So in our PHP script, we can grab the query string value via the $_GET variable. It will contain either GET, HEAD, OPTIONS, and so forth. This is very powerful because it tells us the pattern that was matched by our rule set.

Part 2: Logging attacks

Equipped with the information provided in Part 1 of this tutorial, we can create a custom PHP script that will log the request data, send the data via email report, or do pretty much anything that is necessary. Let’s look at an example to get a better idea.

Let’s say that want to use the 6G Firewall to block bad query strings. Here is the code provided by 6G Firewall:

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteCond %{QUERY_STRING} (eval\() [NC,OR]
	RewriteCond %{QUERY_STRING} (127\.0\.0\.1) [NC,OR]
	RewriteCond %{QUERY_STRING} ([a-z0-9]{2000,}) [NC,OR]
	RewriteCond %{QUERY_STRING} (javascript:)(.*)(;) [NC,OR]
	RewriteCond %{QUERY_STRING} (base64_encode)(.*)(\() [NC,OR]
	RewriteCond %{QUERY_STRING} (GLOBALS|REQUEST)(=|\[|%) [NC,OR]
	RewriteCond %{QUERY_STRING} (<|%3C)(.*)script(.*)(>|%3) [NC,OR]
	RewriteCond %{QUERY_STRING} (\\|\.\.\.|\.\./|~|`|<|>|\|) [NC,OR]
	RewriteCond %{QUERY_STRING} (boot\.ini|etc/passwd|self/environ) [NC,OR]
	RewriteCond %{QUERY_STRING} (thumbs?(_editor|open)?|tim(thumb)?)\.php [NC,OR]
	RewriteCond %{QUERY_STRING} (\'|\")(.*)(drop|insert|md5|select|union) [NC]
	RewriteRule .* - [F]

This code checks the query string of each request. If the query string matches any of the 6G patterns, it will be denied access. So let’s change the RewriteRule to redirect all matching requests to our PHP script. We change the RewriteRule like so:

RewriteRule (.*) /path/to/custom.php?6g_1=%1&6g_2=%2&6g_3=%3 [R=302,L]

Nice. This new rule redirects matching requests, and also appends a query string to pass the following information:

  • 6g_1=%1 – pattern matched in the first set of parentheses
  • 6g_2=%2 – pattern matched in the second set of parentheses
  • 6g_3=%3 – pattern matched in the third set of parentheses

If any of the RewriteCond directives were to match with more than three sets of parentheses (), we could pass those data as well, by simply appending 6g_4=%4, 6g_5=%5, and so forth.

Now that we have our .htaccess properly configured, we’re pretty much done. Everything else from this point depends on what you want to do with each attack or unwanted request. For this tutorial, we will keep it simple and just say that we want to create a PHP script that does the following:

  1. Get the set of matching patterns
  2. Get the requested URI
  3. Get the query string
  4. Get the IP address
  5. Get the user agent
  6. Send all of this info in an email report

To achieve this, we can add the following basic PHP script to our file located at /path/to/custom.php.


// report blocked requests
// @

$email   = '';
$subject = 'Blocked Request Report';

$patterns = array('6g_1', '6g_2', '6g_3');

$matches = '';

foreach ($patterns as $pattern) {
	if (isset($_GET[$pattern])) {
		$matches .= !empty($_GET[$pattern]) ? htmlentities($_GET[$pattern], ENT_QUOTES, 'UTF-8') : 'n/a';
		$matches .= ', ';

$matches = trim(trim($matches), ',');

$request_uri  = isset($_SERVER['REQUEST_URI'])     ? urlencode($_SERVER['REQUEST_URI'])  : 'n/a';
$query_string = isset($_SERVER['QUERY_STRING'])    ? urlencode($_SERVER['QUERY_STRING']) : 'n/a';
$ip_address   = isset($_SERVER['REMOTE_ADDR'])     ? htmlentities($_SERVER['REMOTE_ADDR'],     ENT_QUOTES, 'UTF-8') : 'n/a';
$user_agent   = isset($_SERVER['HTTP_USER_AGENT']) ? htmlentities($_SERVER['HTTP_USER_AGENT'], ENT_QUOTES, 'UTF-8') : 'n/a';

$report  = 'Matches: '      . $matches      ."\n";
$report .= 'Request URI: '  . $request_uri  ."\n";
$report .= 'Query String: ' . $query_string ."\n";
$report .= 'IP Address: '   . $ip_address   ."\n";
$report .= 'User Agent: '   . $user_agent   ."\n";

mail($email, $subject, $report);

die('Invalid Request');


Once in place, this script will send an email report that contains all of the desired information. Some notes:

  • You can edit the email address and subject via the $email and $subject variables
  • Any empty/undefined values will be replaced with n/a
  • After sending the report, the request is blocked via die()

This is just an example so you can see how the technique works. If implementing on an actual/live site, you would want to extend and fine-tune the script to suit your needs. For example, you could log the requests to a database or text file. You also could create a fancy UI to display the logged data. When combining the power of Apache’s mod_rewrite with the flexibility of PHP, the sky’s the limit.

Related resources

If you’re into the sort of functionality discussed in this tutorial, you may want to check out the following resources.

These plugins can help secure your site against bad bots, bad requests, HTTP attacks, and other malicious activity. And they also provide statistics, email alerts, logging of request data, and much more. Full disclosure, I am the author of these plugins, so you know they’re the best around :)