/* * Copyright (C) 2013-2019 Cisco Systems, Inc. and/or its affiliates. All rights reserved. * Copyright (C) 2008-2013 Sourcefire, Inc. * * Author: aCaB * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #if HAVE_CONFIG_H #include "clamav-config.h" #endif #include #include #include #include #include #include #include #include #include #include "libclamav/clamav.h" #include "shared/optparser.h" #include "shared/output.h" #include "libclamav/others.h" #include "connpool.h" #include "netcode.h" #include "whitelist.h" #include "clamfi.h" #if __GNUC__ >= 3 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 7) #define _UNUSED_ __attribute__ ((__unused__)) #else #define _UNUSED_ #endif uint64_t maxfilesize; static sfsistat FailAction; static sfsistat (*CleanAction)(SMFICTX *ctx); static sfsistat (*InfectedAction)(SMFICTX *ctx); static char *rejectfmt = NULL; int addxvirus = 0; /* 0 - don't add | 1 - replace | 2 - add */ char xvirushdr[255]; char *viraction = NULL; int multircpt = 1; #define LOGINF_NONE 0 #define LOGINF_BASIC 1 #define LOGINF_FULL 2 #define LOGCLN_BASIC 4 #define LOGCLN_FULL 8 int loginfected; #define CLAMFIBUFSZ 1424 static const char *HDR_UNAVAIL = "UNKNOWN"; static pthread_mutex_t virusaction_lock = PTHREAD_MUTEX_INITIALIZER; struct CLAMFI { const char *virusname; char *msg_subj; char *msg_date; char *msg_id; char **recipients; int local; int main; int alt; unsigned int totsz; unsigned int bufsz; unsigned int all_whitelisted; unsigned int gotbody; unsigned int scanned_count; unsigned int status_count; unsigned int nrecipients; uint32_t sendme; char buffer[CLAMFIBUFSZ]; }; static void add_x_header(SMFICTX *ctx, char *st, unsigned int scanned, unsigned int status) { if(addxvirus == 1) { /* Replace/Yes */ while(scanned) if(smfi_chgheader(ctx, (char *)"X-Virus-Scanned", scanned--, NULL) != MI_SUCCESS) logg("^Failed to remove existing X-Virus-Scanned header\n"); while(status) if(smfi_chgheader(ctx, (char *)"X-Virus-Status", status--, NULL) != MI_SUCCESS) logg("^Failed to remove existing X-Virus-Status header\n"); if(smfi_addheader(ctx, (char *)"X-Virus-Scanned", xvirushdr) != MI_SUCCESS) logg("^Failed to add X-Virus-Scanned header\n"); if(smfi_addheader(ctx, (char *)"X-Virus-Status", st) != MI_SUCCESS) logg("^Failed to add X-Virus-Status header\n"); } else { /* Add */ if(smfi_insheader(ctx, 1, (char *)"X-Virus-Scanned", xvirushdr) != MI_SUCCESS) logg("^Failed to insert X-Virus-Scanned header\n"); if(smfi_insheader(ctx, 1, (char *)"X-Virus-Status", st) != MI_SUCCESS) logg("^Failed to insert X-Virus-Status header\n"); } } enum CFWHAT { CF_NONE, /* 0 */ CF_MAIN, /* 1 */ CF_ALT, /* 2 */ CF_BOTH, /* 3 */ CF_ANY /* 4 */ }; static const char *makesanehdr(char *hdr) { char *ret = hdr; if(!hdr) return HDR_UNAVAIL; while(*hdr) { if(*hdr=='\'' || *hdr=='\t' || *hdr=='\r' || *hdr=='\n' || !isprint(*hdr)) *hdr = ' '; hdr++; } return ret; } static void nullify(SMFICTX *ctx, struct CLAMFI *cf, enum CFWHAT closewhat) { if(closewhat & CF_MAIN || ((closewhat & CF_ANY) && cf->main >= 0)) close(cf->main); if(closewhat & CF_ALT || ((closewhat & CF_ANY) && cf->alt >= 0)) close(cf->alt); if(cf->msg_subj) free(cf->msg_subj); if(cf->msg_date) free(cf->msg_date); if(cf->msg_id) free(cf->msg_id); if(multircpt && cf->nrecipients) { while(cf->nrecipients) { cf->nrecipients--; free(cf->recipients[cf->nrecipients]); } free(cf->recipients); } smfi_setpriv(ctx, NULL); } static sfsistat sendchunk(struct CLAMFI *cf, unsigned char *bodyp, size_t len, SMFICTX *ctx) { if(cf->totsz >= maxfilesize || len == 0) return SMFIS_CONTINUE; if(!cf->totsz) { sfsistat ret; if(nc_connect_rand(&cf->main, &cf->alt, &cf->local)) { logg("!Failed to initiate streaming/fdpassing\n"); nullify(ctx, cf, CF_NONE); return FailAction; } cf->totsz = 1; /* do not infloop */ if((ret = sendchunk(cf, (unsigned char *)"From clamav-milter\n", 19, ctx)) != SMFIS_CONTINUE) return ret; cf->totsz -= 1; } if(cf->totsz + len > maxfilesize) len = maxfilesize - cf->totsz; cf->totsz += len; if(cf->local) { while(len) { int n = write(cf->alt, bodyp, len); if (n==-1) { logg("!Failed to write temporary file\n"); nullify(ctx, cf, CF_BOTH); return FailAction; } len -= n; bodyp += n; } } else { int sendfailed = 0; if(len < CLAMFIBUFSZ - cf->bufsz) { memcpy(&cf->buffer[cf->bufsz], bodyp, len); cf->bufsz += len; } else if(len < CLAMFIBUFSZ) { memcpy(&cf->buffer[cf->bufsz], bodyp, CLAMFIBUFSZ - cf->bufsz); cf->sendme = htonl(CLAMFIBUFSZ); sendfailed = nc_send(cf->main, &cf->sendme, CLAMFIBUFSZ + 4); len -= (CLAMFIBUFSZ - cf->bufsz); memcpy(cf->buffer, &bodyp[CLAMFIBUFSZ - cf->bufsz], len); cf->bufsz = len; } else { uint32_t sendmetoo = htonl(len); cf->sendme = htonl(cf->bufsz); if((cf->bufsz && nc_send(cf->main, &cf->sendme, cf->bufsz + 4)) || nc_send(cf->main, &sendmetoo, 4) || nc_send(cf->main, bodyp, len)) sendfailed = 1; cf->bufsz = 0; } if(sendfailed) { logg("!Streaming failed\n"); nullify(ctx, cf, CF_NONE); return FailAction; } } return SMFIS_CONTINUE; } sfsistat clamfi_header(SMFICTX *ctx, char *headerf, char *headerv) { struct CLAMFI *cf; sfsistat ret; if(!(cf = (struct CLAMFI *)smfi_getpriv(ctx))) return SMFIS_CONTINUE; /* whatever */ if(!cf->totsz && cf->all_whitelisted) { logg("*Skipping scan (all destinations whitelisted)\n"); nullify(ctx, cf, CF_NONE); free(cf); return SMFIS_ACCEPT; } if(!headerf) return SMFIS_CONTINUE; /* just in case */ if((loginfected & (LOGINF_FULL | LOGCLN_FULL)) || viraction) { if(!cf->msg_subj && !strcasecmp(headerf, "Subject")) cf->msg_subj = strdup(headerv ? headerv : ""); if(!cf->msg_date && !strcasecmp(headerf, "Date")) cf->msg_date = strdup(headerv ? headerv : ""); if(!cf->msg_id && !strcasecmp(headerf, "Message-ID")) cf->msg_id = strdup(headerv ? headerv : ""); } if(addxvirus==1) { if(!strcasecmp(headerf, "X-Virus-Scanned")) cf->scanned_count++; if(!strcasecmp(headerf, "X-Virus-Status")) cf->status_count++; } if((ret = sendchunk(cf, (unsigned char *)headerf, strlen(headerf), ctx)) != SMFIS_CONTINUE) { free(cf); return ret; } if((ret = sendchunk(cf, (unsigned char *)": ", 2, ctx)) != SMFIS_CONTINUE) { free(cf); return ret; } if(headerv && (ret = sendchunk(cf, (unsigned char *)headerv, strlen(headerv), ctx)) != SMFIS_CONTINUE) { free(cf); return ret; } ret = sendchunk(cf, (unsigned char *)"\r\n", 2, ctx); if(ret != SMFIS_CONTINUE) free(cf); return ret; } sfsistat clamfi_body(SMFICTX *ctx, unsigned char *bodyp, size_t len) { struct CLAMFI *cf; sfsistat ret; if(!(cf = (struct CLAMFI *)smfi_getpriv(ctx))) return SMFIS_CONTINUE; /* whatever */ if(!cf->gotbody) { ret = sendchunk(cf, (unsigned char *)"\r\n", 2, ctx); if(ret != SMFIS_CONTINUE) { free(cf); return ret; } cf->gotbody = 1; } ret = sendchunk(cf, bodyp, len, ctx); if(ret != SMFIS_CONTINUE) free(cf); return ret; } sfsistat clamfi_abort(SMFICTX *ctx) { struct CLAMFI *cf; if((cf = (struct CLAMFI *)smfi_getpriv(ctx))) { nullify(ctx, cf, CF_ANY); free(cf); } return SMFIS_CONTINUE; } sfsistat clamfi_eom(SMFICTX *ctx) { struct CLAMFI *cf; char *reply; int len, ret; unsigned int crcpt; if(!(cf = (struct CLAMFI *)smfi_getpriv(ctx))) return SMFIS_CONTINUE; /* whatever */ if(!cf->totsz) { /* got no headers and no body */ logg("*Not scanning an empty message\n"); ret = CleanAction(ctx); nullify(ctx, cf, CF_NONE); free(cf); return ret; } if(cf->local) { lseek(cf->alt, 0, SEEK_SET); if(nc_sendmsg(cf->main, cf->alt) == -1) { logg("!FD send failed\n"); nullify(ctx, cf, CF_ALT); free(cf); return FailAction; } } else { uint32_t sendmetoo = 0; cf->sendme = htonl(cf->bufsz); if((cf->bufsz && nc_send(cf->main, &cf->sendme, cf->bufsz + 4)) || nc_send(cf->main, &sendmetoo, 4)) { logg("!Failed to flush STREAM\n"); nullify(ctx, cf, CF_NONE); free(cf); return FailAction; } } reply = nc_recv(cf->main); if(cf->local) close(cf->alt); cf->alt = -1; if(!reply) { logg("!No reply from clamd\n"); nullify(ctx, cf, CF_NONE); free(cf); return FailAction; } len = strlen(reply); if(len>5 && !strcmp(reply + len - 5, ": OK\n")) { if(addxvirus) add_x_header(ctx, "Clean", cf->scanned_count, cf->status_count); if(loginfected & LOGCLN_FULL) { const char *id = smfi_getsymval(ctx, "{i}"); const char *from = smfi_getsymval(ctx, "{mail_addr}"); const char *msg_subj = makesanehdr(cf->msg_subj); const char *msg_date = makesanehdr(cf->msg_date); const char *msg_id = makesanehdr(cf->msg_id); if(multircpt && cf->nrecipients) { for(crcpt = 0; crcpt < cf->nrecipients; crcpt++) logg("~Clean message %s from <%s> to <%s> with subject '%s' message-id '%s' date '%s'\n", id, from, cf->recipients[crcpt], msg_subj, msg_id, msg_date); } else { const char *to = smfi_getsymval(ctx, "{rcpt_addr}"); logg("~Clean message %s from <%s> to <%s> with subject '%s' message-id '%s' date '%s'\n", id, from, to ? to : HDR_UNAVAIL, msg_subj, msg_id, msg_date); } } else if(loginfected & LOGCLN_BASIC) { const char *from = smfi_getsymval(ctx, "{mail_addr}"); if(multircpt && cf->nrecipients) { for(crcpt = 0; crcpt < cf->nrecipients; crcpt++) logg("~Clean message from <%s> to <%s>\n", from, cf->recipients[crcpt]); } else { const char *to = smfi_getsymval(ctx, "{rcpt_addr}"); logg("~Clean message from <%s> to <%s>\n", from, to ? to : HDR_UNAVAIL); } } ret = CleanAction(ctx); } else if (len>7 && !strcmp(reply + len - 7, " FOUND\n")) { cf->virusname = NULL; if((loginfected & (LOGINF_BASIC | LOGINF_FULL)) || addxvirus || rejectfmt || viraction) { char *vir; reply[len-7] = '\0'; vir = strrchr(reply, ' '); if(vir) { unsigned int have_multi = (multircpt != 0 && cf->nrecipients); unsigned int lst_rcpt = (have_multi * (cf->nrecipients - 1)) + 1; vir++; if(rejectfmt) cf->virusname = vir; if(addxvirus) { char msg[255]; snprintf(msg, sizeof(msg), "Infected (%s)", vir); msg[sizeof(msg)-1] = '\0'; add_x_header(ctx, msg, cf->scanned_count, cf->status_count); } for(crcpt = 0; crcpt < lst_rcpt; crcpt++) { if(loginfected || viraction) { const char *from = smfi_getsymval(ctx, "{mail_addr}"); const char *to = have_multi ? cf->recipients[crcpt] : smfi_getsymval(ctx, "{rcpt_addr}"); if(!from) from = HDR_UNAVAIL; if(!to) to = HDR_UNAVAIL; if((loginfected & LOGINF_FULL) || viraction) { const char *id = smfi_getsymval(ctx, "{i}"); const char *msg_subj = makesanehdr(cf->msg_subj); const char *msg_date = makesanehdr(cf->msg_date); const char *msg_id = makesanehdr(cf->msg_id); if(!id) id = HDR_UNAVAIL; if(loginfected & LOGINF_FULL) logg("~Message %s from <%s> to <%s> with subject '%s' message-id '%s' date '%s' infected by %s\n", id, from, to, msg_subj, msg_id, msg_date, vir); if(viraction) { char er[256]; char *e_id = strdup(id); char *e_from = strdup(from); char *e_to = strdup(to); char *e_msg_subj = strdup(msg_subj); char *e_msg_date = strdup(msg_date); char *e_msg_id = strdup(msg_id); pid_t pid; logg("*VirusEvent: about to execute '%s' '%s' '%s' '%s' '%s' '%s' '%s' '%s'\n", viraction, vir, e_id, e_from, e_to, e_msg_subj, e_msg_id, e_msg_date); pthread_mutex_lock(&virusaction_lock); pid = fork(); if(!pid) { char * args[9]; /* avoid element is not computable at load time warns */ args[0]= viraction; args[1] = vir; args[2] = e_id; args[3] = e_from; args[4] = e_to; args[5] = e_msg_subj; args[6] = e_msg_id; args[7] = e_msg_date; args[8] = NULL; exit(execvp(viraction, args)); } else if(pid > 0) { int wret; pthread_mutex_unlock(&virusaction_lock); while((wret = waitpid(pid, &ret, 0)) == -1 && errno == EINTR); if(wret<0) logg("!VirusEvent: waitpid() failed: %s\n", cli_strerror(errno, er, sizeof(er))); else { if(WIFEXITED(ret)) logg("*VirusEvent: child exited with code %d\n", WEXITSTATUS(ret)); else if(WIFSIGNALED(ret)) logg("*VirusEvent: child killed by signal %d\n", WTERMSIG(ret)); else logg("*VirusEvent: child lost\n"); } } else { logg("!VirusEvent: fork failed: %s\n", cli_strerror(errno, er, sizeof(er))); } free(e_id); free(e_from); free(e_to); free(e_msg_subj); free(e_msg_date); free(e_msg_id); } } if(loginfected & LOGINF_BASIC) logg("~Message from <%s> to <%s> infected by %s\n", from, to, vir); } } } } ret = InfectedAction(ctx); } else { logg("!Unknown reply from clamd\n"); ret = FailAction; } nullify(ctx, cf, CF_MAIN); free(cf); free(reply); return ret; } sfsistat clamfi_connect(_UNUSED_ SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr) { while(1) { /* Postfix doesn't seem to honor passing a NULL hostaddr and hostname set to "localhost" for non-smtp messages (they still appear as SMTP messages from 127.0.0.1). Here's a small workaround. */ if(hostaddr) { if(islocalnet_sock(hostaddr)) { logg("*Skipping scan for %s (in LocalNet)\n", hostname); return SMFIS_ACCEPT; } break; } if(!strcasecmp(hostname, "localhost")) hostname = NULL; if(islocalnet_name(hostname)) { logg("*Skipping scan for %s (in LocalNet)\n", hostname ? hostname : "local"); return SMFIS_ACCEPT; } break; } return SMFIS_CONTINUE; } static int parse_action(char *action) { if(!strcasecmp(action, "Accept")) return 0; if(!strcasecmp(action, "Defer")) return 1; if(!strcasecmp(action, "Reject")) return 2; if(!strcasecmp(action, "Blackhole")) return 3; if(!strcasecmp(action, "Quarantine")) return 4; logg("!Unknown action %s\n", action); return -1; } static sfsistat action_accept(_UNUSED_ SMFICTX *ctx) { return SMFIS_ACCEPT; } static sfsistat action_defer(_UNUSED_ SMFICTX *ctx) { return SMFIS_TEMPFAIL; } static sfsistat action_reject(_UNUSED_ SMFICTX *ctx) { return SMFIS_REJECT; } static sfsistat action_blackhole(_UNUSED_ SMFICTX *ctx) { return SMFIS_DISCARD; } static sfsistat action_quarantine(SMFICTX *ctx) { if(smfi_quarantine(ctx, "quarantined by clamav-milter") != MI_SUCCESS) { logg("^Failed to quarantine message\n"); return SMFIS_TEMPFAIL; } return SMFIS_ACCEPT; } static sfsistat action_reject_msg(SMFICTX *ctx) { struct CLAMFI *cf; char buf[1024]; if(!rejectfmt || !(cf = (struct CLAMFI *)smfi_getpriv(ctx))) return SMFIS_REJECT; snprintf(buf, sizeof(buf), rejectfmt, cf->virusname); buf[sizeof(buf)-1] = '\0'; smfi_setreply(ctx, "550", "5.7.1", buf); return SMFIS_REJECT; } int init_actions(struct optstruct *opts) { const struct optstruct *opt; if(!(opt = optget(opts, "LogInfected"))->enabled || !strcasecmp(opt->strarg, "Off")) loginfected = LOGINF_NONE; else if(!strcasecmp(opt->strarg, "Basic")) loginfected = LOGINF_BASIC; else if(!strcasecmp(opt->strarg, "Full")) loginfected = LOGINF_FULL; else { logg("!Invalid setting %s for option LogInfected\n", opt->strarg); return 1; } if((opt = optget(opts, "LogClean"))->enabled) { if(!strcasecmp(opt->strarg, "Basic")) loginfected |= LOGCLN_BASIC; else if(!strcasecmp(opt->strarg, "Full")) loginfected |= LOGCLN_FULL; else if(strcasecmp(opt->strarg, "Off")) { logg("!Invalid setting %s for option LogClean\n", opt->strarg); return 1; } } if((opt = optget(opts, "VirusAction"))->enabled) viraction = strdup(opt->strarg); if((opt = optget(opts, "OnFail"))->enabled) { switch(parse_action(opt->strarg)) { case 0: FailAction = SMFIS_ACCEPT; break; case 1: FailAction = SMFIS_TEMPFAIL; break; case 2: FailAction = SMFIS_REJECT; break; default: logg("!Invalid action %s for option OnFail\n", opt->strarg); return 1; } } else FailAction = SMFIS_TEMPFAIL; if((opt = optget(opts, "OnClean"))->enabled) { switch(parse_action(opt->strarg)) { case 0: CleanAction = action_accept; break; case 1: CleanAction = action_defer; break; case 2: CleanAction = action_reject; break; case 3: CleanAction = action_blackhole; break; case 4: CleanAction = action_quarantine; break; default: logg("!Invalid action %s for option OnClean\n", opt->strarg); return 1; } } else CleanAction = action_accept; if((opt = optget(opts, "OnInfected"))->enabled) { switch(parse_action(opt->strarg)) { case 0: InfectedAction = action_accept; break; case 1: InfectedAction = action_defer; break; case 3: InfectedAction = action_blackhole; break; case 4: InfectedAction = action_quarantine; break; case 2: InfectedAction = action_reject_msg; if((opt = optget(opts, "RejectMsg"))->enabled) { const char *src = opt->strarg; char *dst, c; int gotpctv = 0; rejectfmt = dst = malloc(strlen(src) * 4 + 1); if(!dst) { logg("!Failed to allocate memory for RejectMsg\n"); return 1; } while ((c = *src++)) { if(!isprint(c)) { logg("!RejectMsg contains non printable characters\n"); free(rejectfmt); return 1; } *dst++ = c; if(c == '%') { if(*src == 'v') { if(gotpctv) { logg("!%%v may appear at most once in RejectMsg\n"); free(rejectfmt); return 1; } gotpctv |= 1; src++; *dst++ = 's'; } else { dst[0] = dst[1] = dst[2] = '%'; dst += 3; } } } *dst = '\0'; } break; default: logg("!Invalid action %s for option OnInfected\n", opt->strarg); return 1; } } else InfectedAction = action_quarantine; return 0; } sfsistat clamfi_envfrom(SMFICTX *ctx, char **argv) { struct CLAMFI *cf; const char *login = smfi_getsymval(ctx, "{auth_authen}"); if(login && smtpauthed(login)) { logg("*Skipping scan for authenticated user %s\n", login); return SMFIS_ACCEPT; } if(whitelisted(argv[0], 1)) { logg("*Skipping scan for %s (whitelisted from)\n", argv[0]); return SMFIS_ACCEPT; } if(!(cf = (struct CLAMFI *)malloc(sizeof(*cf)))) { logg("!Failed to allocate CLAMFI struct\n"); return FailAction; } cf->totsz = 0; cf->bufsz = 0; cf->main = cf->alt = -1; cf->all_whitelisted = 1; cf->gotbody = 0; cf->msg_subj = cf->msg_date = cf->msg_id = NULL; if(multircpt) { cf->recipients = NULL; cf->nrecipients = 0; } if(addxvirus==1) { cf->scanned_count = 0; cf->status_count = 0; } smfi_setpriv(ctx, (void *)cf); return SMFIS_CONTINUE; } sfsistat clamfi_envrcpt(SMFICTX *ctx, char **argv) { struct CLAMFI *cf; if(!(cf = (struct CLAMFI *)smfi_getpriv(ctx))) return SMFIS_CONTINUE; /* whatever */ if(cf->all_whitelisted) cf->all_whitelisted &= whitelisted(argv[0], 0); if(multircpt) { void *new_rcpt = realloc(cf->recipients, (cf->nrecipients + 1) * sizeof(*(cf->recipients))); unsigned int rcpt_cnt; if(!new_rcpt) { logg("!Failed to allocate array for new recipient\n"); nullify(ctx, cf, CF_ANY); free(cf); return FailAction; } cf->recipients = new_rcpt; rcpt_cnt = cf->nrecipients++; if(!(cf->recipients[rcpt_cnt] = strdup(argv[0]))) { logg("!Failed to allocate space for new recipient\n"); nullify(ctx, cf, CF_ANY); free(cf); return FailAction; } } return SMFIS_CONTINUE; } /* * Local Variables: * mode: c * c-basic-offset: 4 * tab-width: 8 * End: * vim: set cindent smartindent autoindent softtabstop=4 shiftwidth=4 tabstop=8: */