up arrow How To Stop A WordPress Dictionary Attack

You guys! Lest we forget, Your Joe Dog was under attack!

Apparently there’s a widespread dictionary attack that uses tens of thousands of malwared computers to attack WordPress sites. Your JoeDog uses WordPress as a CMS. Your JoeDog was attacked!

The extent of the attack was not initially clear. I was alerted by sluggish performance. I noticed a lot of POSTs to wp-login.php. Those POSTs appeared in the access log like this: - - [17/Jun/2013:09:06:42 -0400] "POST /wp-login.php HTTP/1.0" 
200 3444 "-" "Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/2010 Firefox/19.0"

I have a script that allows me to quickly block IP addresses with iptables. So I started harvesting addresses and blocking them. Done and done.

Except the attacker seemed to have an endless supply of IP addresses. The attack persisted no matter how many addresses I blocked.

Take a look at the log entry above. The referer field is empty. A JoeDog Fellow suggested I block all POSTs that don’t include a referer. Afterall, you don’t POST out of the blue – you submit a form in your browser. I blocked those types of requests with a simple mod_rewrite rule:

 RewriteCond %{HTTP_REFERER} ^-?$
 RewriteRule ^/(wp-login.php|wp-admin) - [F,NS,L]

Done and done. Amiright? Sadly, no….

Soon after I added that rule, I noticed someone was crafting wp-login.php requests in a browser. It didn’t take long for them to determine the rule I used to “thwart” them. Entries started to appear in the logs that looked similar to this one: - - [17/Jun/2013:09:03:41 -0400] "POST /wp-login.php HTTP/1.0"
200 3444 "www.joedog.org/wp-login.php" "Mozilla/5.0 (Windows NT 6.1; rv:19.0)
Gecko/20100101 Firefox/19.0"

See what he did? He hard-coded a referer into his script. If a browser would have submitted that request, it would have included an http:// scheme in the referer field.

I had to stop this guy. When he was able to send requests that generated database transactions, it brought the site to its knees. As a stop-gap measure, I password protected the login page:

<FilesMatch "wp-login.php">
  AuthType Basic
  AuthName "Kiss my fscking ass"
  AuthUserFile /path/to/my/file
  Require user franklindelanoroosevelt

Apache doesn’t require many resources to say “401, gimme a password!” The site was no longer on its knees but now I had two different logins in order to gain access. PITA!

I considered allowing access by IP address, but mine changes frequently so I’d be as likely to block myself as the hacker. I wanted something that was convenient to me and provided enough obstacle for him to move along in another direction.

I discovered an interesting hack on the apache website. You can allow access to requests based on environment variables. If the variable is set, then let them in. Way cool!

In the example on the apache site, they set an env variable based on a User-agent. It’s easy to find a browser add-on that let’s you set a custom agent. Unfortunately, developers often deliver custom content based on User-agents. By using this approach, I could alter the look and feel of the web.

Here was the solution I finally went with: Custom headers.

I added a Chrome module called Extra Headers. It allows you to send a custom header to a website based on its domain. The module appears as a ghost icon after you install it. Click the ghost, then click options. On that page you’ll find a form that allows you to add a header:


Now we need to give it a domain to which it will send our X-MamaMia header:


So when our domain is joedog.org, we send a X-MamaMia header. Now all we have to do is give it a value.  To do that, click on the ghost again. You should see your custom header and an empty field. Put a value in the empty field. In this example, I’m going to add “iszesty”.  Now every time I visit joedog.org, I’ll send the following header:

X-MamaMia: iszesty

Now we’re ready to add our access controls to the web server. You want to add something like this to your apache config:

 SetEnvIf X-MamaMia iszesty thats_a_meatball
 <Directory "/path/to/wp-admin">
   Order deny,allow
   Deny from all
   Allow from env=thats_a_meatball
 <FilesMatch "wp-login.php">
   Order deny,allow
   Deny from all
   Allow from env=thats_a_meatball

Now, we’re finally done and done. In order to request wp-login.php or anything inside wp-admin, you must send a custom header which matches the one in our apache config. You have to clear that hurdle, just to try guessing our actual username and password in the login form.

One more thing: If you’re using WordPress, don’t use an ‘admin’ account. Set up another account with admin privileges then delete the admin account (When you delete admin, WordPress will let you transfer ownership to the new users).

UPDATE: I inadvertently blocked Mrs. JoeDog. She used the login page while I was harvesting IP addresses to block with iptables. I found the address and removed it. Now she’s back again….

  • Gus

    hi Jeff,

    Thank you for publishing this. I must say I am a novice when it comes down to configuring apache’s conf file. Where exactly does one add the environmental variable portion? After I added it to httpd.conf and restarted apache, all the bot attack lines in my access log turned from a 200 status to a 302, and browser visits would become an endless loop that’d eventually end in Chrome. What am I doing wrong? Much appreciated, Gus.

    • http://joedog Jeff Fulmer

      If you put them in httpd.conf, that should work. I have mine in a VirtualHost. You should also make sure you have mod_setenvif loaded:

      LoadModule setenvif_module modules/mod_setenvif.so

      If the config didn’t barf at startup, then you probably do.

      What’s curious is that it turned from 200 to 302. There’s nothing in the config I posted here that would force a redirect. Without seeing more, that’s not much I can recommend.

  • Gus

    Thanks for the update.

    I ended up adding the code to the VirtualHost as you suggested. I no longer see any bot requests, so unless the attack has stopped, the trick works. I received 150k requests in two days, the password protection of the wp-admin directory made things a little less messy on the VPS. Still, system load averages remained above 3 for the day. Right now, it’s at 0.50 or so.

    For the record, I am using this extension for Firefox to modify the headers: https://addons.mozilla.org/en-us/firefox/addon/header-tool/

    I’ll test it some more tomorrow and will report back :-)

  • Gus

    So here’s an update. Bot traffic dropped tremendously today and 99% of the requests are legit. It seems that the extra header method works, either by directly blocking access or by sending the request into a loop (that 302 error results in an endless loop that is ended by the browser). Thanks again for providing a great solution to those pests!