I'm using the FTP server that comes with kerberos 5 (in krb5-workstation),
and noticed that by default everyone with a password can log in *except*
those
in /etc/ftpusers (which by default doesn't exist... but probably should,
even if it
only includes "root" by default).
Anyway, I noticed that this server was susceptible to getting FTP password
attacks from East Asia.. and I decided this was pretty annoying. So I
came up with a graylisting mechanism that applies exponential backoff (up
to a maximum) for someone logging in with bad user names or passwords.
It's not enabled by default, but if you use the -g, -G, and -S arguments
then
it gets turned on (actually, you can get by with just the -g argument,
and the
rest will default to reasonable values).
We use it here as (in /etc/xinetd.d/gssftp):
server_args = -l -l -g 60 -G 604800 -S 2
which means:
the first time someone logs in with 3 bad connection attempts in a row,
graylist them for 60 seconds.
If the user logs in correctly, his future graylisting time is forgotten.
If the user connects after his graylisting time expires, the connection
is permitted. Otherwise, the user is spanked and his graylisting time
increased by the scaling factor (in this case doubling) until it reaches
the maximum graylist amount (here it's 7 days), starting from that
instant.
While this doesn't make a site significantly more protected, it does
hugely increase the amount of time (and simultaneous resources) that
someone trying to randomly break into sites would have to take...
making it more likely that they would be detected... then reported,
investigated, prosecuted... and hung... before doing any substantial
damage.
I've suggested to the Kerberos team at MIT that they include these
changes upstream... (minus the man page updates, which I only
just finished).
If anyone wants to try these out or send me comments on the patches,
I'd appreciate it.
Thanks,
-Philip
--- src/appl/gssftp/ftpd/ftpd.c.graylist 2006-06-01 15:09:09.000000000 -0600
+++ src/appl/gssftp/ftpd/ftpd.c 2006-06-01 16:25:06.000000000 -0600
@@ -101,6 +101,9 @@
#endif
#include "pathnames.h"
#include <libpty.h>
+#ifdef GRAYLIST
+#include <gdbm.h>
+#endif
#ifdef NEED_SETENV
extern int setenv(char *, char *, int);
@@ -222,6 +225,20 @@
char rhost_addra[16];
char *rhost_sane;
+#ifdef GRAYLIST
+const char *gl_path= "/etc/ftpgraylistdb";
+int gl_initial = 0;
+int gl_scale = 0;
+int gl_max = 0;
+
+typedef struct {
+ time_t start;
+ unsigned shutout;
+} graylist_t;
+
+static int gl_connect(struct in_addr, int);
+#endif
+
/* Defines for authlevel */
#define AUTHLEVEL_NONE 0
#define AUTHLEVEL_AUTHENTICATE 1
@@ -277,6 +294,18 @@
}
#endif
+#ifdef KRB5_KRB4_COMPAT
+#define KRB5_OPTIONS "s:"
+#else
+#define KRB5_OPTIONS ""
+#endif
+
+#ifdef GRAYLIST
+#define GL_OPTIONS "G:g:S:"
+#else
+#define GL_OPTIONS
+#endif
+
int stripdomain = 1;
int maxhostlen = 0;
int always_ip = 0;
@@ -290,11 +319,11 @@
int addrlen, c, on = 1, tos, port = -1;
extern char *optarg;
extern int optopt;
-#ifdef KRB5_KRB4_COMPAT
- char *option_string = "AaCcdlp:r:s:T:t:U:u:vw:";
-#else /* !KRB5_KRB4_COMPAT */
- char *option_string = "AaCcdlp:r:T:t:U:u:vw:";
-#endif /* KRB5_KRB4_COMPAT */
+ char *option_string = "AaCcdlp:r:T:t:U:u:vw:" KRB5_OPTIONS
+ GL_OPTIONS;
+#ifdef GRAYLIST
+ unsigned expire;
+#endif
ftpusers = _PATH_FTPUSERS_DEFAULT;
#ifdef KRB5_KRB4_COMPAT
@@ -420,6 +449,29 @@
}
break;
}
+#ifdef GRAYLIST
+ case 'G':
+ gl_max = atoi(optarg);
+ if (gl_max <= 300 || gl_max >= (365 * 24 * 3600)) {
+ fprintf(stderr, "ftpd: bad arg to -G\n");
+ exit(1);
+ }
+ break;
+ case 'g':
+ gl_initial = atoi(optarg);
+ if (gl_initial <= 5 || gl_initial > 7200) {
+ fprintf(stderr, "ftpd: bad arg to -g\n");
+ exit(1);
+ }
+ break;
+ case 'S':
+ gl_scale = atoi(optarg);
+ if (gl_scale <= 2 || gl_scale > 100) {
+ fprintf(stderr, "ftpd: bad arg to -S\n");
+ exit(1);
+ }
+ break;
+#endif
default:
fprintf(stderr, "ftpd: Unknown flag -%c ignored.\n",
(char)optopt);
@@ -427,6 +479,21 @@
}
}
+#ifdef GRAYLIST
+ if (!gl_scale && gl_initial)
+ gl_scale = 2;
+ if (gl_max && !gl_initial) {
+ fprintf(stderr, "ftpd: -G must be used with -g\n");
+ exit(1);
+ } else if (gl_initial && !gl_max) {
+ gl_max = gl_initial * (gl_scale * gl_scale * gl_scale *
+ gl_scale * gl_scale);
+ } else if (gl_initial > gl_max) {
+ fprintf(stderr, "ftpd: -g arg must be less than -G arg\n");
+ exit(1);
+ }
+#endif
+
if (port != -1) {
struct sockaddr_in sin4;
int s, ns, sz;
@@ -538,6 +605,15 @@
stru = STRU_F;
mode = MODE_S;
tmpline[0] = '\0';
+
+#ifdef GRAYLIST
+ expire = gl_connect(his_addr.sin_addr, 1);
+ if (expire > 0) {
+ reply(530, "Client graylisted for %u seconds.", expire);
+ dologout(0);
+ }
+#endif
+
(void) gethostname(hostname, sizeof (hostname));
reply(220, "%s FTP server (%s) ready.", hostname, version);
(void) setjmp(errcatch);
@@ -909,6 +985,171 @@
guest = 0;
}
+#ifdef GRAYLIST
+static void
+gl_badlogin(addr)
+struct in_addr addr;
+{
+ GDBM_FILE dbf;
+ datum key, content;
+ graylist_t rec;
+ time_t now;
+ int status;
+
+ /* feature is disabled */
+ if (!gl_initial)
+ return;
+
+ syslog(LOG_DEBUG, "graylist: badlogin %s", inet_ntoa(addr));
+
+ dbf = gdbm_open(gl_path, 512, GDBM_WRCREAT, 0600, 0);
+ if (!dbf) {
+ syslog(LOG_ERR, "graylist: can't open database (errno %d)",
+ gdbm_errno);
+ return;
+ }
+
+ key.dptr = (caddr_t) &addr;
+ key.dsize = sizeof(addr);
+ content = gdbm_fetch(dbf, key);
+
+ time(&now);
+
+ if (!content.dptr) {
+ rec.shutout = gl_initial;
+ } else {
+ memcpy(&rec, content.dptr, content.dsize);
+ free(content.dptr);
+ /* if the user waited the maximum time, then we reset him */
+ if (now > rec.start + gl_max)
+ rec.shutout = gl_initial;
+ else {
+ rec.shutout *= gl_scale;
+ if (rec.shutout > gl_max)
+ rec.shutout = gl_max;
+ }
+ }
+ /* reset the timer... */
+ rec.start = now;
+
+ content.dptr = (caddr_t) &rec;
+ content.dsize = sizeof(rec);
+
+ syslog(LOG_INFO, "graylisting %s for %u seconds", inet_ntoa(addr),
+ rec.shutout);
+
+ status = gdbm_store(dbf, key, content, GDBM_REPLACE);
+ if (status < 0) {
+ syslog(LOG_ERR, "graylist: gdbm_store(%s) returns %d (errno %d)",
+ inet_ntoa(addr), status, gdbm_errno);
+ }
+
+ gdbm_close(dbf);
+}
+
+static int
+gl_connect(addr, punish)
+struct in_addr addr;
+int punish;
+{
+ GDBM_FILE dbf;
+ datum key, content;
+ graylist_t rec;
+ time_t now, expire;
+ unsigned interval;
+ int status;
+
+ /* feature is disabled */
+ if (!gl_initial)
+ return 0;
+
+ syslog(LOG_DEBUG, "graylist: connect %s, %d", inet_ntoa(addr), punish);
+
+ dbf = gdbm_open(gl_path, 512, GDBM_WRCREAT, 0600, 0);
+ if (!dbf) {
+ syslog(LOG_ERR, "graylist: can't open database (errno %d)",
+ gdbm_errno);
+ return;
+ }
+
+ key.dptr = (caddr_t) &addr;
+ key.dsize = sizeof(addr);
+ content = gdbm_fetch(dbf, key);
+
+ if (!content.dptr) {
+ok: gdbm_close(dbf);
+ return 0;
+ }
+
+ memcpy(&rec, content.dptr, content.dsize);
+ free(content.dptr);
+
+ expire = rec.start + rec.shutout;
+ time(&now);
+
+ /* we're past the expiry... but we don't delete the record */
+ if (expire <= now)
+ goto ok;
+
+ /* do we spank them for connecting before the graylist expires? */
+ if (punish) {
+ rec.start = now;
+ rec.shutout *= gl_scale;
+ if (rec.shutout > gl_max)
+ rec.shutout = gl_max;
+ time(&rec.start);
+ status = gdbm_store(dbf, key, content, GDBM_REPLACE);
+ if (status < 0) {
+ syslog(LOG_ERR, "graylist: gdbm_store(%s) returns %d (errno %d)",
+ inet_ntoa(addr), status, gdbm_errno);
+ }
+ /* update expiry to reflect spanking */
+ expire = rec.start + rec.shutout;
+ }
+
+ syslog(LOG_INFO, "graylisted host %s %shas %u seconds remaining",
+ inet_ntoa(addr), (punish ? "now " : ""), expire - now);
+
+ gdbm_close(dbf);
+
+ /* how long before they can retry */
+ return (expire - now);
+}
+
+static int
+gl_unblock(addr)
+struct in_addr addr;
+{
+ GDBM_FILE dbf;
+ datum key;
+ time_t now, expire;
+ int status;
+
+ /* feature is disabled */
+ if (!gl_initial)
+ return;
+
+ syslog(LOG_DEBUG, "graylist: authenticated %s", inet_ntoa(addr));
+
+ dbf = gdbm_open(gl_path, 512, GDBM_WRCREAT, 0600, 0);
+ if (!dbf) {
+ syslog(LOG_ERR, "graylist: can't open database (errno %d)", gdbm_errno);
+ return;
+ }
+
+ key.dptr = (caddr_t) &addr;
+ key.dsize = sizeof(addr);
+ status = gdbm_delete(dbf, key);
+
+ if (status != 0 && status != -1) {
+ syslog(LOG_ERR, "graylist: gdbm_delete(%s) returns %d (errno %d)",
+ inet_ntoa(addr), status, gdbm_errno);
+ }
+
+ gdbm_close(dbf);
+}
+#endif
+
static int
kpass(name, passwd)
char *name, *passwd;
@@ -1076,6 +1317,9 @@
syslog(LOG_NOTICE,
"repeated login failures from %s (%s)",
rhost_addra, remotehost);
+#ifdef GRAYLIST
+ gl_badlogin(his_addr.sin_addr);
+#endif
dologout(0);
}
reply(530, "Login incorrect.");
@@ -1084,6 +1328,10 @@
}
login_attempts = 0; /* this time successful */
+#ifdef GRAYLIST
+ gl_unblock(his_addr.sin_addr);
+#endif
+
login(passwd, 0);
return;
}
--- src/appl/gssftp/ftpd/ftpd.M.graylist 2006-06-01 15:58:00.000000000 -0600
+++ src/appl/gssftp/ftpd/ftpd.M 2006-06-01 16:25:33.000000000 -0600
@@ -38,6 +38,7 @@
.B ftpd
[\fB\-A \fP|\fB -a\fP] [\fB\-C\fP] [\fB\-c\fP] [\fB\-d\fP] [\fB\-l\fP]
[\fB\-v\fP] [\fB\-T\fP \fImaxtimeout\fP] [\fB\-t\fP \fItimeout\fP]
+[\fB\-g\fP \fIinitialgraylist \fP [\fB-G\fP \fImaxgraylist\fP] [\fB-S\fP \fIscalefactor\fP]
[\fB\-p\fP \fIport\fP] [\fB\-U\fP \fIftpusers-file\fP] [\fB\-u\fP \fIumask\fP]
[\fB\-r\fP \fIrealm-file\fP] [\fB\-s\fP \fIsrvtab\fP]
[\fB\-w\fP{\fBip\fP|\fImaxhostlen\fP[\fB,\fP{\fBstriplocal\fP|\fBnostriplocal\fP}]}]
@@ -116,6 +117,25 @@
file to use. The default value is normally
.IR /etc/ftpusers .
.TP
+\fB\-g\fP \fIinitialgraylist\fP
+Sets the initial time to graylist a user following 3 successive failed
+logins on the same connection. Should be in the range of 5..300.
+.TP
+\fB-G\fP \fImaxgraylist\fP
+Sets the maximum time to graylist a user should he attempt to reconnect
+before the graylist expires, or fail to login correctly 3 times on the same
+connection before the maximum time has expired (but after the current
+graylist interval expired). Should be in the range of 300..31536000.
+If unspecified, it defaults to the \fIinitialgraylist\fP multipled by the
+scalefactor to the fifth power.
+.TP
+\fB-S\fP \fIscalefactor\fP
+Sets the scaling factor to increase the graylist period by should the
+user attempt to connect before the graylist expires. That is, if the
+user connects before the current graylist expires, then the
+current graylist will be increased by \fIscalefactor\fP times. Should
+be in the range of 2..100. If unspecified, defaults to 2.
+.TP
\fB\-u\fP \fIumask\fP
Sets the umask for the ftpd process. The default value is normally 027.
.TP
--
fedora-devel-list mailing list
fedora-devel-list@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/fedora-devel-list