homepage Welcome to WebmasterWorld Guest from 54.197.215.146
register, free tools, login, search, pro membership, help, library, announcements, recent posts, open posts,
Pubcon Platinum Sponsor 2014
Home / Forums Index / Code, Content, and Presentation / Apache Web Server
Forum Library, Charter, Moderators: Ocean10000 & incrediBILL & phranque

Apache Web Server Forum

    
Whitelisting IP to Combat Brute Force Login Attempts
jk3210




msg:4595137
 12:27 am on Jul 22, 2013 (gmt 0)

I've been getting hammered on all my Wordpress sites by brute force login attempts, and to combat this I'm whitelisting my IP to the Wordpress login page using this...

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_URI} ^/wp-login\.php(.*)$ [OR]
RewriteCond %{REQUEST_URI} ^/wp-admin$
RewriteCond %{REMOTE_ADDR} !^99\.999\.999\.999$
RewriteRule ^(.*)$ - [R=403,L]
</IfModule>

However, when I run the login pages through a header checker I get a series of five 302 re-directs and then it stops/timesout. The code above SHOULD return a "403 - Forbidden," since the header checker's IP is not the one listed above, correct?

 

JD_Toims




msg:4595138
 12:33 am on Jul 22, 2013 (gmt 0)

R=403 says "redirect and issue a 403 status code". It's not a 403 Forbidden.

There are a couple other efficiency changes I made to your code, basically removing the (.*) patterns since we don't need to "match and store everything for back-reference".

Removing the end anchor on the first condition implicitly says "and anything else" but doesn't store anything for back-reference, so there's no reason I can think of to not just leave the end anchor off rather than using a "match and store" pattern or even a "match everything" pattern.

Using .? in the rule says if there are 0 or 1 characters present in a request to run the check through any conditions present. (Basically, it says "check any request" much like .* but in a more efficient way.)

The default behavior of the F (Forbidden) flag is "last", so the L flag in this case is unnecessary.

RewriteEngine on
RewriteCond %{REQUEST_URI} ^/wp-login\.php [OR]
RewriteCond %{REQUEST_URI} ^/wp-admin$
RewriteCond %{REMOTE_ADDR} !^99\.999\.999\.999$
RewriteRule .? - [F]

jk3210




msg:4595140
 1:02 am on Jul 22, 2013 (gmt 0)

Thanks very much JD. I learned something tonight!

JD_Toims




msg:4595145
 1:06 am on Jul 22, 2013 (gmt 0)

NP, glad I could help!

Mod_Rewrite can definitely be a confusing subject that's tough to find good info on, so in some ways I'm glad I've been getting headaches from it for nearly a decade and can provide some answers to other people on the subject... Of course in other ways the headaches it can give anyone, including me, Suck! lol

lucy24




msg:4595184
 3:26 am on Jul 22, 2013 (gmt 0)

R=403 says "redirect and issue a 403 status code". It's not a 403 Forbidden.

Actually it is. A weird quirk of the [R] flag is that you can attach absolutely any number, and the server will return that response. You can do the same thing in mod_alias with
Redirect some-number-here rest-of-rule
It's how you return a 410, for example, if you're not using mod_rewrite.

RewriteCond %{REQUEST_URI} ^/wp-login\.php(.*)$ [OR]
RewriteCond %{REQUEST_URI} ^/wp-admin$
<snip>
RewriteRule ^(.*)$ - [F]

The combination of
RewriteCond %{REQUEST_URI} blahblah (without leading ! for "is not")
with
RewriteRule .* (no request specified)

is almost never necessary. Anything that can go in the body of the rule should go there. In this case all you need is
RewriteRule ^wp-(login\.php|admin) - [F]
with only one condition: your own IP.

If the request is for something other than wp-login.php or wp-admin, the condition does not even need to be evaluated.

And now go rename your admin and login files ;) Why take chances?

JD_Toims




msg:4595191
 3:41 am on Jul 22, 2013 (gmt 0)

Actually it is.

Interesting, thanks!

Now I'm wondering if there's a different behavior on different boxes, because I just tested serving a 403 via R flag and it's working for me, so you're correct. It should have worked... Could this be one of those "mod_rewrite + box" idiosyncrasies?

NOTE: I've run into "oddities" with mod_rewrite before. In fact, I've posted rules/conditions here under a former username and I was told by the previous moderator, who knew mod_rewrite as well as anyone, they should work, even though they caused a 500 Server Error on the specific box I was on for some reason so I had to edit them to get them to work in my specific situation. [head scratch & go figure here] lol

JD_Toims




msg:4595210
 4:46 am on Jul 22, 2013 (gmt 0)

Okay, huge thanks to Lucy24 for "piquing my interest" in the posted ruleset which drove me to test it, because I wanted to know if there's a "mod_rewrite + box" idiosyncracy present here.

I tested the following ruleset with my IP Address correct in the condition:

RewriteCond %{REQUEST_URI} ^/test\.php$
RewriteCond %{REMOTE_ADDR} !^99\.99\.999\.9$
RewriteRule ^(.*)$ - [R=403,L]


> Result: I got in

Then I tested the following ruleset with my IP Address one number off:

RewriteCond %{REQUEST_URI} ^/test\.php$
RewriteCond %{REMOTE_ADDR} !^99\.99\.999\.8$
RewriteRule ^(.*)$ - [R=403,L]


> Result: Forbidden

Bottom Line: Either I'm totally missing something (please tell me what) OR even though the posted ruleset is not "the most efficient" construction of the rule/conditions, the initial ruleset should have worked as posted!

phranque




msg:4595216
 5:30 am on Jul 22, 2013 (gmt 0)

http://httpd.apache.org/docs/current/rewrite/flags.html#flag_r
Any valid HTTP response status code may be specified, using the syntax [R=305], with a 302 status code being used by default if none is specified. The status code specified need not necessarily be a redirect (3xx) status code. However, if a status code is outside the redirect range (300-399) then the substitution string is dropped entirely, and rewriting is stopped as if the L were used.

phranque




msg:4595217
 5:39 am on Jul 22, 2013 (gmt 0)

http://httpd.apache.org/docs/current/rewrite/flags.html#flag_f
Using the [F] flag causes the server to return a 403 Forbidden status code to the client.
...
When using [F], an [L] is implied - that is, the response is returned immediately, and no further rules are evaluated.


so this:
RewriteRule ^(.*)$ - [R=403,L]
is essentially equivalent to this:
RewriteRule .* - [R=403]
which is the same as this:
RewriteRule .* - [F]
JD_Toims




msg:4595222
 6:08 am on Jul 22, 2013 (gmt 0)

so this:
RewriteRule ^(.*)$ - [R=403,L]
is essentially equivalent to this:
RewriteRule .* - [R=403]
which is the same as this:
RewriteRule .* - [F]

Exactly, and no argument, because everything you're saying and the docs say means the initial ruleset should be valid and...

RewriteRule .* - [F]
is essentially the same as
RewriteRule .? - [F]

So, the initially posted ruleset should have worked, unless there's something I'm missing (please, someone, point it out if there is).

If there's not something I'm completely missing AND the initial ruleset did not work, then there must be a "box difference" in the way certain expressions/directives/flags are handled, which I have run into previously, as uncommon as it may be.

phranque




msg:4595223
 6:17 am on Jul 22, 2013 (gmt 0)

All of which means the initially posted ruleset should have worked


actually it means we need more information about how it failed.

However, when I run the login pages through a header checker I get a series of five 302 re-directs and then it stops/timesout.

what did the intermediate redirected url(s) look like?
were there any clues in the server access and error logs?

JD_Toims




msg:4595224
 6:25 am on Jul 22, 2013 (gmt 0)

actually it means we need more information about how it failed.

Good point... Let me add, And Why?

As far as I can tell NOW it shouldn't have... (Initially, the only thing I could see that could be "wrong" was the R=403. Yes, there were some efficiency issues and Lucy24 even pointed out one I missed, but beyond that, the R=403 is the only place I saw it could be failing.)

Like I said previously though, there have been cases with mod_rewrite and a specific box where rulesets fail and they shouldn't according to the docs and even the experts who have looked at the rules/conditions, so "something different" between boxes and configurations must make a difference.

ADDED: Still, if someone can point out why the ruleset initially posted should not have worked I'd really like to see what I'm missing, thanks! (To me Mod_Rewrite always seems to be an ongoing study, hence the headaches I still occasionally get from it! lol)

lucy24




msg:4595236
 7:32 am on Jul 22, 2013 (gmt 0)

I tested the following ruleset with my IP Address correct in the condition:

RewriteCond %{REQUEST_URI} ^/test\.php$
RewriteCond %{REMOTE_ADDR} !^99\.99\.999\.9$
RewriteRule ^(.*)$ - [R=403,L]

> Result: I got in

Then I tested the following ruleset with my IP Address one number off:

RewriteCond %{REQUEST_URI} ^/test\.php$
RewriteCond %{REMOTE_ADDR} !^99\.99\.999\.8$
RewriteRule ^(.*)$ - [R=403,L]

> Result: Forbidden


You mean the results were the exact opposite of what you'd expect? In both directions? That really is weird. Now, you're sure your ISP hasn't changed your IP behind your back? :)

Are you in a position to run a RewriteLog? One obvious question, of course, is whether the 403 came from the rule begin tested or some entirely different source.

I'm told by an unimpeachable source that browsers remember redirect responses. Do they also remember [F] responses? Did both requests reach the server?

What happens if you change only the target of the rule-- for example, instead of a 403, let it redirect to "foo.html" or some other recognizable name? Then there's no question about whether the rule has executed.

JD_Toims




msg:4595241
 7:50 am on Jul 22, 2013 (gmt 0)

That really is weird. Now, you're sure your ISP hasn't changed your IP behind your back? :)

I checked my IP with dnsstuff.com before testing and used the one they showed, so... If there was anything wrong with the initial ruleset I should have received a 403 error on the initial test as I did on the second.

Are you in a position to run a RewriteLog? One obvious question, of course, is whether the 403 came from the rule begin tested or some entirely different source.

No, I don't log rewrites, but at the same time, no the 403 did not come from a different source, because the test.php page is the page I've been testing code on before I post all day long... No cache issues, no other intervening rewrites or redirects. I've changed the page + dumped my cache and reloaded the page 20+ times today without issue... In the .htaccess file of that specific site and that specific page the rules/conditions I posted here were the exact rules/conditions I tested and I dumped my cache between each test.

Do they also remember [F] responses? Did both requests reach the server?

Yes, some remember 403s, but I dumped my cache between each test, so there was sure to be no browser/caching issue.

What happens if you change only the target of the rule-- for example, instead of a 403, let it redirect to "foo.html" or some other recognizable name? Then there's no question about whether the rule has executed.

I didn't test that, but will if it's necessary... With an empty cache and the page being served, then an empty cache and receiving a 403 requesting the same page after altering the IP Address in the condition of the posted ruleset test I was convinced the initially posted ruleset should have worked.

jk3210




msg:4595323
 12:59 pm on Jul 22, 2013 (gmt 0)

This is what I get using both my original code AND JD's original code...

GET /wp-login.php HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
Referer: http://www.example.com/httpview.html
Connection: close
• Finding host IP address...
• Host IP address = 999.999.999.999
• Finding TCP protocol...
• Binding to local socket...
• Connecting to host...
• Sending request...
• Waiting for response...



Receiving Header:

HTTP/1.1·302·Found(CR)(LF)
Date:·Mon,·22·Jul·2013·12:45:00·GMT(CR)(LF)
Server:·Apache(CR)(LF)
X-Pingback:·http://www.example.com/xmlrpc.php(CR)(LF)
Expires:·Wed,·11·Jan·1984·05:00:00·GMT(CR)(LF)
Cache-Control:·no-cache,·must-revalidate,·max-age=0(CR)(LF)
Pragma:·no-cache(CR)(LF)
Set-Cookie:·PHPSESSID=26e0bec4f0408ec789d757321488105e;·path=/;·HttpOnly(CR)(LF)
Location:·http://www.example.com/wp-login.php(CR)(LF)
Content-Length:·0(CR)(LF)
Connection:·close(CR)(LF)
Content-Type:·text/html;·charset=UTF-8(CR)(LF)
(CR)(LF)
End of Header (Length = 440)

• Elapsed time so far: 1 seconds
• Waiting for additional response until connection closes...
Total bytes received = 440

Elapsed time so far: 1 seconds
Content (Length = 0):

Done

Elapsed time so far: 1 seconds

The Location: line in the header above would redirect your browser to a new URL:

Location 2

Parameters:

URL = http://www.example.com/wp-login.php
UAG = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
REF = http://www.example.com/httpview.html
AEN =
REQ = GET ; VER = 1.1 ; FMT = TXT
Link: http://www.example.com/cgi-bin/httpview.cgi?url=http:/ ... =&req=GET&ver=1.1&fmt=TXT

Sending request:

GET /wp-login.php HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
Referer: http://www.example.com/httpview.html
Connection: close
• Finding host IP address...
• Host IP address = 999.999.999.999
• Finding TCP protocol...
• Binding to local socket...
• Connecting to host...
• Sending request...
• Waiting for response...
Receiving Header:

HTTP/1.1·302·Found(CR)(LF)
Date:·Mon,·22·Jul·2013·12:55:04·GMT(CR)(LF)
Server:·Apache(CR)(LF)
X-Pingback:·http://www.example.com/xmlrpc.php(CR)(LF)
Expires:·Wed,·11·Jan·1984·05:00:00·GMT(CR)(LF)
Cache-Control:·no-cache,·must-revalidate,·max-age=0(CR)(LF)
Pragma:·no-cache(CR)(LF)
Set-Cookie:·PHPSESSID=5b6fc1202b6a582c512e10acffb21408;·path=/;·HttpOnly(CR)(LF)
Location:·http://www.example.com/wp-login.php(CR)(LF)
Content-Length:·0(CR)(LF)
Connection:·close(CR)(LF)
Content-Type:·text/html;·charset=UTF-8(CR)(LF)
(CR)(LF)
End of Header (Length = 440)

• Elapsed time so far: 2 seconds
• Waiting for additional response until connection closes...
Total bytes received = 440

Elapsed time so far: 2 seconds
Content (Length = 0):

Done

Elapsed time so far: 2 seconds


Location 3

(Same as above)


Location 4

(Same as above)


Location 5

Done

Total elapsed time: 6 seconds

lucy24




msg:4595398
 4:33 pm on Jul 22, 2013 (gmt 0)

###.

It sure looks as if your server hasn't read the Apache documentation, doesn't it ;)

And yes, we'll stipulate the CRLF at each line end.

jk3210




msg:4595407
 4:48 pm on Jul 22, 2013 (gmt 0)

I really don't know what to make of it, since it's one of the largest hosting companies in the world.

JD_Toims




msg:4595455
 7:25 pm on Jul 22, 2013 (gmt 0)

It sure looks as if your server hasn't read the Apache documentation, doesn't it ;)

LOL +1

I'm assuming without either set of code you can login and everything is working like it should? If that's the case, then just as a "dart throw" I'd try this:

RewriteEngine on
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /wp-(login|admin)
RewriteCond %{REMOTE_ADDR} !^99\.999\.999\.999$
RewriteRule ^wp-(login|admin) - [F]

jk3210




msg:4595509
 9:33 pm on Jul 22, 2013 (gmt 0)

Using my code or yours, either way, everyone but me who tries to login gets sent on a wild goose chase of re-directs, so it fulfills the initial purpose.

JD_Toims




msg:4595512
 9:41 pm on Jul 22, 2013 (gmt 0)

Okay, cool!

Something I just thought of when re-reading this is: If you have custom error pages it might be the 403 error page that's causing the issue somehow and not the ruleset(s). If you decide to try and fix it someday, the error doc locations and their interaction with the .htaccess file redirects is where I'd start looking.

MickeyRoush




msg:4600859
 10:05 am on Aug 10, 2013 (gmt 0)

I would do it just like this:

RewriteEngine on
RewriteCond %{REMOTE_ADDR} !^99\.999\.999\.999$
RewriteRule ^wp-login\.php - [F]

Then use a separate .htaccess file for your wp-admin directory using HTTP Authentication, because depending on any plugins/themes you may use, some options may need to call admin-ajax.php and a few other PHP files from HTTP for regular viewers of your site. So blocking the whole wp-admin directory in it's entirety is usually not good unless you whitelist static and a few PHP files as they may need to be called from HTTP for non-admins.

That code above is basically all you need to prevent brute force attacks on your login unless you're doing some redirects on wp-login.php.

Dideved




msg:4601637
 10:04 pm on Aug 13, 2013 (gmt 0)

Whew... this thread got really long-winded.

JD_Toims was right -- however many posts ago it was -- that the OP's code, as posted, works correctly. Which means...

@jk3210 The real problem is almost certainly coming from elsewhere in your htaccess. If you post your full htaccess file, I'm sure someone here will be able to spot the problem in a jiffy.

jk3210




msg:4601671
 12:18 am on Aug 14, 2013 (gmt 0)

Here it is...

# compress text, html, javascript, css, xml:
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript

# Or, compress certain file types by extension:
<files *.html>
SetOutputFilter DEFLATE
</files>


## EXPIRES CACHING ##
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access 1 month"
ExpiresByType image/jpeg "access 1 month"
ExpiresByType image/gif "access 1 month"
ExpiresByType image/png "access 1 month"
ExpiresByType text/css "access 1 day"
ExpiresByType application/pdf "access 1 month"
ExpiresByType text/x-javascript "access 1 day"
ExpiresByType application/x-shockwave-flash "access 1 month"
ExpiresByType image/x-icon "access 1 month"
ExpiresDefault "access 1 month"
</IfModule>
## EXPIRES CACHING ##

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_URI} ^/wp-login\.php [OR]
RewriteCond %{REQUEST_URI} ^/wp-admin$
RewriteCond %{REMOTE_ADDR} !^99\.999\.999\.999$
RewriteRule .? - [F]
</IfModule>

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

<Files 403.shtml>
order allow,deny
allow from all
</Files>

deny from 93.999.999.999

lucy24




msg:4601694
 2:31 am on Aug 14, 2013 (gmt 0)

<IfModule mod_rewrite.c>
RewriteEngine on

Sounds like the htaccess needs some housekeeping. An IfModule envelope is never needed in a specific, individual htaccess: either you've got the mod or you haven't. (And if you don't have access to mod_rewrite, what on earth are you doing with a WordPress installation!)

Two separate IfModule envelopes, especially involving mod_rewrite, tend to imply that pieces of htaccess have been imported from different places. So along with dumping the envelope --not its contents!-- you need to ensure that all RewriteRules are in the correct order. Here you're OK because you've got a rule ending in [F] followed by one ending in [L], which is the order you'd use anyway. But there should NEVER be two separate occurrences of RewriteEngine on (lower case in the docs). Unless-- maybe-- there's an intervening RewriteEngine off that you added for debugging. (The horse's mouth itself says the only reason you'd ever use RewriteEngine off is as an alternative to commenting-out a RewriteRule.)

Is wp-login.php a real, physical file? I sure hope so, or you've been rewriting yourself to the WP index page :) Requests for /wp-admin (and that's all) are almost bound to be rewritten, unless you've really got a file (not directory) named "wp-admin" without extension.

Oops! One more thing. The <Files 403 et cetera envelope is absolutely right. But if you're issuing 403s through anything other than mod_authz-whatever, you'll need something equivalent for each mod. For example

RewriteRule ^403\.shtml$ - [L]

so you don't get a cascading 403 error. This rule would go before all other RewriteRules, regardless of flag.

jk3210




msg:4601725
 3:51 am on Aug 14, 2013 (gmt 0)

Thanks, I'll do the clean-up. And yes, wp-login.php is a real, physical file.

You know, it's the strangest thing...I have this exact same htaccess on another site where WP is installed in a "/news" sub-directory, so the code is...

RewriteCond %{REQUEST_URI} ^/news/wp-login\.php [OR]
RewriteCond %{REQUEST_URI} ^/news/wp-admin$

...and it works perfectly and issues the correct 403 response.

But with the original code in a straight-up WP root install, I get the chain of 302s. It definitely serves the purpose of stopping the (18,000+) bogus login attempts, but it sure would be nice to know what's causing the difference is between the 302s verses the correct 403.

lucy24




msg:4601769
 8:20 am on Aug 14, 2013 (gmt 0)

Urk.

Are you sure there's no server-site glitch? WP on shared hosting involves some button-pushing at their end-- and someone may simply have pushed the wrong button.

Scrolling back through this thread it sure as ### looks as if each request is getting redirected right back to the same page -- and your LiveHeaders-or-equivalent shows the full hostname, so you know it isn't a case of www.example.com redirecting to example.com and back again.

If you try it in some completely different browser, where it can't possibly be cached, how many requests make it through to logs? Browsers pull the plug at 10, 20 or 30, but you'll rarely see that many separate log entries unless it's something really creative.

jk3210




msg:4601826
 1:51 pm on Aug 14, 2013 (gmt 0)

Just tried it in another browser and I get the exact same thing... five 302s, then it terminates.

This huge host has been going through numerous server upgrades and moving sites around a LOT recently, and they have had problems with the redirects they put in place to the extent that some sites even redirected to two year old versions of the same site. So, maybe that's what's causing the 302s now. At any rate, I'll give it some time and see how it goes. Thanks.

Global Options:
 top home search open messages active posts  
 

Home / Forums Index / Code, Content, and Presentation / Apache Web Server
rss feed

All trademarks and copyrights held by respective owners. Member comments are owned by the poster.
Home ¦ Free Tools ¦ Terms of Service ¦ Privacy Policy ¦ Report Problem ¦ About ¦ Library ¦ Newsletter
WebmasterWorld is a Developer Shed Community owned by Jim Boykin.
© Webmaster World 1996-2014 all rights reserved