1 #define HASSNPRINTF 1
2 #ifndef __P
3 #define __P(proto) proto
4 #endif
5 
6 #include <ctype.h>
7 #include <fcntl.h>
8 #ifdef linux
9 #include <getopt.h>
10 #include <sys/ioctl.h>
11 #endif
12 #include <stdio.h>
13 #include <string.h>
14 #include <sysexits.h>
15 #include <termios.h>
16 #include <unistd.h>
17 #ifdef __FreeBSD__
18 #include <stdlib.h>
19 #include <netdb.h>
20 #include <unistd.h>
21 #endif
22 #include "libmilter/mfapi.h"
23 
24 /*
25  * Al Smith <Al.Smith@aeschi.ch.eu.org>  May 9 2000
26  *
27  * Enhancements submitted by Sven Nielsen <dalvenjah@dal.net>  May 13 2000
28  * Inline rejection suggested by Paul Yeh <Paul.Yeh@abc.com>   May 24 2000
29  *
30  * Please note that this code requires sendmail-8.11.0 or newer.
31  *
32  * This code is distributed under the GNU General Public License.
33  * Share and enjoy.
34  */
35 
36 /*
37  * HISTORY
38  * February 6 2004 -- vbsfilter-1.15
39  * Executable name could be in Quoted-Printable format.
40  * Thanks to David Chapeau for reporting this.
41  *
42  * June 26 2003 -- vbsfilter-1.14
43  * New version of sobig, using the .zi extension. Block this.
44  * Thanks to David F. Russell for reporting this.
45  *
46  * January 30 2003
47  * Implement some suggestions from David F. Russell.
48  *
49  * January 14 2003 -- vbsfilter-1.11
50  * Recognise quoted filenames with missing end quote.
51  * Problem (and fix!) supplied by Ian j Hart <ianjhart@ntlworld.com>
52  * and Andrea Adams <andrea@vividimage.com> in response to a virus
53  * termed W32.Sobig.A@mm. Thanks!
54  *
55  * November 12 2002 -- vbsfilter-1.10
56  * Use ioctl(TIOCNOTTY) on /dev/tty.
57  *
58  * Herbert Straub <h.straub@aon.at>
59  * close STDIN, STDOUT and STDERR to avoid hanging processes on exit.
60  *
61  * June 12 2002 -- vbsfilter-1.9
62  * Ross Bergman <rbergman@vividusa.com> noted that some malformed endings,
63  * for example "foo.exe.". Also that a file can have a CLSID as a file
64  * extension, again hidden by windows, and Windows will use the CLSID to
65  * determine how to open the attachment.
66  *
67  * January 30 2002 -- vbsfilter-1.8
68  * Updated vbsfilter to detect uuencoded attachments.
69  * Thanks to Matthew Wong <matthew_wong@aelhk.com.hk> for noting
70  * that the "MyParty" virus spreads itself in this way.
71  *
72  * August 6 2001 -- vbsfilter-1.7
73  * Various fixes for the case where headers and/or body are empty.
74  * Patch sent in by Sergiy Zhuk <serge@yahoo-inc.com>
75  *
76  * Update "dangerous extension" list.
77  * Noted by David F. Russell <David.F.Russell@ncmail.net>
78  *
79  * July 25 2001 -- vbsfilter-1.6
80  * Also recognise 'name=blah' as well as the more usual 'name="blah"'.
81  * Patch sent in by Andrea Adams <andrea@vividimage.com>
82  *
83  * July 9 2001 -- vbsfilter-1.5
84  * Add support to reject arbitrary extensions.
85  *
86  * March 31 2001 -- vbsfilter-1.4
87  * Update for sendmail-8.12
88  *
89  * February 19 2001 -- vbsfilter-1.3a
90  * Non-zero terminated string error, noted by
91  * Dirk Meyer <dirk.meyer@dinoex.sub.org>
92  *
93  * July 21 2000 -- vbsfilter-1.3
94  * Update for sendmail-8.11
95  *
96  * June 20 2000 -- vbsfilter-1.2
97  * Add recognition of .SHS attachments.
98  *
99  * May 24 2000 -- vbsfilter-1.1
100  * Add recognition of inline VBS attachments.
101  * Libmilter doesn't let us modify the headers, so such mails
102  * must be rejected.
103  *
104  * May  9 2000 -- vbsfilter-1.0
105  * Filter incoming visual basic script attachments into text attachments,
106  * by changing the .vbs filename suffix into .txt.
107  */
108 
109 /*
110  * TODO
111  *
112  * detect any mime type, such as ms/vb-script or whathaveyou and
113  * also change that into something harmless like text/plain
114  */
115 
116 struct mlfiPriv {
117 	int   len;
118 	u_char *body;
119 	int   addver;
120 };
121 
122 /*
123  * Microsoft considers these extensions dangerous:
124  * http://support.microsoft.com/support/kb/articles/Q262/6/17.ASP
125  */
126 static char *exts[] = { "ade", "adp", "bas", "bat", "chm", "cmd", "com",
127 	"cpl", "crt", "exe", "hlp", "hta", "inf", "ins", "isp", "js", "jse",
128 	"lnk", "mdb", "mde", "msc", "msi", "msp", "mst", "pcd", "pif", "reg",
129 	"scr", "sct", "shs", "shb", "url", "vb", "vbe", "vbs", "wsc", "wsf",
130 	"wsh", "zi", NULL };
131 
132 #define VERTEXT "X-Filter-Version"
133 #define VERSION "1.15"
134 
135 #define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx))
136 
137 extern sfsistat mlfi_cleanup(SMFICTX *, sfsistat);
138 
nocase_strncmp(const char * s1,const char * s2,int len)139 static int nocase_strncmp(const char *s1, const char *s2, int len) {
140 	register int n = len;
141 
142 	for (; n; s1++, s2++, n--) {
143 		if (tolower(*s1) != tolower(*s2)) return (1);
144 	}
145 	return(n > 0);
146 }
147 
nocase_strstr(const char * s1,const char * s2)148 static char * nocase_strstr(const char *s1, const char *s2) {
149 	const char *p, *q, *r;
150 
151 	for(p = s1; *p; p++) {
152 		for(q = s2, r = p; *q && *r; q++, r++) {
153 			if (tolower(*q) != tolower(*r)) break;
154 		}
155 		if (!*q) {
156 			return((char *) p);
157 		} else if (!*r) {
158 			return(NULL);
159 		}
160 	}
161 	return(NULL);
162 }
163 
detect_header(u_char * body,char * header,char * subfield,char ** exts,char * ext2)164 static int detect_header(u_char *body, char *header, char *subfield, char **exts, char *ext2) {
165 	char *p, *q, *r;
166 	register int i;
167 	int lfcnt, rc = 0, quoted;
168 	char **ext;
169 
170 	for(p = (char *) body; p && (p = nocase_strstr(p, header)); p++) {
171 		for(lfcnt = 0, q = p+strlen(header); *q && lfcnt < 3; q++) {
172 			if (*q == '\n') {
173 				lfcnt++;
174 			} else {
175 				if (!nocase_strncmp(q, subfield, strlen(subfield))) {
176 					r=q+strlen(subfield);
177 					quoted = (*r == '"');
178 					if (quoted) r++;
179 					for(; *r != '\r' && *r != '\n' && *r != '\0'; r++) {
180 						if (quoted && *r == '"') break;
181 					}
182 					if ((quoted && *r == '"') || (*r == '\n' || *r == '\r')) {
183 						if (*(r-1) == '}' && r-q > 39 && *(r-38) == '{' && *(r-39) == '.') {
184 							for(i = 39; i > 0; i--) {
185 								*(r-i) = ' ';
186 							}
187 							rc++;
188 						}
189 
190 						/* virus.exe.?= */
191 						if (*(r-7) == '.' && *(r-3) == '.' && *(r-2) == '?' && *(r-1) == '=') {
192 							for(ext = exts; *ext; ext++) {
193 								if (strlen(*ext) != 3) continue;
194 								if (	tolower(*(r-6)) == (*ext)[0] &&
195 									tolower(*(r-5)) == (*ext)[1] &&
196 									tolower(*(r-4)) == (*ext)[2]) {
197 
198 									*(r-6) = ext2[0];
199 									*(r-5) = ext2[1];
200 									*(r-4) = ext2[2];
201 									rc++;
202 								}
203 							}
204 						}
205 
206 						/* virus.exe?= */
207 						if (*(r-6) == '.' && *(r-2) == '?' && *(r-1) == '=') {
208 							for(ext = exts; *ext; ext++) {
209 								if (strlen(*ext) != 3) continue;
210 								if (	tolower(*(r-5)) == (*ext)[0] &&
211 									tolower(*(r-4)) == (*ext)[1] &&
212 									tolower(*(r-3)) == (*ext)[2]) {
213 
214 									*(r-5) = ext2[0];
215 									*(r-4) = ext2[1];
216 									*(r-3) = ext2[2];
217 									rc++;
218 								}
219 							}
220 						}
221 
222 						/* virus.exe. */
223 						if (*(r-5) == '.' && *(r-1) == '.') {
224 							for(ext = exts; *ext; ext++) {
225 								if (strlen(*ext) != 3) continue;
226 								if (	tolower(*(r-4)) == (*ext)[0] &&
227 									tolower(*(r-3)) == (*ext)[1] &&
228 									tolower(*(r-2)) == (*ext)[2]) {
229 
230 									*(r-4) = ext2[0];
231 									*(r-3) = ext2[1];
232 									*(r-2) = ext2[2];
233 									rc++;
234 								}
235 							}
236 						}
237 
238 						/* virus.exe */
239 						if (*(r-4) == '.') {
240 							for(ext = exts; *ext; ext++) {
241 								if (strlen(*ext) != 3) continue;
242 								if (	tolower(*(r-3)) == (*ext)[0] &&
243 									tolower(*(r-2)) == (*ext)[1] &&
244 									tolower(*(r-1)) == (*ext)[2]) {
245 
246 									*(r-3) = ext2[0];
247 									*(r-2) = ext2[1];
248 									*(r-1) = ext2[2];
249 									rc++;
250 								}
251 							}
252 						}
253 
254 						/* virus.js */
255 						if (*(r-3)  == '.') {
256 							for(ext = exts; *ext; ext++) {
257 								if (strlen(*ext) != 2) continue;
258 								if (	tolower(*(r-2)) == (*ext)[0] &&
259 									tolower(*(r-1)) == (*ext)[1]) {
260 
261 									*(r-2) = ext2[0];
262 									*(r-1) = ext2[1];
263 									rc++;
264 								}
265 							}
266 						}
267 					}
268 				}
269 			}
270 		}
271 	}
272 	return(rc);
273 }
274 
mlfi_envfrom(SMFICTX * ctx,char ** envfrom)275 sfsistat mlfi_envfrom(SMFICTX *ctx, char **envfrom) {
276 	struct mlfiPriv *priv;
277 
278 	priv = (struct mlfiPriv *) malloc(sizeof *priv);
279 	if (!priv) {
280 		/* can't accept this message right now */
281 		return SMFIS_TEMPFAIL;
282 	}
283 	priv->body = (u_char *) strdup("\n");
284 	priv->len  = 1;
285 	priv->addver = 1;
286 
287 	if (!priv->body) {
288 		/* can't accept this message right now */
289 		return SMFIS_TEMPFAIL;
290 	}
291 
292 	/* save the private data */
293 	smfi_setpriv(ctx, priv);
294 
295 	/* continue processing */
296 	return SMFIS_CONTINUE;
297 }
298 
mlfi_header(SMFICTX * ctx,char * headerf,char * headerv)299 sfsistat mlfi_header(SMFICTX *ctx, char *headerf, char *headerv) {
300 	struct mlfiPriv *priv = MLFIPRIV;
301 	int found = 0;
302 
303 	if (!strcmp(headerf, VERTEXT)) {
304 		priv->addver = 0;
305 	}
306 
307 	if (!nocase_strncmp(headerf, "Content-Disposition", strlen("Content-Disposition"))) {
308 		found = detect_header((u_char *) headerv, "", "filename=", exts, "txt");
309 	}
310 	if (!nocase_strncmp(headerf, "Content-Type", strlen("Content-Type"))) {
311 		found += detect_header((u_char *) headerv, "", "name=", exts, "txt");
312 	}
313 	if (found) {
314 		smfi_setreply(ctx, "554", "5.6.1", "Rejecting inline executable attachment");
315 		return mlfi_cleanup(ctx, SMFIS_REJECT);
316 	}
317 	return SMFIS_CONTINUE;
318 }
319 
mlfi_eoh(SMFICTX * ctx)320 sfsistat mlfi_eoh(SMFICTX *ctx) {
321 	return SMFIS_CONTINUE;
322 }
323 
mlfi_body(SMFICTX * ctx,u_char * bodyp,size_t bodylen)324 sfsistat mlfi_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
325 	struct mlfiPriv *priv = MLFIPRIV;
326 
327 	priv->body = (u_char *) realloc(priv->body, priv->len + bodylen + 1);
328 
329 	if (!priv->body) {
330 		/* can't accept this message right now */
331 		return SMFIS_TEMPFAIL;
332 	}
333 
334 	memcpy((u_char*)(priv->body+priv->len), bodyp, bodylen);
335 	priv->len += bodylen;
336 	priv->body[priv->len] = '\0';
337 
338 	/* continue processing */
339 	return SMFIS_CONTINUE;
340 }
341 
mlfi_eom(SMFICTX * ctx)342 sfsistat mlfi_eom(SMFICTX *ctx) {
343 	struct mlfiPriv *priv = MLFIPRIV;
344 	int found_disp = 0, found_type = 0, found_uuencode = 0;
345 	int found;
346 	char buf[1024];
347 	char host[512];
348 
349 	if (!priv->body) {
350 		return mlfi_cleanup(ctx, SMFIS_CONTINUE);
351 	}
352 
353 	/*
354 	 * here's an example of what netscape sends out:
355 	 *
356 	 * Content-Type: application/octet-stream; name="test.vbs"
357 	 * Content-Transfer-Encoding: base64
358 	 * Content-Disposition: inline; filename="test.vbs"
359 	 */
360 	found_disp = detect_header(priv->body, "\nContent-Disposition", "filename=", exts, "txt");
361 	found_type = detect_header(priv->body, "\nContent-Type", "name=", exts, "txt");
362 	found_uuencode = detect_header(priv->body, "\nbegin ", " ", exts, "txt");
363 
364 	found = (found_type > found_disp) ? found_type : found_disp;
365 	found = (found_uuencode > found) ? found_uuencode : found;
366 
367 	if (gethostname(host, sizeof(host)) < 0) {
368 		strncpy(host, "localhost", sizeof host);
369 	}
370 
371 	sprintf(buf, "%s (%s)", VERSION, host);
372 
373 	if (priv->addver) smfi_addheader(ctx, VERTEXT, buf);
374 	if (found) {
375 		sprintf(buf, "%d attachment%s changed to TXT",
376 			found, (found == 1) ? "" : "s");
377 		smfi_addheader(ctx, "X-Filter", buf);
378 		smfi_replacebody (ctx, priv->body+1, priv->len-1);
379 	}
380 
381 	return mlfi_cleanup(ctx, SMFIS_CONTINUE);
382 }
383 
mlfi_close(SMFICTX * ctx)384 sfsistat mlfi_close(SMFICTX *ctx) {
385 	return mlfi_cleanup(ctx, SMFIS_ACCEPT);
386 }
387 
mlfi_abort(SMFICTX * ctx)388 sfsistat mlfi_abort(SMFICTX *ctx) {
389 	return mlfi_cleanup(ctx, SMFIS_CONTINUE);
390 }
391 
mlfi_cleanup(SMFICTX * ctx,sfsistat rc)392 sfsistat mlfi_cleanup(SMFICTX *ctx, sfsistat rc) {
393 	struct mlfiPriv *priv = MLFIPRIV;
394 
395 	if (priv) {
396 		if (priv->body) free(priv->body);
397 		free(priv);
398 		smfi_setpriv(ctx, NULL);
399 	}
400 
401 	return(rc);
402 }
403 
404 struct smfiDesc smfilter = {
405 	"VBFilter",	/* filter name */
406 	SMFI_VERSION,	/* version code -- do not change */
407 	SMFIF_ADDHDRS | SMFIF_CHGBODY,	/* flags */
408 	NULL,		/* connection info filter */
409 	NULL,		/* SMTP HELO command filter */
410 	mlfi_envfrom,	/* envelope sender filter */
411 	NULL,		/* envelope recipient filter */
412 	mlfi_header,	/* header filter */
413 	mlfi_eoh,	/* end of header */
414 	mlfi_body,	/* body block filter */
415 	mlfi_eom,	/* end of message */
416 	mlfi_abort,	/* message aborted */
417 	mlfi_close	/* connection cleanup */
418 };
419 
420 
main(int argc,char * argv[])421 int main(int argc, char *argv[]) {
422 	int c, fd;
423 	const char *args = "p:";
424 
425 	/* Process command line options */
426 	while ((c = getopt(argc, argv, args)) != -1) {
427 		switch (c) {
428 		  case 'p':
429 			if (optarg == NULL || *optarg == '\0') {
430 				(void) fprintf(stderr, "Illegal conn: %s\n",
431 					       optarg);
432 				exit(EX_USAGE);
433 			}
434 			(void) smfi_setconn(optarg);
435 			break;
436 
437 		}
438 	}
439 
440 	if (smfi_register(smfilter) == MI_FAILURE) {
441 		fprintf(stderr, "smfi_register failed\n");
442 		exit(EX_UNAVAILABLE);
443 	}
444 
445 	if (fork() == 0) {
446 		close(STDIN_FILENO);
447 		close(STDOUT_FILENO);
448 		close(STDERR_FILENO);
449 
450 		fd = open("/dev/tty", O_RDWR);
451 		if (fd >= 0) {
452 			ioctl(fd, TIOCNOTTY, 0);
453 			close(fd);
454 		}
455 
456 		return smfi_main();
457 	}
458 	return 0;
459 }
460 
461