From a4ac8286be21b1495af8ec1db83271dacaa79556 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Wed, 31 Dec 2014 19:21:47 -0800 Subject: [PATCH] sshlockout - Add sshlockout utility * Add sshlockout utility, typically setup as a syslog pipe. This utility monitors for failed ssh login attempts and excessive preauth failures and will add a rule via IPFW to block the originating IP. The operator also typically sets up a cron job to clean out the IPFW rules that have accumulated once a day. * See man page for details. Still under construction (feel free to submit additional features). TODO - IPV6 TODO - Use a PF table instead of IPFW, which will greatly improve performance if a lot of rules have to be added. --- usr.sbin/Makefile | 1 + usr.sbin/sshlockout/Makefile | 6 + usr.sbin/sshlockout/sshlockout.8 | 78 +++++++++ usr.sbin/sshlockout/sshlockout.c | 279 +++++++++++++++++++++++++++++++ 4 files changed, 364 insertions(+) create mode 100644 usr.sbin/sshlockout/Makefile create mode 100644 usr.sbin/sshlockout/sshlockout.8 create mode 100644 usr.sbin/sshlockout/sshlockout.c diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile index b48663fb12..6a0430e7cc 100644 --- a/usr.sbin/Makefile +++ b/usr.sbin/Makefile @@ -112,6 +112,7 @@ SUBDIR= 802_11 \ sensorsd \ service \ setkey \ + sshlockout \ sliplogin \ slstat \ spray \ diff --git a/usr.sbin/sshlockout/Makefile b/usr.sbin/sshlockout/Makefile new file mode 100644 index 0000000000..fd184947c3 --- /dev/null +++ b/usr.sbin/sshlockout/Makefile @@ -0,0 +1,6 @@ +# +# +PROG= sshlockout +MAN= sshlockout.8 + +.include diff --git a/usr.sbin/sshlockout/sshlockout.8 b/usr.sbin/sshlockout/sshlockout.8 new file mode 100644 index 0000000000..c785d53afc --- /dev/null +++ b/usr.sbin/sshlockout/sshlockout.8 @@ -0,0 +1,78 @@ +.\" Copyright (c) 2015 The DragonFly Project. All rights reserved. +.\" +.\" This code is derived from software contributed to The DragonFly Project +.\" by Matthew Dillon +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in +.\" the documentation and/or other materials provided with the +.\" distribution. +.\" 3. Neither the name of The DragonFly Project nor the names of its +.\" contributors may be used to endorse or promote products derived +.\" from this software without specific, prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +.\" FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +.\" COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +.\" INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, +.\" BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +.\" LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +.\" AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +.\" OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +.\" OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd January 1, 2015 +.Dt SSHLOCKOUT 8 +.Os +.Sh NAME +.Nm sshlockout +.Nd utility to block port 22 on preauth failures +.Sh SYNOPSIS +.Cd auth.info;authpriv.info |exec /usr/sbin/sshlockout +.Cd 3 3 * * * ipfw delete 2100 +.Sh DESCRIPTION +This program is generally installed in +.Pa /etc/syslog.conf +as a pipe to parse the +.Xr sshd 8 +demons error log in realtime. +In addition, a root crontab entry should generally be created to clean +out +.Xr ipfw 8 +rule 2100 at least once a day. +The +.Xr ipfw 8 +module must be loaded and operational as well. +.Pp +This program will monitor the ssh syslog output and keep track of attempts +to login to unknown users as well as preauth failures. +If 5 attempts fail in any one hour period, a permanent entry is added to +rule 2100 in ipfw to block port 22 from the associated IP address. +The cron entry you create cleans the block list out typically once a day. +.Pp +This program generally limits brute-force attempts to break into a machine +via ssh. +.Sh NOTICE +This program is still a work in progress. +Currently this program only operates on IPV4 addresses. +.Sh SEE ALSO +.Xr ssh 1 , +.Xr sshd 8 , +.Xr ipfw 8 , +.Xr syslog.conf 5 +.Sh HISTORY +The +.Nm +utility first appeared in +.Dx 4.1 . +.Sh AUTHORS +.An Matthew Dillon Aq Mt dillon@backplane.com diff --git a/usr.sbin/sshlockout/sshlockout.c b/usr.sbin/sshlockout/sshlockout.c new file mode 100644 index 0000000000..6da2eb0e43 --- /dev/null +++ b/usr.sbin/sshlockout/sshlockout.c @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2015 The DragonFly Project. All rights reserved. + * + * This code is derived from software contributed to The DragonFly Project + * by Matthew Dillon + * by Venkatesh Srinivas + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of The DragonFly Project nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific, prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +/* + * Use: pipe syslog auth output to this program. + * + * Detects failed ssh login attempts and maps out the originating IP and + * issues 'ipfw add' commands adding a lockout for rule 2100. + * + * /etc/syslog.conf line example: + * auth.info;authpriv.info |exec /usr/sbin/sshlockout + * + * Also suggest a cron entry to clean out the ipfw list at least once a day. + * 3 3 * * * ipfw delete 2100 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct iphist { + struct iphist *next; + struct iphist *hnext; + char *ips; + time_t t; + int hv; +} iphist_t; + +#define HSIZE 1024 +#define HMASK (HSIZE - 1) +#define MAXHIST 100 +#define SSHLIMIT 5 /* per hour */ + +iphist_t *hist_base; +iphist_t **hist_tail = &hist_base; +iphist_t *hist_hash[HSIZE]; +int hist_count; + +static void checkline(char *buf); +static int insert_iph(const char *ips); +static void delete_iph(iphist_t *ip); + +/* + * Stupid simple string hash + */ +static __inline +int +iphash(const char *str) +{ + int hv = 0xA1B3569D; + while (*str) { + hv = (hv << 5) ^ *str ^ (hv >> 23); + ++str; + } + return hv; +} + +int +main(int ac __unused, char **av __unused) +{ + char buf[1024]; + + openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH); + syslog(LOG_ERR, "sshlockout starting up"); + freopen("/dev/null", "w", stdout); + freopen("/dev/null", "w", stderr); + + while (fgets(buf, sizeof(buf), stdin) != NULL) { + if (strstr(buf, "sshd") == NULL) + continue; + checkline(buf); + } + syslog(LOG_ERR, "sshlockout exiting"); + return(0); +} + +static +void +checkline(char *buf) +{ + char ips[128]; + char *str; + int n1; + int n2; + int n3; + int n4; + + /* + * ssh login attempt with password (only hit if ssh allows + * password entry). Root or admin. + */ + if ((str = strstr(buf, "Failed password for root from")) != NULL || + (str = strstr(buf, "Failed password for admin from")) != NULL) { + while (*str && (*str < '0' || *str > '9')) + ++str; + if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { + snprintf(ips, sizeof(ips), "%d.%d.%d.%d", + n1, n2, n3, n4); + if (insert_iph(ips)) { + syslog(LOG_ERR, + "Detected ssh password login attempt " + "for root, locking out %s\n", + ips); + snprintf(buf, sizeof(buf), + "ipfw add 2100 deny tcp from " + "%s to me 22", + ips); + system(buf); + } + } + return; + } + + /* + * ssh login attempt with password (only hit if ssh allows password + * entry). Non-existant user. + */ + if ((str = strstr(buf, "Failed password for invalid user")) != NULL) { + str += 32; + while (*str == ' ') + ++str; + while (*str && *str != ' ') + ++str; + if (strncmp(str, " from", 5) == 0 && + sscanf(str + 5, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { + snprintf(ips, sizeof(ips), "%d.%d.%d.%d", + n1, n2, n3, n4); + if (insert_iph(ips)) { + syslog(LOG_ERR, + "Detected ssh password login attempt " + "for an invalid user, locking out %s\n", + ips); + snprintf(buf, sizeof(buf), + "ipfw add 2100 deny tcp from " + "%s to me 22", + ips); + system(buf); + } + } + return; + } + + /* + * Premature disconnect in pre-authorization phase, typically an + * attack but require 5 attempts in an hour before cleaning it out. + */ + if ((str = strstr(buf, "Received disconnect from ")) != NULL && + strstr(buf, "[preauth]") != NULL) { + if (sscanf(str + 25, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { + snprintf(ips, sizeof(ips), "%d.%d.%d.%d", + n1, n2, n3, n4); + if (insert_iph(ips)) { + syslog(LOG_ERR, + "Detected ssh password login attempt " + "for an invalid user, locking out %s\n", + ips); + snprintf(buf, sizeof(buf), + "ipfw add 2100 deny tcp from " + "%s to me 22", + ips); + system(buf); + } + } + return; + } +} + +/* + * Insert IP record + */ +static +int +insert_iph(const char *ips) +{ + iphist_t *ip = malloc(sizeof(*ip)); + iphist_t *scan; + time_t t = time(NULL); + int found; + + ip->hv = iphash(ips); + ip->ips = strdup(ips); + ip->t = t; + + ip->hnext = hist_hash[ip->hv & HMASK]; + hist_hash[ip->hv & HMASK] = ip; + ip->next = NULL; + *hist_tail = ip; + hist_tail = &ip->next; + ++hist_count; + + /* + * hysteresis + */ + if (hist_count > MAXHIST + 16) { + while (hist_count > MAXHIST) + delete_iph(hist_base); + } + + /* + * Check limit + */ + found = 0; + for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) { + if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) { + int dt = (int)(t - ip->t); + if (dt < 60 * 60) { + ++found; + if (found > SSHLIMIT) + break; + } + } + } + return (found > SSHLIMIT); +} + +/* + * Delete an ip record. Note that we always delete from the head of the + * list, but we will still wind up scanning hash chains. + */ +static +void +delete_iph(iphist_t *ip) +{ + iphist_t **scanp; + iphist_t *scan; + + scanp = &hist_base; + while ((scan = *scanp) != ip) { + scanp = &scan->next; + } + *scanp = ip->next; + if (hist_tail == &ip->next) + hist_tail = scanp; + + scanp = &hist_hash[ip->hv & HMASK]; + while ((scan = *scanp) != ip) { + scanp = &scan->hnext; + } + *scanp = ip->hnext; + + --hist_count; + free(ip); +} -- 2.41.0