1 /* 2 * Copyright (c) 2015 The DragonFly Project. All rights reserved. 3 * 4 * This code is derived from software contributed to The DragonFly Project 5 * by Matthew Dillon <dillon@dragonflybsd.org> 6 * by Venkatesh Srinivas <vsrinivas@dragonflybsd.org> 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in 16 * the documentation and/or other materials provided with the 17 * distribution. 18 * 3. Neither the name of The DragonFly Project nor the names of its 19 * contributors may be used to endorse or promote products derived 20 * from this software without specific, prior written permission. 21 * 22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 30 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 32 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 33 * SUCH DAMAGE. 34 */ 35 /* 36 * Use: pipe syslog auth output to this program. 37 * 38 * Detects failed ssh login attempts and maps out the originating IP and 39 * issues 'ipfw add' commands adding a lockout for rule 2100. 40 * 41 * /etc/syslog.conf line example: 42 * auth.info;authpriv.info |exec /usr/sbin/sshlockout 43 * 44 * Also suggest a cron entry to clean out the ipfw list at least once a day. 45 * 3 3 * * * ipfw delete 2100 46 */ 47 48 #include <sys/types.h> 49 #include <sys/time.h> 50 #include <stdio.h> 51 #include <stdlib.h> 52 #include <unistd.h> 53 #include <string.h> 54 #include <stdarg.h> 55 #include <syslog.h> 56 57 typedef struct iphist { 58 struct iphist *next; 59 struct iphist *hnext; 60 char *ips; 61 time_t t; 62 int hv; 63 } iphist_t; 64 65 #define HSIZE 1024 66 #define HMASK (HSIZE - 1) 67 #define MAXHIST 100 68 #define SSHLIMIT 5 /* per hour */ 69 70 iphist_t *hist_base; 71 iphist_t **hist_tail = &hist_base; 72 iphist_t *hist_hash[HSIZE]; 73 int hist_count; 74 75 static void checkline(char *buf); 76 static int insert_iph(const char *ips); 77 static void delete_iph(iphist_t *ip); 78 79 /* 80 * Stupid simple string hash 81 */ 82 static __inline 83 int 84 iphash(const char *str) 85 { 86 int hv = 0xA1B3569D; 87 while (*str) { 88 hv = (hv << 5) ^ *str ^ (hv >> 23); 89 ++str; 90 } 91 return hv; 92 } 93 94 int 95 main(int ac __unused, char **av __unused) 96 { 97 char buf[1024]; 98 99 openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH); 100 syslog(LOG_ERR, "sshlockout starting up"); 101 freopen("/dev/null", "w", stdout); 102 freopen("/dev/null", "w", stderr); 103 104 while (fgets(buf, sizeof(buf), stdin) != NULL) { 105 if (strstr(buf, "sshd") == NULL) 106 continue; 107 checkline(buf); 108 } 109 syslog(LOG_ERR, "sshlockout exiting"); 110 return(0); 111 } 112 113 static 114 void 115 checkline(char *buf) 116 { 117 char ips[128]; 118 char *str; 119 int n1; 120 int n2; 121 int n3; 122 int n4; 123 124 /* 125 * ssh login attempt with password (only hit if ssh allows 126 * password entry). Root or admin. 127 */ 128 if ((str = strstr(buf, "Failed password for root from")) != NULL || 129 (str = strstr(buf, "Failed password for admin from")) != NULL) { 130 while (*str && (*str < '0' || *str > '9')) 131 ++str; 132 if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { 133 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", 134 n1, n2, n3, n4); 135 if (insert_iph(ips)) { 136 syslog(LOG_ERR, 137 "Detected ssh password login attempt " 138 "for root, locking out %s\n", 139 ips); 140 snprintf(buf, sizeof(buf), 141 "ipfw add 2100 deny tcp from " 142 "%s to me 22", 143 ips); 144 system(buf); 145 } 146 } 147 return; 148 } 149 150 /* 151 * ssh login attempt with password (only hit if ssh allows password 152 * entry). Non-existant user. 153 */ 154 if ((str = strstr(buf, "Failed password for invalid user")) != NULL) { 155 str += 32; 156 while (*str == ' ') 157 ++str; 158 while (*str && *str != ' ') 159 ++str; 160 if (strncmp(str, " from", 5) == 0 && 161 sscanf(str + 5, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { 162 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", 163 n1, n2, n3, n4); 164 if (insert_iph(ips)) { 165 syslog(LOG_ERR, 166 "Detected ssh password login attempt " 167 "for an invalid user, locking out %s\n", 168 ips); 169 snprintf(buf, sizeof(buf), 170 "ipfw add 2100 deny tcp from " 171 "%s to me 22", 172 ips); 173 system(buf); 174 } 175 } 176 return; 177 } 178 179 /* 180 * Premature disconnect in pre-authorization phase, typically an 181 * attack but require 5 attempts in an hour before cleaning it out. 182 */ 183 if ((str = strstr(buf, "Received disconnect from ")) != NULL && 184 strstr(buf, "[preauth]") != NULL) { 185 if (sscanf(str + 25, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) { 186 snprintf(ips, sizeof(ips), "%d.%d.%d.%d", 187 n1, n2, n3, n4); 188 if (insert_iph(ips)) { 189 syslog(LOG_ERR, 190 "Detected ssh password login attempt " 191 "for an invalid user, locking out %s\n", 192 ips); 193 snprintf(buf, sizeof(buf), 194 "ipfw add 2100 deny tcp from " 195 "%s to me 22", 196 ips); 197 system(buf); 198 } 199 } 200 return; 201 } 202 } 203 204 /* 205 * Insert IP record 206 */ 207 static 208 int 209 insert_iph(const char *ips) 210 { 211 iphist_t *ip = malloc(sizeof(*ip)); 212 iphist_t *scan; 213 time_t t = time(NULL); 214 int found; 215 216 ip->hv = iphash(ips); 217 ip->ips = strdup(ips); 218 ip->t = t; 219 220 ip->hnext = hist_hash[ip->hv & HMASK]; 221 hist_hash[ip->hv & HMASK] = ip; 222 ip->next = NULL; 223 *hist_tail = ip; 224 hist_tail = &ip->next; 225 ++hist_count; 226 227 /* 228 * hysteresis 229 */ 230 if (hist_count > MAXHIST + 16) { 231 while (hist_count > MAXHIST) 232 delete_iph(hist_base); 233 } 234 235 /* 236 * Check limit 237 */ 238 found = 0; 239 for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) { 240 if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) { 241 int dt = (int)(t - ip->t); 242 if (dt < 60 * 60) { 243 ++found; 244 if (found > SSHLIMIT) 245 break; 246 } 247 } 248 } 249 return (found > SSHLIMIT); 250 } 251 252 /* 253 * Delete an ip record. Note that we always delete from the head of the 254 * list, but we will still wind up scanning hash chains. 255 */ 256 static 257 void 258 delete_iph(iphist_t *ip) 259 { 260 iphist_t **scanp; 261 iphist_t *scan; 262 263 scanp = &hist_base; 264 while ((scan = *scanp) != ip) { 265 scanp = &scan->next; 266 } 267 *scanp = ip->next; 268 if (hist_tail == &ip->next) 269 hist_tail = scanp; 270 271 scanp = &hist_hash[ip->hv & HMASK]; 272 while ((scan = *scanp) != ip) { 273 scanp = &scan->hnext; 274 } 275 *scanp = ip->hnext; 276 277 --hist_count; 278 free(ip); 279 } 280