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