Current .htaccess Miscreant Traps

I update this from time to time based on current attack trends. It’s a list of .htaccess entries to redirect miscreants looking for vulnerable pages on CMS platforms to a trap page that blocks and reports them.

Obviously don’t use any pages or directories that actually exist on the site. This is really only useful on hand-coded sites that bots attack assuming they’re running on a CMS.

RewriteEngine On
RewriteCond %{REQUEST_URI} /admin.php [NC,OR]
RewriteCond %{REQUEST_URI} /install.php [NC,OR]
RewriteCond %{REQUEST_URI} /lequ.php [NC,OR]
RewriteCond %{REQUEST_URI} /login.php [NC,OR]
RewriteCond %{REQUEST_URI} /setup.php [NC,OR]
RewriteCond %{REQUEST_URI} /shell.php [NC,OR]
RewriteCond %{REQUEST_URI} /user.php [NC,OR]
RewriteCond %{REQUEST_URI} /webconfig.txt.php [NC,OR]
RewriteCond %{REQUEST_URI} /wp-config.php [NC,OR]
RewriteCond %{REQUEST_URI} /wp-contacts.php [NC,OR]
RewriteCond %{REQUEST_URI} /wp-login.php [NC,OR]
RewriteCond %{REQUEST_URI} /xmlrpc [NC,OR]
RewriteCond %{REQUEST_URI} /xmlrpc.php [NC,OR]
RewriteCond %{REQUEST_URI} ^/2020/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/2019/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/admin/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/administrator/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/config/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/blog/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/cms/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/data/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/demo/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/fckeditor/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/.git/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/inc/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/install/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/magento/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/manager/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/news/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/plus/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/setup/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/shop/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/site/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/staging/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/templates/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/test/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/web/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/website/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wordpress/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp1/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp2/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-admin/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-contacts/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-includes/(.*)$ [NC]
RewriteRule .* https://example.com/trap-page.php?req=%{THE_REQUEST} [L]

Many (but not all) of the entries on my blocklists and my AbuseIPDB page are generated this way. The trap page enters them into the database from whence the blocklists are created, as well as reports them to AbuseIPDB.

Richard

3 Likes

A sample trap page:

<?php
date_default_timezone_set('America/New_York');
$ip=$_SERVER['REMOTE_ADDR'];
//ip="127.0.0.2"; // for testing
$origRequest =  $_GET['req'];
putenv("TZ=US/Eastern");
$timeNow = time();
$fresh = time() - 900;
$domain = "example.com"; // the domain the script is installed on
$currentDateTime = (date("M d, Y h:i:s a"));
$ports="80,443";
$categories="15,21"; // for AbuseIPDB Report

$con = mysqli_connect("example.com","database_user","password","database");
    if (!$con) { die('Could not connect: ' . mysqli_error($con)); }
    $result = mysqli_query($con, "SELECT * FROM table WHERE (ip LIKE '$ip' AND time >= '$fresh')");
    $row = mysqli_fetch_array($result);
        $reportDate = $row['datetime'];
        if (empty($reportDate)) {
        // sanitize
        $timeNow = mysqli_real_escape_string($con, $timeNow);
        $ip = mysqli_real_escape_string($con, $ip);
        $domain = mysqli_real_escape_string($con, $domain);
        $currentDateTime = mysqli_real_escape_string($con, $currentDateTime);
        $origRequest =  mysqli_real_escape_string($con, $origRequest);
        $comment=$origRequest; // for AbuseIPDB Report and database entry
        /* If origRequest is stripped out by mysqli_real_escape_string, it means it contained malicious SQL code. Therefore:*/
        if (empty($origRequest)) {
            $comment = "Web-based SQL injection attempt";
            $categories = "15,16,21";
        }
        // insert to db
        mysqli_select_db($con, "database");
        $sql = "INSERT INTO reports (datetime, time, ip4, ports, categories, domain, origRequest, comment) VALUES ('$currentDateTime','$timeNow','$ip','$ports','$categories','$domain','$origRequest','$comment')";
        if (!mysqli_query($con,$sql)) {
            echo("Error description: " . mysqli_error($con));
            }
        // make report
        $data = (array(
            "ip"  => $ip,
            "categories" => $categories,
            "comment" => $comment
        ));
        $headers =  array('Key: the-abuseipdb-key-goes-here', 'Accept: application/json');
        $ch = curl_init("https://api.abuseipdb.com/api/v2/report");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); // Set to 0 for testing to display response from AbuseIPDB
        curl_setopt($ch, CURLOPT_POST,           1 );
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $output=curl_exec($ch);
        curl_close($ch);
    }
include("401.php");
?>

Ancient code, but it still works.

Richard

1 Like

I’ve made a few updates to this. One is experimental, but I can’t think of a reason why it would be a problem: I added the following to .htaccess right before the rewrites:

# remove duplicate slashes to frustrate miscreants
RewriteCond %{THE_REQUEST} ^[A-Z]{3,}\s/{2,} [NC]
RewriteRule ^(.*) $1 [R=301]

I’ve noticed that a lot of pests are doubling up the slashes, for example:

https://domain.tld//wordpress/

to get around the trap. It works unless MergeSlashes in enabled in Apache, which won’t be the case if you’re on CentOS 7. (It was introduced in a more recent Apache version, though I don’t know which offhand.) Rewriting to remove the extra slash(es) lands them in the traps.

The second change was to add

RewriteCond %{REQUEST_URI} ^/wlwmanifest.xml [NC,OR]

To the list, anywhere except the last line (because of the OR). There’s been a lot of action on that file lately.

Richard

1 Like

Okay, so the problem with that is that the 301 and 302 responses clue in the bot that it’s being redirected. So dispense with the code to rewrite the slashes, and do this instead:

# Honeypot for non-existent cms login attempts
RewriteCond %{REQUEST_URI} /admin.php [NC,OR]
RewriteCond %{REQUEST_URI} /install.php [NC,OR]
RewriteCond %{REQUEST_URI} /lequ.php [NC,OR]
RewriteCond %{REQUEST_URI} /login.php [NC,OR]
RewriteCond %{REQUEST_URI} /setup.php [NC,OR]
RewriteCond %{REQUEST_URI} /shell.php [NC,OR]
RewriteCond %{REQUEST_URI} /user.php [NC,OR]
RewriteCond %{REQUEST_URI} /webconfig.txt.php [NC,OR]
RewriteCond %{REQUEST_URI} /wlwmanifest.xml [NC,OR]
RewriteCond %{REQUEST_URI} /wp-config.php [NC,OR]
RewriteCond %{REQUEST_URI} /wp-contacts.php [NC,OR]
RewriteCond %{REQUEST_URI} /wp-login.php [NC,OR]
RewriteCond %{REQUEST_URI} /xmlrpc [NC,OR]
RewriteCond %{REQUEST_URI} /xmlrpc.php [NC,OR]
RewriteCond %{REQUEST_URI} ^/2020/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/2019/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/admin/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/administrator/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/config/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/blog/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/cms/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/data/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/demo/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/fckeditor/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/.git/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/inc/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/install/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/magento/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/manager/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/media/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/news/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/old/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/plus/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/setup/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/shop/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/site/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/sito/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/staging/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/templates/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/test/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/web/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/website/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wordpress/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp1/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp2/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-admin/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-contacts/(.*)$ [NC,OR]
RewriteCond %{REQUEST_URI} ^/wp-includes/(.*)$ [NC]
RewriteRule .* /trap.php?req=%{THE_REQUEST} [PT,L]

So the request is passed through without a response code. Then once it’s too late for the bot to do about it, the trap page is executed and (optionally) a 401 or 403 returned for use in a firewall, etc.

<?php
header('X-PHP-Response-Code: 401', true, 401);
date_default_timezone_set('America/New_York');
$ip=$_SERVER['REMOTE_ADDR'];
//ip="127.0.0.2"; // for testing
$origRequest =  $_GET['req'];
putenv("TZ=US/Eastern");
$timeNow = time();
$fresh = time() - 900;
$domain = "example.com"; // the domain the script is installed on
$currentDateTime = (date("M d, Y h:i:s a"));
$ports="80,443";
$categories="15,21"; // for AbuseIPDB Report

$con = mysqli_connect("example.com","database_user","password","database");
    if (!$con) { die('Could not connect: ' . mysqli_error($con)); }
    $result = mysqli_query($con, "SELECT * FROM table WHERE (ip LIKE '$ip' AND time >= '$fresh')");
    $row = mysqli_fetch_array($result);
        $reportDate = $row['datetime'];
        if (empty($reportDate)) {
        // sanitize
        $timeNow = mysqli_real_escape_string($con, $timeNow);
        $ip = mysqli_real_escape_string($con, $ip);
        $domain = mysqli_real_escape_string($con, $domain);
        $currentDateTime = mysqli_real_escape_string($con, $currentDateTime);
        $origRequest =  mysqli_real_escape_string($con, $origRequest);
        $comment=$origRequest; // for AbuseIPDB Report and database entry
        /* If origRequest is stripped out by mysqli_real_escape_string, it means it contained malicious SQL code. Therefore:*/
        if (empty($origRequest)) {
            $comment = "Web-based SQL injection attempt";
            $categories = "15,16,21";
        }
        // insert to db
        mysqli_select_db($con, "database");
        $sql = "INSERT INTO reports (datetime, time, ip4, ports, categories, domain, origRequest, comment) VALUES ('$currentDateTime','$timeNow','$ip','$ports','$categories','$domain','$origRequest','$comment')";
        if (!mysqli_query($con,$sql)) {
            echo("Error description: " . mysqli_error($con));
            }
        // make report
        $data = (array(
            "ip"  => $ip,
            "categories" => $categories,
            "comment" => $comment
        ));
        $headers =  array('Key: the-abuseipdb-key-goes-here', 'Accept: application/json');
        $ch = curl_init("https://api.abuseipdb.com/api/v2/report");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); // Set to 0 for testing to display response from AbuseIPDB
        curl_setopt($ch, CURLOPT_POST,           1 );
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $output=curl_exec($ch);
        curl_close($ch);
    }
?>
<!DOCTYPE HTML>
<html>
<head>
    <title>Busted</title>
</head>
<body>
    <p style="text-align: center"><img src="/Images/401.png" style="max-width: 90%" alt="Outstretched hand with palm facing user in a halt gesture inside a circle with a slash."></p>
</body>
</html>

A bot (or human) might not continue with the attack once they get a 301 or 302 response, so using the pass through (the PT in the [PT,L] directive) prevents the response from being sent. It just executes the script and renders the content of trap.php in the same browser window and URL. By that time they’ve been trapped and the report has been sent.

Hey, everyone needs a hobby.

Richard

Okay, that’s much better. Apache returns a 200 even though the miscreant is being redirected to a trap:

The attempt is logged into the database that feeds my own blocklists:

And reported to AbuseIPDB. Note that only four members have reported this particular IP address, which likely means that the miscreant is avoiding traps that redirect:

So I think using the pass through is the ticket to catching more scumbag hackers.

Richard

If you want the 401 (or 403) returned at the end, after the IP has been reported, then replace this


<!DOCTYPE HTML>
<html>
<head>
    <title>Busted</title>
</head>
<body>
    <p style="text-align: center"><img src="/Images/401.png" style="max-width: 90%" alt="Outstretched hand with palm facing user in a halt gesture inside a circle with a slash."></p>
</body>
</html>

with an include to the 401 or 403 error page, for example

<?php include("401.php"); ?>

By that time the activity will have been logged and reported; and if the firewall is configured to block repeated 401 / 403 hits, locally blocked. But the trap will have run already at that point.

well nice solutions… why try to fix something which is not broken??? - just deploy f2b

Mainly because my hobby of busting the balls of hackers, crackers, spammers, and other Internet vermin long preceded the existence of f2b. I started doing this sort of thing in earnest in the late 1990’s.

I think it was 2006 when the beta of CSF came out, and I immediately started looking for ways for it to feed data into my databases. Back then it was a private service for myself and a few colleagues to share data about attacks between our servers. We all gathered data that I tabulated into lists that could be imported into CSF. We pooled our attack data, as it were.

Unfortunately, I’m the sole remaining survivor of that group; so nowadays I generate free public blocklists that anyone can import daily, or paid blocklists that can be subscribed to for peanuts. About 17,000 servers around the world download my blocklists daily.

Unfortunately, I can’t tell you where to find them because it seems that some crybaby or another complains whenever I post a link to one of my own sites because the forum pages disappear without explanation. Oh well. C’est la vie.

What sets my blocklists apart is that they are self-rehabilitating. When an IP address stops misbehaving, it drops off the list about 72 hours later. No request needs to be made. It just drops off once it’s no longer doing whatever it did to get on my bad side.

Right now I’m in the early stages of working on a way to get sufficient data out of SpamAssassin to feed into an ephemeral spam database. I’ve been too busy to really work on it much, but it’s on the agenda. As with the one I can’t tell you about, it will be self-rehabilitating. IP addresses that behave themselves will be removed from the database within 72 hours.

At present, CSF generates between 40 and 55 percent of the total entries that wind up in my blocklists in an average month. The rest come from scripts like this one, honeypots, and traps on production Web forms. So it would be foolish not to use it; and if I’m using CSF, I really don’t need f2b. I can make CSF monitor and report anything I want it to.

Richard

1 Like

While I’m using f2b myself: I agree, that Richard’s Traps sounds great.

f2b does not detect connection/tries that come within several hours and/or separately from different IPs.

I’ve seen a similar approach called 7G firewall. It protects from unneeded waste of resources by not allowing to load e.g. the full CMS stack before sending a 404, like Richards Trap.

But 7G does not collect the intruder’s IPs nor saves it for further processing like sending to abuse DB or controlling firewall rules.

1 Like

Thanx @RJM_Web_Design for your detailed posting.

If you do this replacement … do we get the 401/403 in the apache/nginx logs, too, instead of the 200s like in your screenshot above?

I haven’t done this with nginx, so I can’t say.

I also haven’t used the 401 / 403 include AND the passthrough in .htaccess in the same implementation, but it does without the passthrough. I can’t think of a reason why the passthrough would make a difference as long as the 401 / 403 page is included.

I just set it up that way on one site. I’ll let you know.

EDIT: Yes, it does generate a 401 / 403 error in the Apache log in the error page is included, even with the PT directive in .htaccess. In fact, it generated two 401’s with a 302 in between them because I forgot to remove the 401 header from the trap page itself. So if you’re going to include the 401 / 403 page, you can dispense with that header on the trap page.

Richard

1 Like

@RJM_Web_Design great, thank you for your quick reply and instant check.

1 Like

My pleasure.

By way of a caveat, there’s a downside to all of this:

When you mess with vermin, they sometimes fight back. Of course, it’s stupid because all those particular reports were generated by CSF after it blocked the IP’s. So the incessant DBF attack that’s been going on since yesterday has only resulted in the IP’s being blocked in the firewall itself, added to my blocklists, and reported to AbuseIPDB.

The attackers are also wasting their time because most of the attacks have been directed toward port 22, which is not in use, but is still being monitored. So aside from giving CSF and my scripts some exercise, they’re doing no harm and a great deal of good by exposing themselves.

I can’t link to my own sites without some unknown crybaby wetting his or her diaper; but you can probably figure out where the blocklist site is if you think of a domain name a guy with the initials RJM might choose for a blocklist site.

Richard