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, in case of a PF firewall, adds to a PF table <lockout> using
40  * 'pfctl -tlockout -Tadd' commands.
41  *
42  * /etc/syslog.conf line example:
43  *	auth.info;authpriv.info		|exec /usr/sbin/sshlockout -pf lockout
44  *
45  * Also suggest a cron entry to clean out the PF table at least once a day.
46  *	3 3 * * *       pfctl -tlockout -Tflush
47  *
48  * Alternatively there is an ipfw(8) mode (-ipfw <rulenum>).
49  */
50 
51 #include <sys/types.h>
52 #include <sys/socket.h>
53 #include <sys/sysctl.h>
54 #include <sys/time.h>
55 
56 #include <arpa/inet.h>
57 #include <netinet/in.h>
58 #include <net/if.h>
59 #include <net/ipfw/ip_fw.h>
60 
61 #include <stdio.h>
62 #include <stdlib.h>
63 #include <unistd.h>
64 #include <string.h>
65 #include <stdarg.h>
66 #include <syslog.h>
67 #include <ctype.h>
68 #include <stdbool.h>
69 
70 typedef struct iphist {
71 	struct iphist *next;
72 	struct iphist *hnext;
73 	char	*ips;
74 	time_t	t;
75 	int	hv;
76 } iphist_t;
77 
78 struct args {
79 	int   fw_type;
80 	char *arg1;
81 	int   arg2;
82 };
83 
84 #define FW_IS_PF	1
85 #define FW_IS_IPFW	2
86 #define FW_IS_IPFWTBL	3
87 
88 #define HSIZE		1024
89 #define HMASK		(HSIZE - 1)
90 #define MAXHIST		100
91 #define SSHLIMIT	5		/* per hour */
92 #define MAX_TABLE_NAME	20		/* PF table name limit */
93 
94 static iphist_t *hist_base;
95 static iphist_t **hist_tail = &hist_base;
96 static iphist_t *hist_hash[HSIZE];
97 static int hist_count = 0;
98 
99 static int ipfw_sock = -1;
100 
101 static struct args args;
102 
103 static void init_iphist(void);
104 static void checkline(char *buf);
105 static int insert_iph(const char *ips, time_t t);
106 static void delete_iph(iphist_t *ip);
107 
108 static void
109 block_ip(const char *ips)
110 {
111 	struct ipfw_ioc_tblcont ent;
112 	struct ipfw_ioc_tblent *te;
113 	char buf[128];
114 	int r = 0;
115 
116 	switch (args.fw_type) {
117 	case FW_IS_PF:
118 		r = snprintf(buf, sizeof(buf),
119 			"pfctl -t%s -Tadd %s", args.arg1, ips);
120 		break;
121 
122 	case FW_IS_IPFW:
123 		r = snprintf(buf, sizeof(buf),
124 			"ipfw add %s deny tcp from %s to me 22",
125 			args.arg1, ips);
126 		break;
127 
128 	case FW_IS_IPFWTBL:
129 		memset(&ent, 0, sizeof(ent));
130 		ent.tableid = args.arg2;
131 		ent.entcnt = 1;
132 		te = &ent.ent[0];
133 
134 		r = inet_pton(AF_INET, ips, &te->key.sin_addr);
135 		if (r <= 0)
136 			break;
137 		te->key.sin_family = AF_INET;
138 		te->key.sin_len = sizeof(struct sockaddr_in);
139 
140 		if (setsockopt(ipfw_sock, IPPROTO_IP, IP_FW_TBL_ADD,
141 		    &ent, sizeof(ent)) < 0) {
142 			r = -1;
143 			break;
144 		}
145 		/* Done */
146 		return;
147 	}
148 
149 	if (r > 0 && (int)strlen(buf) == r) {
150 		system(buf);
151 	} else {
152 		syslog(LOG_ERR, "sshlockout: invalid command");
153 	}
154 }
155 
156 /*
157  * Stupid simple string hash
158  */
159 static __inline int
160 iphash(const char *str)
161 {
162 	int hv = 0xA1B3569D;
163 
164 	while (*str) {
165 		hv = (hv << 5) ^ *str ^ (hv >> 23);
166 		++str;
167 	}
168 	return hv;
169 }
170 
171 
172 static bool
173 parse_args(int ac, char **av)
174 {
175 	if (ac >= 2) {
176 		if (strcmp(av[1], "-pf") == 0 && ac == 3) {
177 			/* -pf <tablename> */
178 			char *tablename = av[2];
179 
180 			if (strlen(tablename) > 0 &&
181 			    strlen(tablename) < MAX_TABLE_NAME) {
182 				args.fw_type = FW_IS_PF;
183 				args.arg1 = tablename;
184 				return true;
185 			}
186 		}
187 		if (strcmp(av[1], "-ipfw") == 0 && ac == 3) {
188 			/* -ipfw <rule> */
189 			char *rule = av[2];
190 
191 			if (strlen(rule) > 0 && strlen(rule) <= 5) {
192 				for (char *s = rule; *s; ++s) {
193 					if (!isdigit(*s))
194 						return false;
195 				}
196 				if (atoi(rule) < 1)
197 					return false;
198 				if (atoi(rule) > 65535)
199 					return false;
200 				args.fw_type = FW_IS_IPFW;
201 				args.arg1 = rule;
202 				return true;
203 			}
204 		}
205 
206 		if (strcmp(av[1], "-ipfwtbl") == 0 && ac == 3) {
207 			/* -ipfwtbl <tableid> */
208 			int tableid;
209 			char *eptr;
210 
211 			tableid = strtoul(av[2], &eptr, 0);
212 			if (*eptr != '\0')
213 				return false;
214 
215 			ipfw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
216 			if (ipfw_sock < 0)
217 				return false;
218 
219 			args.fw_type = FW_IS_IPFWTBL;
220 			args.arg2 = tableid;
221 			return true;
222 		}
223 	}
224 
225 	return false;
226 }
227 
228 int
229 main(int ac, char **av)
230 {
231 	char buf[1024];
232 
233 	args.fw_type = 0;
234 	args.arg1 = NULL;
235 	args.arg2 = 0;
236 
237 	if (!parse_args(ac, av)) {
238 		syslog(LOG_ERR, "sshlockout: invalid argument");
239 		return(1);
240 	}
241 
242 	init_iphist();
243 
244 	openlog("sshlockout", LOG_PID|LOG_CONS, LOG_AUTH);
245 	syslog(LOG_ERR, "sshlockout starting up");
246 	freopen("/dev/null", "w", stdout);
247 	freopen("/dev/null", "w", stderr);
248 
249 	while (fgets(buf, sizeof(buf), stdin) != NULL) {
250 		if (strstr(buf, "sshd") == NULL)
251 			continue;
252 		checkline(buf);
253 	}
254 	syslog(LOG_ERR, "sshlockout exiting");
255 	return(0);
256 }
257 
258 static void
259 checkip(const char *str, const char *reason1, const char *reason2)
260 {
261 	char ips[128];
262 	int n1;
263 	int n2;
264 	int n3;
265 	int n4;
266 	time_t t = time(NULL);
267 
268 	ips[0] = '\0';
269 
270 	if (sscanf(str, "%d.%d.%d.%d", &n1, &n2, &n3, &n4) == 4) {
271 		snprintf(ips, sizeof(ips), "%d.%d.%d.%d", n1, n2, n3, n4);
272 	} else {
273 		/*
274 		 * Check for IPv6 address (primitive way)
275 		 */
276 		int cnt = 0;
277 		while (str[cnt] == ':' || isxdigit(str[cnt])) {
278 			++cnt;
279 		}
280 		if (cnt > 0 && cnt < (int)sizeof(ips)) {
281 			memcpy(ips, str, cnt);
282 			ips[cnt] = '\0';
283 		}
284 	}
285 
286 	/*
287 	 * We do not block localhost as is makes no sense.
288 	 */
289 	if (strcmp(ips, "127.0.0.1") == 0)
290 		return;
291 	if (strcmp(ips, "::1") == 0)
292 		return;
293 
294 	if (strlen(ips) > 0) {
295 
296 		/*
297 		 * Check for DoS attack. When connections from too many
298 		 * IP addresses come in at the same time, our hash table
299 		 * would overflow, so we delete the oldest entries AND
300 		 * block it's IP when they are younger than 10 seconds.
301 		 * This prevents massive attacks from arbitrary IPs.
302 		 */
303 		if (hist_count > MAXHIST + 16) {
304 			while (hist_count > MAXHIST) {
305 				iphist_t *iph = hist_base;
306 				int dt = (int)(t - iph->t);
307 				if (dt < 10) {
308 					syslog(LOG_ERR,
309 					       "Detected overflow attack, "
310 					       "locking out %s\n",
311 					       iph->ips);
312 					block_ip(iph->ips);
313 				}
314 				delete_iph(iph);
315 			}
316 		}
317 
318 		if (insert_iph(ips, t)) {
319 			syslog(LOG_ERR,
320 			       "Detected ssh %s attempt "
321 			       "for %s, locking out %s\n",
322 			       reason1, reason2, ips);
323 			block_ip(ips);
324 		}
325 	}
326 }
327 
328 static void
329 checkline(char *buf)
330 {
331 	char *str;
332 
333 	/*
334 	 * ssh login attempt with password (only hit if ssh allows
335 	 * password entry).  Root or admin.
336 	 */
337 	if ((str = strstr(buf, "Failed password for root from")) != NULL ||
338 	    (str = strstr(buf, "Failed password for admin from")) != NULL) {
339 		while (*str && (*str < '0' || *str > '9'))
340 			++str;
341 		checkip(str, "password login", "root or admin");
342 		return;
343 	}
344 
345 	/*
346 	 * ssh login attempt with password (only hit if ssh allows password
347 	 * entry).  Non-existant user.
348 	 */
349 	if ((str = strstr(buf, "Failed password for invalid user")) != NULL) {
350 		str += 32;
351 		while (*str == ' ')
352 			++str;
353 		while (*str && *str != ' ')
354 			++str;
355 		if (strncmp(str, " from", 5) == 0) {
356 			checkip(str + 5, "password login", "an invalid user");
357 		}
358 		return;
359 	}
360 
361 	/*
362 	 * ssh login attempt for non-existant user.
363 	 */
364 	if ((str = strstr(buf, "Invalid user")) != NULL) {
365 		str += 12;
366 		while (*str == ' ')
367 			++str;
368 		while (*str && *str != ' ')
369 			++str;
370 		if (strncmp(str, " from", 5) == 0) {
371 			checkip(str + 5, "login", "an invalid user");
372 		}
373 		return;
374 	}
375 
376 	/*
377 	 * Premature disconnect in pre-authorization phase, typically an
378 	 * attack but require 5 attempts in an hour before cleaning it out.
379 	 */
380 	if ((str = strstr(buf, "Received disconnect from ")) != NULL &&
381 	    strstr(buf, "[preauth]") != NULL) {
382 		checkip(str + 25, "preauth", "an invalid user");
383 		return;
384 	}
385 
386 	/*
387 	 * Maximum authentication attempts exceeded
388 	 */
389 	if ((str = strstr(buf, "maximum authentication "
390 			       "attempts exceeded for ")) != NULL &&
391 	    strstr(buf, "[preauth]") != NULL) {
392 		str += 45;
393 		while (*str == ' ')
394 			++str;
395 		while (*str && *str != ' ')
396 			++str;
397 		if (strncmp(str, " from", 5) == 0) {
398 			checkip(str + 5, "login", "many attempts");
399 		}
400 		return;
401 	}
402 }
403 
404 /*
405  * Insert IP record
406  */
407 static int
408 insert_iph(const char *ips, time_t t)
409 {
410 	iphist_t *ip = malloc(sizeof(*ip));
411 	iphist_t *scan;
412 	int found;
413 
414 	ip->hv = iphash(ips);
415 	ip->ips = strdup(ips);
416 	ip->t = t;
417 
418 	ip->hnext = hist_hash[ip->hv & HMASK];
419 	hist_hash[ip->hv & HMASK] = ip;
420 	ip->next = NULL;
421 	*hist_tail = ip;
422 	hist_tail = &ip->next;
423 	++hist_count;
424 
425 	/*
426 	 * hysteresis
427 	 */
428 	if (hist_count > MAXHIST + 16) {
429 		while (hist_count > MAXHIST)
430 			delete_iph(hist_base);
431 	}
432 
433 	/*
434 	 * Check limit
435 	 */
436 	found = 0;
437 	for (scan = hist_hash[ip->hv & HMASK]; scan; scan = scan->hnext) {
438 		if (scan->hv == ip->hv && strcmp(scan->ips, ip->ips) == 0) {
439 			int dt = (int)(t - ip->t);
440 			if (dt < 60 * 60) {
441 				++found;
442 				if (found > SSHLIMIT)
443 					break;
444 			}
445 		}
446 	}
447 	return (found > SSHLIMIT);
448 }
449 
450 /*
451  * Delete an ip record.  Note that we always delete from the head of the
452  * list, but we will still wind up scanning hash chains.
453  */
454 static void
455 delete_iph(iphist_t *ip)
456 {
457 	iphist_t **scanp;
458 	iphist_t *scan;
459 
460 	scanp = &hist_base;
461 	while ((scan = *scanp) != ip) {
462 		scanp = &scan->next;
463 	}
464 	*scanp = ip->next;
465 	if (hist_tail == &ip->next)
466 		hist_tail = scanp;
467 
468 	scanp = &hist_hash[ip->hv & HMASK];
469 	while ((scan = *scanp) != ip) {
470 		scanp = &scan->hnext;
471 	}
472 	*scanp = ip->hnext;
473 
474 	--hist_count;
475 	free(ip);
476 }
477 
478 static void
479 init_iphist(void)
480 {
481 	hist_base = NULL;
482 	hist_tail = &hist_base;
483 	for (int i = 0; i < HSIZE; i++) {
484 		hist_hash[i] = NULL;
485 	}
486 	hist_count = 0;
487 }
488