xref: /dflybsd-src/usr.sbin/sshlockout/sshlockout.c (revision d217426c317e15b34c1a61f020c85bd2b08f5c0d)
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