Anti bruteforce

428 views
Skip to first unread message

Stefano Gargiulo

unread,
Jan 28, 2011, 6:21:51 AM1/28/11
to simple...@googlegroups.com
Does anyone implemented a module to avoid bruteforce login attemps into SS IdP?

Peter Schober

unread,
Jan 28, 2011, 6:42:32 AM1/28/11
to simple...@googlegroups.com
* Stefano Gargiulo <rast...@gmail.com> [2011-01-28 12:22]:

> Does anyone implemented a module to avoid bruteforce login attemps
> into SS IdP?

Since a SAML IdP usually is not the only thing that talks to your
actual authentication service (e.g. LDAP DSAs) it's probably best to
implement such a thing there. Otherwise you'd still be vulnerable to
attempts coming in via IMAP, SMTPA, HTTPS, etc (or you'd be requried
to implement these limits in each of those applications).

For the case of SimpleSAMLphp being the authentication source itself
(by way of the selfregister module) there is currently no code to
limit authentication requests. Not sure it's worth the effort though,
for essentially no-trust self-service userids ;)
-peter

Stefano Gargiulo

unread,
Jan 28, 2011, 8:26:57 AM1/28/11
to simple...@googlegroups.com
Since a SAML IdP usually is not the only thing that talks to your
actual authentication service (e.g. LDAP DSAs) it's probably best to
implement such a thing there

Sorry but i don't think this is the best way to implement antibruteforce on an IdP, consider a fact: how do you prevent bruteforce?
Usally you count wrong login attemps by IP and Username in a time-buffer, and you block attempts matching this these parameters when the count goes over a  threshold. 

Now implementing this in LDAP when you have a webapplication with the same IP that try to bind  for the user (the IdP talks with LDAP not the end user) you lose the IP parameter, so you will get an antibruteforce system that works by username only:  you can call this "EasyDOS", because  this is an awesome way to give hackers the possibility to do DenialOfService for all known usernames.

 Otherwise you'd still be vulnerable to
attempts coming in via IMAP, SMTPA, HTTPS, etc (or you'd be requried
to implement these limits in each of those applications).

Good point but we should prefer to do more work, for the motivation exposed above, anyway in my case LDAP and IMAP access is closed to the world and limited to our DMZ, but i've implemented an extension of the selfregister module, to consent the administrator approval of new self registered accounts for SAML webapps, these accounts are stored in a different LDAP subtree than the employee one and this user subtree is not open to IMAP etc.. but just to this simplesamlphp IdP, so these account are semi-trusted: our selfregister is a "self-request-an-account" with administrator approval.  

Best regards,
Stefano.








2011/1/28 Peter Schober <peter....@univie.ac.at>

--
You received this message because you are subscribed to the Google Groups "simpleSAMLphp" group.
To post to this group, send email to simple...@googlegroups.com.
To unsubscribe from this group, send email to simplesamlph...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/simplesamlphp?hl=en.


Richard Gomes

unread,
Jan 28, 2011, 8:25:02 AM1/28/11
to simple...@googlegroups.com
Hi guys,

I hope my answer is in context, in spite the solution does not involve
programming.

Once a failure will possibly redirect to an specific page which tells
"login failure" or something like this, you can easily detect these
situations and block the offending IP for a certain amount of time.

I've written a couple of articles which show how you can protect Apache
and Tomcat against unpleasant, unwanted, undesired spambots using
"fail2ban" on Debian boxes.

* http://www.jquantlib.org/index.php/LazyAdminApacheFail2Ban

*
http://www.jquantlib.org/index.php/Protecting_Tomcat_with_Fail2Ban_on_Debian

I hope it helps.

Cheers :)

--
Richard Gomes
http://www.jquantlib.org/index.php/User:RichardGomes
twitter: frgomes

JQuantLib is a library for Quantitative Finance written in Java.
http://www.jquantlib.org/
twitter: jquantlib

Stefano Gargiulo

unread,
Jan 28, 2011, 8:49:21 AM1/28/11
to simple...@googlegroups.com
Very interesting thank you!

This is a simple but strong solution to implement it with simplesamlphp logs!

Best regards,
Stefano.

2011/1/28 Richard Gomes <rgome...@googlemail.com>

Stefano Gargiulo

unread,
Jan 28, 2011, 9:02:43 AM1/28/11
to simple...@googlegroups.com
Anyway i hope that a simple solution like the following will be included into next simplesamlphp release.


Having this out of the box is a banal thing but i think very useful: not all IdP administrators are sensible to this kind of problems, so maybe someone of them will not take 5 minutes of his life to implement things like fail2ban.

But they probably don't consider a fact: when you manage an IdP, you're responsible not only for your users, but also for all the federated SP that trust in you! 

A bad IdP managment is an hole in any federated application: every IdP is a vulnerable point that must be keept always supersecure.

This little thing in SimpleSAMLphp can give a little contribute to the security of federations.

Best regards,
Stefano

2011/1/28 Stefano Gargiulo <rast...@gmail.com>

Stefano Gargiulo

unread,
Jan 28, 2011, 10:40:15 AM1/28/11
to simple...@googlegroups.com
this is my implementation, thanks to the robustness and flexibility of the simplesamlphp framework was very easy to inculde it, make it configurable, and show graceful errors to users (also an alert when only 1 attempt is left)

here the plain diff text for who wanna implement it

Index: GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php
===================================================================
--- GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php (revision 83)
+++ GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php (revision 87)
@@ -22,6 +22,12 @@
  * SimpleSAMLphp will attempt to create this directory if it doesn't exist.
  */
  'tempdir'               => '/tmp/simplesamlphp',
+
+        /** anti bruteforce for password cracking prevention **/
+        "security.antibruteforce.timewindow.minuites" => 10,
+        "security.antibruteforce.timewindow.max_attempts" => 5,
+        "security.antibruteforce.logfile" => "/tmp-www/simplesaml.antibruteforce.data",
+        "security.antibruteforce.username_check" => false,
 
 
  /**
Index: GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/Auth/UserPassBase.php
===================================================================
--- GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/Auth/UserPassBase.php (revision 83)
+++ GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/Auth/UserPassBase.php (revision 87)
@@ -167,20 +167,46 @@
  }
 
  /*
+                 * antibruteforce
+                 */
+                if (SimpleSAML_Configuration::getConfig()->getBoolean("security.antibruteforce.username_check")){
+                    $ucheck=$username;
+
+                }
+                $deny_login = sspmod_core_AntiBruteForce::bruteCheck($ucheck);
+               
+                if ($deny_login[0]){
+                    return  "TOOMANYATTEMPTS";
+                }
+                
+
+                /*
  * $source now contains the authentication source on which authenticate()
  * was called. We should call login() on the same authentication source.
  */
-
  try {
  /* Attempt to log in. */
  $attributes = $source->login($username, $password);
  } catch (SimpleSAML_Error_Error $e) {
- /*
+
+                        /*
+                         *
  * Login failed. Return the error code to the login form, so that it
  * can display an error message to the user.
  */
+
+
+                        /*
+                         * antibruteforce
+                         */
+                        sspmod_core_AntiBruteForce::bruteCheck($ucheck,true);
+                        if ($deny_login[1]==SimpleSAML_Configuration::getConfig()->getInteger("security.antibruteforce.timewindow.max_attempts") -1){
+                            return  "ONEATTEMPTLEFT";
+                        }
+                        
  return $e->getErrorCode();
  }
+               
 
  /* Save the attributes we received from the login-function in the $state-array. */
  assert('is_array($attributes)');
Index: GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/AntiBruteForce.php
===================================================================
--- GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/AntiBruteForce.php (revision 0)
+++ GarrSSO/usr/local/simplesamlphp-1.7.0/modules/core/lib/AntiBruteForce.php (revision 87)
@@ -0,0 +1,76 @@
+<?php
+
+class sspmod_core_AntiBruteForce {
+
+
+    /**
+     *
+     * @param <type> $username username to log
+     * @param <type> $failed_attempt set to true to log a failed attempt
+     * @return <type> an array: 1st parameter is the boolean var deny_login, second is the number of failed attempts
+     */
+    public static function bruteCheck($username="",$failed_attempt = false) {
+        define('MM_BRUTE_FILE', SimpleSAML_Configuration::getConfig()->getString("security.antibruteforce.logfile"));
+        define('MM_BRUTE_WINDOW', SimpleSAML_Configuration::getConfig()->getInteger("security.antibruteforce.timewindow.minuites") * 60);
+        define('MM_BRUTE_ATTEMPTS', SimpleSAML_Configuration::getConfig()->getInteger("security.antibruteforce.timewindow.max_attempts") );
+
+        $deny_login = false;
+
+        if (!file_exists(MM_BRUTE_FILE))
+            touch(MM_BRUTE_FILE);
+        $cache = unserialize(self::fileToString(MM_BRUTE_FILE));
+        if (!$cache)
+            $cache = array();
+
+        if ($failed_attempt) {  //register the new failed attempt and timestamp
+            if (!isset($cache[$username.$_SERVER['REMOTE_ADDR']])) {
+                $cache[$username.$_SERVER['REMOTE_ADDR']] = array();
+            }
+            $cache[$username.$_SERVER['REMOTE_ADDR']][] = time();
+            if (count($cache[$username.$_SERVER['REMOTE_ADDR']]) > MM_BRUTE_ATTEMPTS)
+                array_shift($cache[$username.$_SERVER['REMOTE_ADDR']]);
+        }
+
+//get the number of failed attempts in the last 15 minutes
+        if (!isset($cache[$username.$_SERVER['REMOTE_ADDR']])) {
+            $deny_login = false;
+        } else {
+            $attempts = $cache[$username.$_SERVER['REMOTE_ADDR']];
+            if (count($attempts) < MM_BRUTE_ATTEMPTS) {
+                $deny_login = false;
+            } else {
+                if ($attempts[0] + MM_BRUTE_WINDOW > time())
+                    $deny_login = true;
+                else
+                    $deny_login = false;
+            }
+        }
+
+//cleanup the cache so it doesn't get too large over time
+        foreach ($cache as $ip => $attempts) {
+            if ($attempts) {
+                if ($attempts[count($attempts) - 1] + MM_BRUTE_WINDOW < time()) {
+                    unset($cache[$ip]);
+                }
+            }
+        }
+
+        self::stringToFile(MM_BRUTE_FILE, serialize($cache));
+
+        return array($deny_login,count($attempts));
+    }
+
+    public static function fileToString($filename) {
+        return file_get_contents($filename);
+    }
+
+    public static function stringToFile($filename, $data) {
+        $file = fopen($filename, "w");
+        fwrite($file, $data);
+        fclose($file);
+        return true;
+    }
+
+}
+
+?>
\ No newline at end of file
Index: GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.translation.json
===================================================================
--- GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.translation.json (revision 83)
+++ GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.translation.json (revision 87)
@@ -250,6 +250,21 @@
  "lt": "\u0160i klaida tikriausiai susijusi d\u0117l simpleSAMLphp neteisingo sukonfig\u016bravimo. Susisiekite su \u0161ios sistemos administratoriumi ir nusi\u0173skite \u017eemiau rodom\u0105 klaidos prane\u0161im\u0105.",
  "it": "Questo errore \u00e8 probabilmente dovuto a qualche comportamento inatteso di simpleSAMLphp o ad un errore di configurazione. Contatta l'amministratore di questo servizio di login con una copia del messaggio di errore riportato qui sopra."
  },
+         "title_TOOMANYATTEMPTS": {
+ "it": "Troppi tentativi di login falliti"
+ },
+        "descr_TOOMANYATTEMPTS": {
+ "it": "Sei stato temporaneamente bannato"
+ },
+
+        "title_ONEATTEMPTLEFT": {
+ "it": "Un solo tentativo rimasto prima del ban temporaneo"
+ },
+        "descr_ONEATTEMPTLEFT": {
+ "it": "Spiacenti, ma dobbiamo prevenire attacchi di tipo bruteforce sulla tua password"
+ },
+
+
  "title_CREATEREQUEST": {
  "no": "Feil i laging av foresp\u00f8rselen",
  "nn": "Feil under oppretting av SAML-sp\u00f8rsm\u00e5l",
Index: GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.definition.json
===================================================================
--- GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.definition.json (revision 83)
+++ GarrSSO/usr/local/simplesamlphp-1.7.0/dictionaries/errors.definition.json (revision 87)
@@ -167,9 +167,27 @@
  "title_WRONGUSERPASS": {
  "en": "Incorrect username or password"
  },
+
  "descr_WRONGUSERPASS": {
  "en": "Either no user with the given username could be found, or the password you gave was wrong. Please check the username and try again."
  },
+
+        "title_TOOMANYATTEMPTS": {
+ "en": "Too many wrong login attempts"
+ },
+        "descr_TOOMANYATTEMPTS": {
+ "en": "You were temoporary banned"
+ },
+        
+        "title_ONEATTEMPTLEFT": {
+ "en": "One attempt left before temporary ban"
+ },
+        "descr_ONEATTEMPTLEFT": {
+ "en": "Sorry but we need to prevent brute force cracking of your password"
+ },
+
+
+
  "title_RESPONSESTATUSNOSUCCESS": {
  "en": "Error received from Identity Provider"
  },


please evaultate to include something like this in the release..

Stefano.

2011/1/28 Stefano Gargiulo <rast...@gmail.com>

Thijs Kinkhorst

unread,
Jan 31, 2011, 3:21:51 AM1/31/11
to simple...@googlegroups.com, Stefano Gargiulo
Hi Stefano,

On Friday 28 January 2011 16:40:15 Stefano Gargiulo wrote:
> this is my implementation, thanks to the robustness and flexibility of the
> simplesamlphp framework was very easy to inculde it, make it configurable,
> and show graceful errors to users (also an alert when only 1 attempt is
> left)

I'm not a simpleSAMLphp developer but I like the idea of this; Peter's
suggestion to implement it in the backend is a good idea to some extent but
it does not allow to limit attemts from a username,IP combination which I
think is the useful thing about your patch.

Still, here's some feedback on it:

> here the plain diff text for who wanna implement it
>
> Index: GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php
> ===================================================================
> --- GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php (revision 83)
> +++ GarrSSO/usr/local/simplesamlphp-1.7.0/config/config.php (revision 87)
> @@ -22,6 +22,12 @@
> * SimpleSAMLphp will attempt to create this directory if it doesn't
> exist. */
> 'tempdir' => '/tmp/simplesamlphp',
> +
> + /** anti bruteforce for password cracking prevention **/
> + "security.antibruteforce.timewindow.minuites" => 10,

Probably you mean "minutes".

> + "security.antibruteforce.timewindow.max_attempts" => 5,
> + "security.antibruteforce.logfile" =>
> "/tmp-www/simplesaml.antibruteforce.data",

Perhaps /tmp-www is a secured directory where only simpleSAMLphp can write.
But if the user changes that to /tmp you've created a tempfilerace. Probably
best to make this logfile location relative to simpleSAMLphp's loggingdir or
datadir setting.

> + "security.antibruteforce.username_check" => false,

The options could use some documentatoin. The ones earlier may speak a bit for
themselves but you really need to read the code to understand what this one
does.


> + $deny_login = false;
> +
> + if (!file_exists(MM_BRUTE_FILE))
> + touch(MM_BRUTE_FILE);
> + $cache = unserialize(self::fileToString(MM_BRUTE_FILE));
> + if (!$cache)
> + $cache = array();
> +
> + if ($failed_attempt) { //register the new failed attempt and
> timestamp
> + if (!isset($cache[$username.$_SERVER['REMOTE_ADDR']])) {

I'll readily admit that it's unlikely, but by just contatenating them you can
get the situation that attempts from 'user' on '12.34.56.78' are conflated
with attempts from 'user1' on '2.34.56.78'. Perhaps for correctness sake it
helps to glue them together with a character that doesn't appear in
IP-addresses, e.g.: $username.'_'.$_SERVER['REMOTE_ADDR'] (and create that
string once at the start of the function, and use it in a variable in all the
places throughout the code).

> +//get the number of failed attempts in the last 15 minutes
> + if (!isset($cache[$username.$_SERVER['REMOTE_ADDR']])) {
> + $deny_login = false;
> +
> + } else {
> + $attempts = $cache[$username.$_SERVER['REMOTE_ADDR']];
> + if (count($attempts) < MM_BRUTE_ATTEMPTS) {
> + $deny_login = false;
> + } else {
> + if ($attempts[0] + MM_BRUTE_WINDOW > time())
> + $deny_login = true;
> + else
> + $deny_login = false;
> + }
> + }

$deny_login is already set to false at the start of the function, so you may
skip all the conditions where you set it to false again here (and only drill
down to the part where it's true).

> + public static function fileToString($filename) {
> + return file_get_contents($filename);
> + }

This just calls file_get_contents, why can't that function be used directly?

> + public static function stringToFile($filename, $data) {
> + $file = fopen($filename, "w");
> + fwrite($file, $data);
> + fclose($file);
> + return true;
> + }

This seems to implement PHP builtin file_put_contents(), so why not use that
instead?

One last thing is that I'm wondering if the writeout of the cache could be
problematic on really busy installations, causing a race condition. But
perhaps its small enough not to be a problem in practice.

Cheers,
Thijs


--
Thijs Kinkhorst <th...@uvt.nl> – LIS Unix

Universiteit van Tilburg – Library and IT Services • Postbus 90153, 5000 LE
Bezoekadres > Warandelaan 2 • Tel. 013 466 3035 • G 236 • http://www.uvt.nl

signature.asc

Olav Morken

unread,
Jan 31, 2011, 7:17:50 AM1/31/11
to simple...@googlegroups.com, Stefano Gargiulo
On Mon, Jan 31, 2011 at 09:21:51 +0100, Thijs Kinkhorst wrote:
> This seems to implement PHP builtin file_put_contents(), so why not use that
> instead?
>
> One last thing is that I'm wondering if the writeout of the cache could be
> problematic on really busy installations, causing a race condition. But
> perhaps its small enough not to be a problem in practice.

If I understand the code correctly, it is only a single file for all
users & IPs, and that file is rewritten every time a username/password
combination fails. Combine that with a server under load (for example
because someone is trying lots of username/password combinations at the
same time), and you are almost guaranteed to hit this race condition.

What is worse is that the file write isn't atomic, so you can expect
some of the reads to read a truncated file, which in turn causes the
counters to be reset.

We have an function for atomically writing a file that can be used:

SimpleSAML_Utilities::writeFile($filename, $data);

It writes the data to a temporary file, and then renames that over the
existing file. Some data will still be lost, when we have multiple
simultaneous requests, but at least some data should survive.

An alternative is to implement this on top of memcache. Memcache has
an "increment" function, which allows you to atomically increase an
counter.

Regards,
Olav Morken
UNINETT / Feide

Olav Morken

unread,
Jan 31, 2011, 7:40:07 AM1/31/11
to simple...@googlegroups.com
On Fri, Jan 28, 2011 at 16:40:15 +0100, Stefano Gargiulo wrote:
> this is my implementation, thanks to the robustness and flexibility of the
> simplesamlphp framework was very easy to inculde it, make it configurable,
> and show graceful errors to users (also an alert when only 1 attempt is
> left)

Generally, I think that this could be better implemented as a wrapper
around an existing authentication source, e.g. configured like:

'mylogin' => array(
'antibruteforce:File',
"timewindow.minuites" => 10,
"timewindow.max_attempts" => 5,
"logfile" => "/tmp-www/simplesaml.antibruteforce.data",
"username_check" => false,

'auth' => array(
'ldap:LDAP',
[...],
),
),

That would require some small modifications to SimpleSAML_Auth_Source
to allow one authentication source to "wrap" a different one, but
should be doable - at least for all that are based on UserPassBase.

It would also make it easier to implement custom backends for
validation.


> + define('MM_BRUTE_FILE',
> SimpleSAML_Configuration::getConfig()->getString("security.antibruteforce.logfile"));
> + define('MM_BRUTE_WINDOW',
> SimpleSAML_Configuration::getConfig()->getInteger("security.antibruteforce.timewindow.minuites")
> * 60);
> + define('MM_BRUTE_ATTEMPTS',
> SimpleSAML_Configuration::getConfig()->getInteger("security.antibruteforce.timewindow.max_attempts")
> );

These define's could be replaced with normal variables?

> +
> + $deny_login = false;
> +
> + if (!file_exists(MM_BRUTE_FILE))
> + touch(MM_BRUTE_FILE);

This will only cause unserialization of an empty string - you might
as well just initialize $cache to an empty array.

> + $cache = unserialize(self::fileToString(MM_BRUTE_FILE));
> + if (!$cache)
> + $cache = array();

Stefano Gargiulo

unread,
Feb 2, 2011, 3:07:52 AM2/2/11
to simple...@googlegroups.com

Hi thank you for the replies. Yes the class i posted is a very ugly code, and not mine (i used a sample i found on the blog post i linked before on this thread, sorry but i was in an hurry i know that is to be reviewed to fit simplesamlphp framework, that was just to give inspiration). Anyway in my installation i will revwrite it maybe using an something like SQLlite database  to prevent concurrency.

Ps. The idea of authsource wrapper sounds very good to me, maybe also useful to implement other interesting things (is the complement to the authprocfilter feature: a filtrer that can be executed before authentication) so, my 50 cents.

Best regards,
Stefano.

Il giorno 31/gen/2011 13:40, "Olav Morken" <olav....@uninett.no> ha scritto:

On Fri, Jan 28, 2011 at 16:40:15 +0100, Stefano Gargiulo wrote:

> this is my implementation, thanks ...

Generally, I think that this could be better implemented as a wrapper
around an existing authentication source, e.g. configured like:

   'mylogin' => array(
       'antibruteforce:File',
       "timewindow.minuites" => 10,
       "timewindow.max_attempts" => 5,

"logfile" => "/tmp-www/simplesaml.antibruteforce.data",

       "username_check" => false,

       'auth' => array(
           'ldap:LDAP',
            [...],
       ),
   ),

That would require some small modifications to SimpleSAML_Auth_Source
to allow one authentication source to "wrap" a different one, but
should be doable - at least for all that are based on UserPassBase.

It would also make it easier to implement custom backends for
validation.



> + define('MM_BRUTE_FILE',
> SimpleSAML_Configuration::getConfig()->getString("security.an...

These define's could be replaced with normal variables?


> +
> + $deny_login = false;
> +
> + if (!file_exists(MM_BRUTE_FILE))

> + ...

This will only cause unserialization of an empty string - you might
as well just initialize $cache to an empty array.


> + $cache = unserialize(self::fileToString(MM_BRUTE_FILE));
> + if (!$cache)

> + ...

Dyonisius Visser

unread,
Feb 2, 2011, 5:02:19 AM2/2/11
to simple...@googlegroups.com
On 2 February 2011 09:07, Stefano Gargiulo <rast...@gmail.com> wrote:

> authentication) so, my 50 cents.

I think the saying is "my 2 cents".
You might have been listen too much to hiphip music ;-)

--
Dyonisius Visser
System & Networking Engineer
TERENA Secretariat
Singel 468 D, 1017 AW Amsterdam
The Netherlands
T +31 20 530 44 88 F +31 20 530 44 99
vis...@terena.org | www.terena.org

Stefano Gargiulo

unread,
Feb 2, 2011, 8:41:20 AM2/2/11
to simple...@googlegroups.com
:) ...call it inflation "bro". ;)

sorry but i coded  to much these days and  i was writing this email via my android.... it's enough to excuse a freudian lapsus? :P


Regards,
Stefano.




2011/2/2 Dyonisius Visser <vis...@terena.org>
Reply all
Reply to author
Forward
0 new messages