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