Fork me on Github
Fork me on Github

Joe Dog Software

Proudly serving the Internets since 1999

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 "" "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, 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, 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….