%PDF- %PDF-
Direktori : /usr/local/maldetect/internals/ |
Current File : //usr/local/maldetect/internals/functions |
## # Linux Malware Detect v1.6.6 # (C) 2002-2025, R-fx Networks <proj@r-fx.org> # (C) 2025, Ryan MacDonald <ryan@r-fx.org> # This program may be freely redistributed under the terms of the GNU GPL v2 ## # lbreakifs() { if [ "$1" == "set" ]; then IFS=$(echo -en "\n\b") else unset IFS fi } prerun() { startdir=$(pwd); if [ ! "$(whoami)" == "root" ]; then if [ -z "$scan_user_access" ] || [ "$scan_user_access" == "0" ]; then args="$@" if [[ "$args" =~ "modsec" ]]; then echo "1 maldet: OK" exit fi header echo "public scanning is currently disabled (scan_user_access=0), please contact your system administrator to enable scan_user_access in $cnffile." exit 1 fi pub=1 user="$(whoami)" quardir="$userbasedir/$user/quar" sessdir="$userbasedir/$user/sess" tmpdir="$userbasedir/$user/tmp" scan_tmpdir_paths="" hits_history="$sessdir/hits.hist" quar_history="$sessdir/quarantine.hist" clean_history="$sessdir/clean.hist" suspend_history="$sessdir/suspend.hist" monitor_scanned_history="$sessdir/monitor.scanned.hist" if [ ! -d "$userbasedir/$user/tmp" ]; then header echo "public scanning is enabled (scan_user_access=1) but paths do not exist, please contact your system administrator to run '$0 --mkpubpaths' or wait for cron.pub to execute in ~10 minutes." exit 1 fi maldet_log="$userbasedir/$user/event_log" clamscan_log="$userbasedir/$user/clamscan_log" mkdir -p $quardir $sessdir $tmpdir 2> /dev/null chmod 711 $userbasedir 2> /dev/null touch $maldet_log 2> /dev/null chown -R ${user}.root $userbasedir/$user 2> /dev/null chmod 750 $userbasedir/$user $quardir $sessdir $tmpdir 2> /dev/null chmod 640 $maldet_log 2> /dev/null cd $tmpdir else echo $ver > $lmd_version_file fi if [ ! -d "$sigdir" ]; then mkdir -p $sigdir chmod 755 $sigdir fi if [ ! -d "$logdir" ]; then mkdir -p $logdir chmod 755 $logdir fi if [ ! -d "$tmpdir" ]; then mkdir -p $tmpdir chmod 755 $tmpdir else chmod 755 $tmpdir fi if [ ! -d "$sessdir" ]; then mkdir -p $sessdir chmod 750 $sessdir fi if [ ! -d "$quardir" ]; then mkdir -p $quardir chmod 750 $quardir fi if [ -z "$md5sum" ]; then header echo "could not find required binary md5sum, aborting." exit 1 fi if [ -z "$od" ]; then header echo "could not find required binary od, aborting." exit 1 fi if [ -z "$find" ]; then header echo "could not find required binary find, aborting." exit 1 fi if [ -z "$perl" ]; then header echo "could not find required binary perl, aborting." exit 1 fi if [ "$email_alert" == "1" ] && [ ! -f "$mail" ] && [ ! -f "$sendmail" ]; then email_alert=0 fi if [ ! -f "$sig_user_hex_file" ]; then touch $sig_user_hex_file chmod 644 $sig_user_hex_file fi if [ ! -f "$sig_user_md5_file" ]; then touch $sig_user_md5_file chmod 644 $sig_user_md5_file fi if [ "$scan_hexfifo" == "1" ]; then mkfifo=`command -v mkfifo 2> /dev/null` if [ ! -f "$mkfifo" ]; then scan_hexfifo=0 else if [ -f "$mkfifo" ] && [ ! -p "$hex_fifo_path" ]; then $mkfifo -m 666 $hex_fifo_path fi fi fi if [ "$user" == "root" ]; then $sed -i -e '/^$/d' $ignore_paths $ignore_sigs $ignore_inotify $ignore_file_ext fi if [ -z "$EDITOR" ]; then defedit=`command -v nano 2> /dev/null` if [ -z "$defedit" ]; then EDITOR=vi else EDITOR=nano fi fi if [ ! "$scan_cpunice" ]; then scan_cpunice=19 fi if [ ! "$scan_ionice" ]; then scan_ionice=6 fi if [ -f "$nice" ]; then nice_command="$nice -n $scan_cpunice" fi if [ -f "$ionice" ] && [ ! -d "/proc/vz" ]; then nice_command="$nice_command $ionice -c2 -n $scan_ionice" fi if [ -f "$cpulimit" ] && [ "$scan_cpulimit" -gt 2> /dev/null "0" ]; then max_cpulimit=$[$(grep -E -w processor /proc/cpuinfo -c)*100] if [ "$scan_cpulimit" -gt "$max_cpulimit" ]; then scan_cpulimit="0" else nice_command="$cpulimit -l $scan_cpulimit -- $nice_command" fi fi if [ -z "$cron_daily_scan" ]; then cron_daily_scan=1 fi } eout() { msg="$1" stdout="$2" appn=maldet if [ ! -d "$logdir" ]; then mkdir -p $logdir ; chmod 700 $logdir fi if [ ! -f "$maldet_log" ]; then touch $maldet_log fi if [ "$maldet_log_truncate" == "1" ]; then log_size=`$wc -l $maldet_log | awk '{print$1}'` if [ "$log_size" -ge "20000" ]; then trim=1000 printf "%s\n" "$trim,${log_size}d" w | ed -s $maldet_log 2> /dev/null fi fi if [ ! "$msg" == "" ]; then echo "$(date +"%b %d %Y %H:%M:%S") $(hostname -s) $appn($$): $msg" >> $maldet_log if [ ! -z "$stdout" ]; then echo "$appn($$): $msg" fi fi } trap_exit() { if [ "$svc" == "m" ]; then echo eout "{glob} monitor interrupt by user, sending kill." 1 monitor_kill exit 1 elif [ "$svc" == "a" ] || [ "$svc" == "r" ] || [ "$svc" == "f" ]; then echo gen_report if [ ! "$tot_hits" == "0" ]; then if [ "$email_ignore_clean" == "1" ] && [ ! "$tot_hits" == "$tot_cl" ]; then genalert file $nsess elif [ "$email_ignore_clean" == "0" ]; then genalert file $nsess fi fi mv $scan_session $nsess_hits 2> /dev/null rm -f $clamscan_results $find_results $runtime_hdb $runtime_hexstrings $runtime_ndb $scan_session $tmpdir/.find_killed.$scanid $tmpdir/.tmp* $tmpdir/.tmpf* $tmpf 2> /dev/null eout "{glob} scan interrupt by user, aborting scan..." 1 eout "{scan} scan report saved, to view run: maldet --report $datestamp.$$" 1 if [ "$quarantine_hits" == "0" ] && [ ! "$tot_hits" == "0" ]; then eout "{glob} quarantine is disabled! set quarantine_hits=1 in $cnffile or to quarantine results run: maldet -q $datestamp.$$" 1 fi exit fi } clean_exit() { mv -f $scan_session $nsess_hits 2> /dev/null rm -f $clamscan_results $find_results $list $runtime_hdb $runtime_hexstrings $runtime_ndb $scan_session $tmpdir/.find_killed.$scanid $tmpdir/.tmp* $tmpdir/.tmpf* $tmpf 0 2> /dev/null } detect_control_panel() { if [[ -d /usr/local/interworx ]]; then iworx_db_ps=$(ps -u iworx | grep iworx-db) iworx_web_ps=$(ps -u iworx-web | grep iworx-web) pex_script=$(readlink -e /usr/local/interworx/bin/listaccounts.pex) siteworx=$(command -v siteworx) # Check that Iworx services are running if [[ -z "$iworx_db_ps" || -z "$iworx_web_ps" ]]; then control_panel="error" eout "{panel} Interworx found, but not running. Panel user alerts will not be sent." # Verify pex script exists and is executable elif ! [[ -x "$pex_script" ]]; then control_panel="error" eout "{panel} Interworx found, but scripts are missing or not executable. Panel user alerts will not be sent." # Ensure /usr/bin/siteworx is executable elif ! [[ -x "$siteworx" ]]; then control_panel="error" eout "{panel} Interworx found, but Siteworx CLI is missing or not executable. Panel user alerts will not be sent." else control_panel="interworx" fi elif [[ -d /usr/local/cpanel ]]; then cpanel_ps=$(ps -u root | grep [c]psrvd) cpapi=$(command -v cpapi2) apitool=$(readlink -e ${cpapi}) # Ensure cpanel service is running if [[ -z ${cpanel_ps} ]]; then control_panel="error" eout "{panel} cPanel found, but services are not running. Panel user alerts will not be sent." # Verify apitool is executable elif ! [[ -x ${apitool} ]]; then control_panel="error" eout "{panel} cPanel found, but apitool is missing or not found. Panel user alerts will not be sent." else control_panel="cpanel" fi else control_panel="unknown" fi } get_panel_contacts() { panel="$1" user="$2" case "$panel" in "cpanel") if [ -f /var/cpanel/users/${user} ]; then contact_emails=$(awk -F '=' '/^CONTACTEMAIL/{print $2}' /var/cpanel/users/${user} | sed '/^ *$/d' | tr '\n' ',' | sed 's/,$//') else contact_emails=$(cpapi2 --user=herpaderpadoo --output=xml CustInfo contactemails | grep -o 'value>.*</value' | sed -r 's,(</)?value>?,,g;/^$/d' | tr '\n' ',' | sed 's/,$//') fi ;; "interworx") master_domain=$(/usr/local/interworx/bin/listaccounts.pex | grep "${user}" | awk '{print $2}') contact_emails=$(siteworx -un --login_domain ${master_domain} -c Users -a listUsers -o json | python -c 'import json,sys;data=json.load(sys.stdin);emails=[elem["email"] for elem in data if elem["user_status"] == "active"];print(", ".join(emails))') ;; esac } get_remote_file() { # $1 = URI, $2 = local service identifier, $3 boolean verbose get_uri="$1" service="$2" verbose="$3" save_path="$4" unset return_file if [ "$hscan" ]; then unset verbose fi if [ -z "$get_uri" ]; then eout "{internal} missing or invalid URI passed to get_remote_file()" 1 break fi if [ -z "$service" ]; then svc="internal" else svc="$service" fi if [ -f "$curl" ] || [ -f "/usr/bin/curl" ]; then get_type="curl" if [ -z "$curl" ]; then get_bin="/usr/bin/curl" else get_bin="$curl" fi elif [ -f "$wget" ] || [ -f "/usr/bin/wget" ]; then get_type="wget" if [ -z "$wget" ]; then get_bin="/usr/bin/wget" else get_bin="$wget" fi else eout "{internal} could not find curl or wget binaries for remote file downloads, fatal error!" exit 1 fi if [ "$lmd_referer" ] && [ "$get_type" == "curl" ]; then id="--referer ${lmd_referer}:curl" elif [ "$lmd_referer" ] && [ "$get_type" == "wget" ]; then id="--referer=${lmd_referer}:wget" fi if [ "$get_type" == "curl" ]; then if [ "$web_proxy" ]; then get_proxy_arg="-x http://$web_proxy" fi get_opts="-s $get_proxy_arg --connect-timeout $remote_uri_timeout --retry $remote_uri_retries $id" get_output_arg='-o' elif [ "$get_type" == "wget" ]; then if [ "$web_proxy" ]; then get_proxy_arg="-e http_proxy=$web_proxy -e https_proxy=$web_proxy" fi get_opts="-q $get_proxy_arg --timeout=$remote_uri_timeout --tries=$remote_uri_retries $id" get_output_arg='-O' fi if [ "$save_path" ]; then tmpf="$save_path" else tmpf="$tmpdir/.tmpf_get.${RANDOM}" touch $tmpf ; chmod 600 $tmpf get_file=`echo "$get_uri" | tr '/' '\n' | tail -n1` fi $get_bin $get_opts "$get_uri" $get_output_arg "$tmpf" || get_return=$? if [ ! -f "$tmpf" ] || [ ! -s "$tmpf" ]; then eout "{$svc} could not download $get_uri, please try again later." $verbose unset return_file else eout "{$svc} downloaded $get_uri" return_file="$tmpf" fi } import_user_sigs() { if [ "$import_custsigs_md5_url" ]; then get_remote_file "$import_custsigs_md5_url" "importsigs" "1" if [ -f "$return_file" ]; then cp -f $return_file $sig_user_md5_file eout "{importsigs} imported custom signature data from $import_custsigs_md5_url" fi fi if [ "$import_custsigs_hex_url" ]; then get_remote_file "$import_custsigs_hex_url" "importsigs" "1" if [ -f "$return_file" ]; then cp -f $return_file $sig_user_hex_file eout "{importsigs} imported custom signature data from $import_custsigs_hex_url" fi fi } import_conf() { current_utime=`date +"%s"` if [ -z "$import_config_expire" ]; then import_config_expire=43200 fi if [ -f "$sessdir/.import_conf.utime" ]; then import_utime=`cat $sessdir/.import_conf.utime` if [ -z "$import_utime" ]; then import_utime="0" fi import_diff=$[current_utime-import_utime] if [ "$import_diff" -lt "$import_config_expire" ]; then import_config_skip="1" eout "{importconf} configuration expire value has not lapsed (${import_diff}/${import_config_expire}), using cache." import_conf_cached=1 fi fi if [ "$import_config_url" ]; then if [ -z "$import_config_skip" ]; then get_remote_file "$import_config_url" "importconf" "1" if [ -f "$return_file" ]; then cp -f $return_file $sessdir/.import_conf.cache echo "$current_utime" > $sessdir/.import_conf.utime fi fi if [ -f "$sessdir/.import_conf.cache" ]; then source $cnf source $intcnf source $sessdir/.import_conf.cache if [ "$import_conf_cached" ]; then eout "{importconf} imported configuration from $import_config_url (cached)" else eout "{importconf} imported configuration from $import_config_url" fi fi fi } clamav_linksigs() { cpath="$1" if [ -d "$cpath" ]; then rm -f $cpath/rfxn.{hdb,ndb,yara} 2> /dev/null ; cp -f $sigdir/rfxn.{hdb,ndb,yara} $cpath/ 2> /dev/null rm -f $cpath/lmd.user.* 2> /dev/null ; cp -f $sigdir/lmd.user.ndb $sigdir/lmd.user.hdb $cpath/ 2> /dev/null fi } usage_short() { cat <<EOF signature set: $sig_version usage maldet [-h|--help] [-a|--scan-all PATH] [-r|--scan-recent PATH DAYS] [-f|--file-list PATH] [-i|--include-regex] [-x|--exclude-regex] [-b|--background] [-m|--monitor] [-k|--kill-monitor] [-c|--checkout] [-q|--quarantine] [-s|--restore] [-n|--clean] [-l|--log] [-e|--report] [-u|--update-sigs] [-d|--update-ver] EOF } usage_long() { cat<<EOF signature set: $sig_version usage $0 [ OPTION ] -b, --background Execute operations in the background, ideal for large scans e.g: maldet -b -r /home/?/public_html 7 -u, --update-sigs [--force] Update malware detection signatures from rfxn.com -d, --update-ver [--force] Update the installed version from rfxn.com -f, --file-list Scan files or paths defined in line spaced file e.g: maldet -f /root/scan_file_list -r, --scan-recent PATH DAYS Scan files created/modified in the last X days (default: 7d, wildcard: ?) e.g: maldet -r /home/?/public_html 2 -a, --scan-all PATH Scan all files in path (default: /home, wildcard: ?) e.g: maldet -a /home/?/public_html -i, --include-regex REGEX Include paths/files from file list based on supplied posix-egrep regular expression. e.g: To include only paths named wp-content and files ending in .php: --include-regex ".*/wp-content/.*|.*.php$" -x, --exclude-regex REGEX Exclude paths/files from file list based on supplied posix-egrep regular expression. e.g: To exclude paths containing 'wp-content/w3tc/' and core files: --exclude-regex ".*wp-content/w3tc/.*|.*core.[0-9]+$" -m, --monitor USERS|PATHS|FILE|RELOAD Run maldet with inotify kernel level file create/modify monitoring If USERS is specified, monitor user homedirs for UID's > 500 If FILE is specified, paths will be extracted from file, line spaced If PATHS are specified, must be comma spaced list, NO WILDCARDS! e.g: maldet --monitor users e.g: maldet --monitor /root/monitor_paths e.g: maldet --monitor /home/mike,/home/ashton -k, --kill-monitor Terminate inotify monitoring service -c, --checkout FILE Upload suspected malware to rfxn.com for review & hashing into signatures -l, --log View maldet log file events -e, --report SCANID email View scan report of most recent scan or of a specific SCANID and optionally e-mail the report to a supplied e-mail address e.g: maldet --report e.g: maldet --report list e.g: maldet --report 050910-1534.21135 e.g: maldet --report SCANID user@domain.com -s, --restore FILE|SCANID Restore file from quarantine queue to orginal path or restore all items from a specific SCANID e.g: maldet --restore $varlibpath/quarantine/config.php.23754 e.g: maldet --restore 050910-1534.21135 -q, --quarantine SCANID Quarantine all malware from report SCANID e.g: maldet --quarantine 050910-1534.21135 -n, --clean SCANID Try to clean & restore malware hits from report SCANID e.g: maldet --clean 050910-1534.21135 -U, --user USER Set execution under specified user, ideal for restoring from user quarantine or to view user reports. e.g: maldet --user nobody --report e.g: maldet --user nobody --restore 050910-1534.21135 -co, --config-option VAR1=VALUE,VAR2=VALUE,VAR3=VALUE Set or redefine the value of $cnffile config options e.g: maldet --config-option email_addr=you@domain.com,quarantine_hits=1 -p, --purge Clear logs, quarantine queue, session and temporary data. --web-proxy IP:PORT Enable use of HTTP/HTTPS proxy for all remote URL calls. EOF } clean() { file="$1" file_signame="$2" file_owner="$3" file_chmod="$4" file_size="$5" file_md5="$6" sh_hitname=`echo $hitname | sed -r -e 's/\{(HEX|MD5|CAV|YARA)\}//' -e 's/\.[0-9]+$//'` if [ -d "$cldir" ] && [ "$quarantine_clean" == "1" ] && [ "$quarantine_hits" == "1" ] && [ -f "$file" ]; then if [ -f "$cldir/$sh_hitname" ] || [ -f "$cldir/custom.$sh_hitname" ] && [ -f "${file}.info" ]; then file_path=`grep -E -v '\#' ${file}.info | cut -d':' -f9` eout "{clean} restoring $file for cleaning attempt" 1 restore "$file" >> /dev/null 2>&1 if [ -f "$cldir/$sh_hitname" ]; then eout "{clean} attempting to clean $file_path with $sh_hitname rule" 1 $cldir/$sh_hitname "$file_path" "$file_signame" "$file_owner" "$file_chmod" "$file_size" "$file_md5" fi if [ -f "$cldir/custom.$sh_hitname" ]; then eout "{clean} attempting to clean $file_path with custom.$sh_hitname rule" 1 $cldir/custom.$sh_hitname "$file_path" "$file_signame" "$file_owner" "$file_chmod" "$file_size" "$file_md5" fi eout "{clean} rescanning $file_path for malware hits" 1 clean_state="1" scan_stage1 "$file_path" >> /dev/null 2>&1 unset clean_state if [ -f "$file_path" ]; then echo "$file_path" >> $sessdir/clean.$$ echo "$file_path" >> $clean_history eout "{clean} clean successful on $file_path" 1 else eout "{clean} clean failed on $file_path and returned to quarantine" 1 fi elif [ -f "$cldir/$sh_hitname" ] || [ -f "$cldir/custom.$sh_hitname" ] && [ -f "$file" ]; then file_path="$file" if [ -f "$cldir/$sh_hitname" ]; then eout "{clean} attempting to clean $file with $sh_hitname rule" 1 $cldir/$sh_hitname "$file_path" fi if [ -f "$cldir/custom.$sh_hitname" ]; then eout "{clean} attempting to clean $file with custom.$sh_hitname rule" 1 $cldir/custom.$sh_hitname "$file_path" fi eout "{clean} scanning $file for malware hits" clean_state="1" unset clean_failed scan_stage1 "$file_path" 1 >> /dev/null 2>&1 unset clean_state if [ "$clean_failed" == "1" ]; then eout "{clean} clean failed on $file" 1 else echo "$file" >> $sessdir/clean.$$ echo "$file_path" >> $clean_history eout "{clean} clean successful on $file" 1 fi else eout "{clean} could not find clean rule for hit $sh_hitname or file $file no longer exists." 1 fi else if [ "$quarantine_clean" == "1" ] && [ "$quarantine_hits" == "1" ]; then eout "file path error on $file, aborting." exit else eout "quarantine_clean and quarantine_hits are disabled; skipped file $file" fi fi } quar_get_filestat() { fstat="$1" if [ -f "$fstat" ]; then # owner:group:mode:size(b):md5:atime(epoch):mtime(epoch):ctime(epoch):file(path) file_owner=`grep -E -v '\#' "$fstat" | awk -F':' '{print$1}'` file_group=`grep -E -v '\#' "$fstat" | awk -F':' '{print$2}'` file_mode=`grep -E -v '\#' "$fstat" | awk -F':' '{print$3}'` file_size=`grep -E -v '\#' "$fstat" | awk -F':' '{print$4}'` md5_hash=`grep -E -v '\#' "$fstat" | awk -F':' '{print$5}'` file_atime=`grep -E -v '\#' "$fstat" | awk -F':' '{print$6}'` file_mtime=`grep -E -v '\#' "$fstat" | awk -F':' '{print$7}'` file_ctime=`grep -E -v '\#' "$fstat" | awk -F':' '{print$8}'` file_path=`grep -E -v '\#' "$fstat" | cut -d':' -f9` fi } restore() { file="$1" fname=`basename "$file"` if [ -f "$quardir/$file" ] && [ -f "$quardir/${file}.info" ]; then quar_get_filestat "$quardir/${file}.info" chown ${file_owner}.${file_group} "$quardir/$file" >> /dev/null 2>&1 chmod $file_mode "$quardir/$file" >> /dev/null 2>&1 mv -f "$quardir/$file" "$file_path" touch -m --date=@${file_mtime} "$file_path" eout "{restore} quarantined file '$file' restored to '$file_path'" 1 elif [ -f "$file" ] && [ -f "${file}.info" ]; then quar_get_filestat "${file}.info" chown ${file_owner}.${file_group} "$file" >> /dev/null 2>&1 chmod $file_mode "$file" >> /dev/null 2>&1 mv -f "$file" "$file_path" touch -m --date=@${file_mtime} "$file_path" eout "{restore} quarantined file '$file' restored to '$file_path'" 1 else eout "{restore} '$file' is not eligible for restore or could not be found" 1 fi } restore_hitlist() { hitlist="$sessdir/session.hits.$1" if [ -f "$hitlist" ]; then lbreakifs set is_autoquar=`tail -n1 $hitlist | awk -F'>' '{print$2}' | grep -E -v '^$' | sed 's/.//'` if [ "$is_autoquar" ]; then for file in `cat $hitlist | cut -d':' -f2 | cut -d'>' -f2 | sed 's/.//'`; do if [ -f "$file" ]; then restore "$file" fi done elif [ ! "$is_autoquar" ]; then for file in `cat $hitlist | cut -d':' -f2 | sed 's/.//'`; do quar_file=`cat $quar_history | grep -E -w "$file" | cut -d':' -f8 | tail -n1` restore "$quar_file" done else eout "{restore} could not find a valid hit list to restore." 1 fi lbreakifs unset fi } clean_hitlist() { if [ "$quarantine_clean" == "0" ] || [ "$quarantine_hits" == "0" ]; then eout "{clean} quarantine_clean and/or quarantine_hits are disabled, nothing to do here." 1 exit 0 fi scanid="$1" hitlist="$sessdir/session.hits.$scanid" if [ -f "$hitlist" ]; then is_quared=`egrep '=>' $hitlist` if [ ! "$is_quared" ]; then lbreakifs set for file in `cat $hitlist | cut -d':' -f2 | sed 's/.//'`; do get_filestat "$file" 1 hitname=`cat $hitlist | grep $file | awk '{print$1}'` if [ ! "$md5_hash" ]; then md5_hash="$($md5sum "$file" | awk '{print$1}')" fi clean "$file" "$hitname" "$file_owner.$file_group" "$file_mode" "$file_size" "$md5_hash" done lbreakifs unset else lbreakifs set for file in `cat $hitlist | cut -d'>' -f2 | sed 's/.//'`; do quar_get_filestat "${file}.info" 1 hitname=`cat $hitlist | grep $file | awk '{print$1}'` if [ ! "$md5_hash" ]; then md5_hash="$($md5sum "$file" | awk '{print$1}')" fi clean "$file" "$hitname" "$file_owner.$file_group" "$file_mode" "$file_size" "$md5_hash" done lbreakifs unset fi else eout "{clean} invalid scanid $scanid or unknown error, aborting." 1 exit fi } view_report() { rid="$1" if [ "$rid" == "list" ]; then tmpf="$tmpdir/.areps$$" for file in `ls $sessdir/session.[0-9]* 2> /dev/null`; do SCANID=`cat $file | grep "SCAN ID" | sed 's/SCAN ID/SCANID/'` FILES=`cat $file | grep "TOTAL FILES" | sed 's/TOTAL //'` HITS=`cat $file | grep "TOTAL HITS" | sed 's/TOTAL //'` CLEAN=`cat $file | grep "TOTAL CLEANED" | sed 's/TOTAL //'` TIME=`cat $file | grep -E "^TIME|^STARTED" | sed -e 's/TIME: //' -e 's/STARTED: //' | awk '{print$1,$2,$3,$4}'` TIME_U=`date -d "$TIME" "+%s" 2> /dev/null` ETIME=`cat $file | grep "ELAPSED" | awk '{print$1,$2}' | sed 's/ELAPSED/RUNTIME/'` if [ -z "$ETIME" ]; then ETIME="RUNTIME: unknown" fi if [ ! -z "$SCANID" ] && [ ! -z "$TIME" ]; then clean_zero=`echo $CLEAN | awk '{print$2}'` if [ -z "$clean_zero" ]; then CLEAN="CLEANED: 0" fi echo "$TIME_U | $TIME | $SCANID | $ETIME | $FILES | $HITS | $CLEAN" >> $tmpf fi done if [ -f "$tmpf" ]; then if [ "$OSTYPE" == "FreeBSD" ]; then cat $tmpf | sort -k1 -n | cut -d'|' -f2-7 | column -t | more else cat $tmpf | sort -k1 -n | tac | cut -d'|' -f2-7 | column -t | more fi rm -f $tmpf 2> /dev/null exit 0 else echo "error no report data found" exit 1 fi fi if [ -f "$sessdir/session.$rid" ] && [ ! -z "$(echo $2 | grep '\@')" ]; then if [ -f "$mail" ]; then cat $sessdir/session.$rid | $mail -s "$email_subj" "$2" elif [ -f "$sendmail" ]; then if ! grep -q "SUBJECT: " "$sessdir/session.$rid"; then echo -e "SUBJECT: $email_subj\n$(cat $sessdir/session.$rid)" > $sessdir/session.$rid fi cat $sessdir/session.$rid | $sendmail -t "$2" else eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled." exit fi eout "{report} report ID $rid sent to $2" 1 exit fi if [ "$rid" == "" ] && [ -f "$sessdir/session.last" ]; then rid=`cat $sessdir/session.last` if [ "$2" == "dump" ]; then cat $sessdir/session.$rid exit fi $EDITOR $sessdir/session.$rid elif [ -f "$sessdir/session.$rid" ]; then if [ "$2" == "dump" ]; then cat $sessdir/session.$rid exit fi $EDITOR $sessdir/session.$rid else echo "{report} no report found, aborting." exit fi } dump_report() { rid="$1" if [ "$rid" == "" ]; then rid=`cat $sessdir/session.last` fi view_report $rid dump } view() { echo "Viewing last 50 lines from $maldet_log:" tail -n 50 $maldet_log } purge() { :> $maldet_log log_size=`$wc -l $inotify_log | awk '{print$1}'` if [ "$inotify_trim" ]; then trim=$(($log_size - 1000)) log_chars=`printf "%s\n" "1,${trim}p" | ed -s $inotify_log 2> /dev/null | wc -c` tlog_new=$(( `cat $inspath/tmp/inotify` - $log_chars )) echo $tlog_new > $inspath/tmp/inotify printf "%s\n" "1,${trim}d" w | ed -s $inotify_log 2> /dev/null eout "{mon} inotify log file trimmed" fi rm -f $tmpdir/* $quardir/* $sessdir/* 2> /dev/null eout "{glob} logs and quarantine data cleared by user request (-p)" 1 } quarantine_suspend_user() { file="$1" get_filestat "$file" user="$file_owner" user_id=`id -u $user` if [ ! "$user" == "" ] && [ "$user_id" -ge "$quarantine_suspend_user_minuid" ]; then if [ -f "/scripts/suspendacct" ]; then if [ ! -f "/var/cpanel/suspended/$user" ]; then /scripts/suspendacct $user "maldet --report $datestamp.$$" >> /dev/null 2>&1 eout "{quar} account $user cpanel suspended" 1 echo "$user" >> $sessdir/suspend.users.$$ echo "$user" >> $suspend_history fi else if [ "$(grep $user /etc/passwd | cut -d':' -f7 | grep /bin/false)" == "" ]; then /usr/sbin/usermod -s /bin/false $user >> /dev/null 2>&1 eout "{quar} account $user suspended; set 'usermod -s /bin/false'" echo "$user" >> $sessdir/suspend.users.$$ echo "$user" >> $suspend_history fi fi fi } get_filestat() { file="$1" times="$2" if [ "$OSTYPE" == "FreeBSD" ]; then file_owner=`$stat -f '%Su' "$file"` file_group=`$stat -f '%Sg' "$file"` file_mode=`$stat -f '%p' "$file" | sed 's/^.//'` file_size=`$stat -f '%Z' "$file"` md5_hash="$($md5sum "$file" | awk '{print$1}')" if [ "$times" ]; then # atime mtime ctime (since epoch) file_times=`$stat -f '%a:%m:%c' "$file"` fi else file_owner=`$stat -c '%U' "$file"` file_group=`$stat -c '%G' "$file"` file_mode=`$stat -c '%a' "$file"` file_size=`$stat -c '%s' "$file"` md5_hash="$($md5sum "$file" | awk '{print$1}')" if [ "$times" ]; then # atime mtime ctime (since epoch) file_times=`$stat -c '%X:%Y:%Z' "$file"` fi fi } record_hit() { file="$1" hitname="$2" if [ -f "$file" ]; then get_filestat "$file" 1 file_name=`basename "$file" 2> /dev/null` if [ ! "$md5_hash" ]; then md5_hash="$($md5sum "$file" | awk '{print$1}')" fi eout "{hit} malware hit $hitname found for $file" echo "${utime}:${hostid}:${hitname}:${md5_hash}:${file_size}:${file_owner}.${file_group}:${file_mode}:${file_times}:${file}" >> $hits_history fi } quarantine() { file="$1" hitname="$2" file_name=`basename "$file"` if [ -f "$file" ] && [ -d "$quardir" ]; then if [ "$quarantine_hits" == "1" ]; then file_namewc=`echo $file_name | $wc -m` rnd="${RANDOM}${RANDOM}" if [ "$quarantine_suspend_user" == "1" ]; then quarantine_suspend_user "$file" fi chattr -ia "$file" mv "$file" "$quardir/$file_name.$rnd" touch --no-create "$quardir/$file_name.$rnd" if [ "$pub" == "1" ]; then chmod 400 "$quardir/$file_name.$rnd" else chmod 000 "$quardir/$file_name.$rnd" chown root.root "$quardir/$file_name.$rnd" fi echo -e "# owner:group:mode:size(b):md5:atime(epoch):mtime(epoch):ctime(epoch):file(path)\n$file_owner:$file_group:$file_mode:$file_size:$md5_hash:$file_times:$file" > $quardir/$file_name.$rnd.info eout "{quar} malware quarantined from '$file' to '$quardir/$file_name.$rnd'" echo "$utime:$hitname:$file:$file_owner:$file_group:$md5_hash:$file_size:$quardir/$file_name.$rnd" >> $quar_history if [ ! -z "$scan_session" ]; then echo "$hitname : $file => $quardir/$file_name.$rnd" >> $scan_session fi if [ "$quarantine_clean" == "1" ] && [ ! "$clean_state" == "1" ]; then unset clean_state clean "$quardir/$file_name.$rnd" "$hitname" "$file_owner.$file_group" "$file_mode" "$file_size" "$md5_hash" "$file" fi else if [ ! -z "$scan_session" ]; then echo "$hitname : $file" >> $scan_session fi fi else eout "{quar} fatal error handling '$file'" fi } quar_hitlist() { hitlist="$sessdir/session.hits.$1" if [ -f "$hitlist" ]; then lbreakifs set for file in `cat $hitlist | cut -d':' -f2 | sed 's/.//'`; do if [ -f "$file" ]; then get_filestat "$file" 1 file_name=`basename "$file"` file_namewc=`echo "$file_name" | $wc -m` file_hitname=`egrep -m1 "$file" $hitlist | awk '{print$1}'` if [ ! "$md5_hash" ]; then md5_hash="$($md5sum "$file" | awk '{print$1}')" fi if [ "$pub" == "1" ]; then chattr -ia "$file" >> /dev/null 2>&1 chmod 400 "$file" else chattr -ia "$file" chmod 000 "$file" fi rnd="${RANDOM}${RANDOM}" mv "$file" "$quardir/$file_name.$rnd" touch --no-create "$quardir/$file_name.$rnd" echo -e "# owner:group:mode:size(b):md5:atime(epoch):mtime(epoch):ctime(epoch):file(path)\n$file_owner:$file_group:$file_mode:$file_size:$md5_hash:$file_times:$file" > $quardir/$file_name.$rnd.info eout "{quar} malware quarantined from '$file' to '$quardir/$file_name.$rnd'" 1 echo "$utime:$file_hitname:$file:$file_owner:$file_group:$md5_hash:$file_size:$quardir/$file_name.$rnd" >> $quar_history if [ "$quarantine_suspend_user" == "1" ]; then quarantine_suspend_user "$file" fi if [ "$quarantine_clean" == "1" ] && [ ! "$clean_state" == "1" ]; then unset clean_state hitname=`cat $hitlist | grep $file | awk '{print$1}'` clean "$quardir/$file_name.$rnd" "$hitname" "$file_owner.$file_group" "$file_mode" "$file_size" "$md5_hah" "$file" fi fi done lbreakifs unset else echo "{quar} invalid quarantine hit list, aborting." exit fi } clamselector() { scan_max_filesize=`cat $sig_md5_file | cut -d':' -f2 | sort -n | tail -n1` if [ "$scan_max_filesize" -gt "1" 2> /dev/null ]; then scan_max_filesize=$[scan_max_filesize+1] clamscan_max_filesize="${scan_max_filesize}" scan_max_filesize="${scan_max_filesize}c" else scan_max_filesize="2048k" clamscan_max_filesize="2592000" fi if [ "$scan_clamscan" == "1" ]; then trim_log $clamscan_log 10000 1 for dpath in $clamav_paths; do if [ -f "${dpath}/main.cld" ] || [ -f "${dpath}/main.cvd" ]; then clamav_db="-d $dpath" fi done isclamd=`pgrep -x clamd 2> /dev/null` isclamd_root=`pgrep -x -u root clamd 2> /dev/null` if [ "$scan_clamd_remote" == "1" ] && [ -f "$remote_clamd_config" ]; then clamd=1 clambin="clamdscan" clamopts="--fdpass -c $remote_clamd_config" elif [ "$isclamd" ] && [ "$isclamd_root" ]; then clamd=1 clambin="clamdscan" clamopts="$clamdscan_extraopts" elif [ "$isclamd" ] && [ ! "$isclamd_root" ]; then clamd=1 clambin="clamdscan" clamopts="--fdpass $clamdscan_extraopts" else clambin="clamscan" clamopts="$clamscan_extraopts --max-filesize=$clamscan_max_filesize --max-scansize=$[clamscan_max_filesize*2] -d $runtime_hdb -d $runtime_ndb $clamav_db -r" if [ "$monitor_mode" ]; then inotify_sleep="120" eout "{mon} warning clamd service not running; force-set monitor mode file scanning to every 120s" fi fi if [ -f "/usr/local/cpanel/3rdparty/bin/$clambin" ]; then clamscan="/usr/local/cpanel/3rdparty/bin/$clambin" elif [ -f "$(command -v $clambin 2> /dev/null)" ]; then clamscan=`command -v $clambin 2> /dev/null` else scan_clamscan="0" fi if [ "$clamd" ] && [ "$scan_clamscan" == "1" ]; then ## test clamdscan for errors as not all 'running' instances of clamd are indicative of working setup if [ "$scan_clamd_remote" == "1" ] && [ -f "$remote_clamd_config" ]; then try=0 while [ $try -le $remote_clamd_max_retry ]; do clamd_test=`$clamscan $clamopts --fdpass --quiet --no-summary /etc/passwd 2> /dev/null || echo $?` if [ "$clamd_test" = "2" ]; then ((try++)) sleep $remote_clamd_retry_sleep else break fi done else clamd_test=`$clamscan --fdpass --quiet --no-summary /etc/passwd 2> /dev/null || echo $?` fi if [ ! -z "$clamd_test" ]; then clamd=0 clambin="clamscan" clamopts="$clamscan_extraopts --max-filesize=$clamscan_max_filesize --max-scansize=$[clamscan_max_filesize*2] -d $runtime_hdb -d $runtime_ndb $clamav_db -r" if [ -f "/usr/local/cpanel/3rdparty/bin/$clambin" ]; then clamscan="/usr/local/cpanel/3rdparty/bin/$clambin" elif [ -f "$(command -v $clambin 2> /dev/null)" ]; then clamscan=`command -v $clambin 2> /dev/null` else scan_clamscan="0" fi fi fi fi } scan() { scan_start_hr=`date +"%b %e %Y %H:%M:%S %z"` scan_start=`date +"%s"` spaths_str=$(echo "$1" | tr '?' '*' | tr ',' '\n') spaths=() while IFS= read -r pattern; do [ -z "$pattern" ] && continue expanded=( $pattern ) spaths+=( "${expanded[@]}" ) done <<< "$spaths_str" days="$2" scanid="$datestamp.$$" if [ "$file_list" ]; then spath="\"$file_list\"" elif [ ! -f "$find" ]; then eout "{scan} could not locate find command" 1 clean_exit exit 1 fi if [ -f "$spath" ] && [ -z "$file_list" ]; then single_filescan=1 fi if [ ! -f "$sig_md5_file" ]; then eout "{scan} required signature file not found ($sig_md5_file), try running -u|--update, aborting!" 1 clean_exit exit 1 fi if [ ! -f "$sig_hex_file" ]; then eout "{scan} required signature file not found ($sig_hex_file), try running -u|--update, aborting!" 1 clean_exit exit 1 fi if [ ! -f "$ignore_paths" ]; then touch $ignore_paths chmod 640 $ignore_paths elif [ ! -f "$ignore_sigs" ]; then touch $ignore_sigs chmod 640 $ignore_sigs fi if [ ! "$days" == "all" ] && [ -z "$file_list" ]; then val=`echo $days | grep "[[:alpha:]]"` if [ ! -z "$val" ]; then eout "{scan} days value must be numeric value in the range of 1 - 90, reverting to default (7)." 1 days=7 elif [ "$days" -gt "90" ]; then eout "{scan} days value must be numeric value in the range of 1 - 90, reverting to default (7)." 1 days=7 fi fi for p in "${spaths[@]}"; do if [ "${p:0:1}" != "/" ]; then eout "{scan} must use absolute path, provided relative path '$p'" 1 exit 1 fi done scan_session="$tmpdir/.sess.$$" find_results="$tmpdir/.find.$$" touch $find_results touch $scan_session sigignore gensigs if [ "$scan_clamscan" == "1" ]; then clamselector fi hex_sigs=`$wc -l $sig_hex_file | awk '{print$1}'` md5_sigs=`$wc -l $sig_md5_file | awk '{print$1}'` yara_sigs=`grep -E -c ^rule $sig_yara_file | awk '{print$1}'` user_hex_sigs=`$wc -l $sig_user_hex_file | awk '{print$1}'` user_md5_sigs=`$wc -l $sig_user_md5_file | awk '{print$1}'` user_sigs=$[user_hex_sigs+user_md5_sigs] tot_sigs=$[md5_sigs+hex_sigs+user_hex_sigs+user_md5_sigs+yara_sigs] if [ -z "$hscan" ]; then eout "{scan} signatures loaded: $tot_sigs ($md5_sigs MD5 | $hex_sigs HEX | $yara_sigs YARA | $user_sigs USER)" 1 fi if [ -f "$ignore_file_ext" ]; then if [ ! "$(cat $ignore_file_ext)" == "" ]; then for i in `cat $ignore_file_ext`; do if [ "$ignore_fext" == "" ]; then ignore_fext="-not -iname \"*$i\"" else ignore_fext="$ignore_fext -not -iname \"*$i\"" fi done fi fi if [ "$scan_ignore_root" == "1" ]; then ignore_root="-not -uid 0 -not -gid 0" fi if [ "$scan_ignore_user" ]; then for i in `echo $scan_ignore_user | tr ', ' ' '`; do if [ "$ignore_user" == "" ]; then ignore_user="-not -user $i" else ignore_user="$ignore_user -not -user $i" fi done fi if [ "$scan_ignore_group" ]; then for i in `echo $scan_ignore_group | tr ', ' ' '`; do if [ "$ignore_group" == "" ]; then ignore_group="-not -group $i" else ignore_group="$ignore_group -not -group $i" fi done fi if [ "$scan_tmpdir_paths" ] && [ -z "$hscan" ] && [ -z "$single_filescan" ]; then spath_tmpdirs="$scan_tmpdir_paths" fi if [ "$file_list" ]; then cat $file_list | grep -E -vf $ignore_paths > $find_results else if [ "$days" == "all" ]; then if [ -z "$hscan" ]; then eout "{scan} building file list for $hrspath, this might take awhile..." 1 fi else rscan=1 if [ -z "$hscan" ]; then eout "{scan} building file list for $hrspath of new/modified files from last $days days, this might take awhile..." 1 fi fi if [ -z "$scan_find_timeout" ];then scan_find_timeout=0 fi if [ "$scan_find_timeout" -ge "60" ]; then echo -e "sleep $scan_find_timeout\ntouch $tmpdir/.find_killed.$scanid\npkill -f lmd_find" > $tmpdir/.lmd_find_sleep.$$ sh -c "sh $tmpdir/.lmd_find_sleep.$$ >> /dev/null 2>&1 &" >> /dev/null 2>&1 & rm -f $tmpdir/.lmd_find_sleep.$$ 2> /dev/null eout "{scan} setting maximum execution time for 'find' file list: ${scan_find_timeout}sec" 1 fi if [ -z "$hscan" ]; then eout "{scan} setting nice scheduler priorities for all operations: cpunice $scan_cpunice , ionice $scan_ionice" 1 fi file_list_start=`date +"%s"` tmpscandir="$tmpdir/scan.$RANDOM" mkdir -p "$tmpscandir" ; chmod 700 $tmpscandir ; cd $tmpscandir if [[ -s "$ignore_paths" ]] && [[ -r "$ignore_paths" ]]; then find_prune="" while IFS= read -r ignore_path; do if [[ -n "$ignore_path" && -d "$ignore_path" ]]; then find_prune="${find_prune} -path \"$(printf '%q' "$ignore_path")\" -prune -o" fi done < "$ignore_paths" fi if [ "$days" == "all" ]; then eout "{scan} executed $nice_command $find $spath $spath_tmpdirs $find_prune -maxdepth $scan_max_depth $find_opts -type f -size +${scan_min_filesize}c -size -$scan_max_filesize $include_regex -not -perm 000 $exclude_regex $ignore_fext $ignore_root $ignore_user $ignore_group" $nice_command $find /lmd_find/ "${spaths[@]}" $spath_tmpdirs $find_prune -maxdepth "$scan_max_depth" $find_opts -type f -size +"${scan_min_filesize}c" -size -"${scan_max_filesize}" $include_regex -not -perm 000 $exclude_regex $ignore_fext $ignore_root $ignore_user $ignore_group 2>/dev/null | grep -E -vf "$ignore_paths" > $find_results else eout "{scan} executed $nice_command $find $spath $spath_tmpdirs $find_prune -maxdepth $scan_max_depth $find_opts \( -mtime -${days} -o -ctime -${days} \) -type f -size +${scan_min_filesize}c -size -$scan_max_filesize $include_regex -not -perm 000 $exclude_regex $ignore_fext $ignore_root $ignore_user $ignore_group" $nice_command $find /lmd_find/ "${spaths[@]}" $spath_tmpdirs $find_prune -maxdepth "$scan_max_depth" $find_opts \( -mtime -${days} -o -ctime -${days} \) -type f -size +"${scan_min_filesize}c" -size -"${scan_max_filesize}" $include_regex -not -perm 000 $exclude_regex $ignore_fext $ignore_root $ignore_user $ignore_group 2>/dev/null | grep -E -vf "$ignore_paths" > $find_results fi cd $tmpdir rm -rf $tmpscandir if [ "$rscan" = "1" ] && [ "$scan_export_filelist" == "1" ]; then rm -f $tmpdir/.find_results.* 2> /dev/null ; cp $find_results $tmpdir/.find_results.shared.$$ 2> /dev/null ln -fs $tmpdir/.find_results.shared.$$ $tmpdir/find_results.last 2> /dev/null fi file_list_end=`date +"%s"` file_list_et=$[file_list_end-file_list_start] if [ -f "$tmpdir/.find_killed.$scanid" ]; then rm -f $tmpdir/.find_killed.$scanid echo && eout "{scan} file list 'find' operation reached maximum execution time (${scan_find_timeout}sec) and was terminated" 1 else pkill -f lmd_find_sleep >> /dev/null 2>&1 fi fi if [ ! -f "$find_results" ] || [ ! -s "$find_results" ]; then if [ -z "$hscan" ]; then if [ "$days" == "all" ]; then eout "{scan} scan returned empty file list; check that path exists and contains files in scope of configuration." 1 rm -f $find_results $scan_session $runtime_ndb $runtime_hdb $runtime_hexstrings $clamscan_results $tmpdir/.tmpf* exit 0 else eout "{scan} scan returned empty file list; check that path exists, contains files in days range or files in scope of configuration." 1 rm -f $find_results $scan_session $runtime_ndb $runtime_hdb $runtime_hexstrings $clamscan_results exit 0 fi fi fi res_col="1" move_to_col="echo -en \\033[${res_col}G" tot_files=`$wc -l $find_results | awk '{print$1}'` if [ -z "$hscan" ] && [ -z "$single_filescan" ]; then if [ "$file_list" ]; then eout "{scan} user supplied file list '$file_list', found $tot_files files..." 1 else eout "{scan} file list completed in ${file_list_et}s, found $tot_files files..." 1 fi fi touch $sessdir/clean.$$ if [ ! -f "$scan_session" ]; then touch $scan_session fi if [ ! -z "$hscan" ]; then eout "{scan.hook} scan of $spath in progress (id: $datestamp.$$)" fi cnt=0 if [ -z "$mail" ] && [ -z "$sendmail" ]; then eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled." fi if [ -f "$clamscan" ] && [ "$scan_clamscan" == "1" ]; then if [ -z "$hscan" ]; then eout "{scan} found clamav binary at $clamscan, using clamav scanner engine..." 1 fi if [ "$string_length_scan" == "1" ]; then if [ -z "$hscan" ]; then eout "{scan} preprocessing file list for string length hits..." 1 scan_strlen list "$find_results" >> /dev/null 2>&1 fi fi if [ -z "$hscan" ]; then eout "{scan} scan of $hrspath ($tot_files files) in progress..." 1 fi echo "$(date +"%b %d %Y %H:%M:%S") $(hostname -s) clamscan start" >> $clamscan_log clamscan_results="$tmpdir/.clamscan.$$" echo "$(date +"%b %d %Y %H:%M:%S") $(hostname -s) executed: $nice_command $clamscan $clamopts --infected --no-summary -f $find_results" >> $clamscan_log if [ "$scan_clamd_remote" == "1" ] && [ -f "$remote_clamd_config" ]; then try=0 while [ $try -le $remote_clamd_max_retry ]; do $nice_command $clamscan $clamopts --infected --no-summary -f $find_results > $clamscan_results 2>> $clamscan_log clamscan_return=$? if [ "$clamscan_return" == "2" ]; then ((try++)) echo "$(date +"%b %d %H:%M:%S") $(hostname -s) remote clamd error - retrying in $retry_sleep seconds ($try)" sleep $remote_clamd_retry_sleep else break fi done else $nice_command $clamscan $clamopts --infected --no-summary -f $find_results > $clamscan_results 2>> $clamscan_log clamscan_return=$? fi if [ "$clamscan_return" == "2" ]; then if [ "$quarantine_on_error" == "0" ] || [ -z "$quarantine_on_error" ]; then quarantine_hits=0 eout "{scan} clamscan returned an error, check $clamscan_log for details; quarantine_on_error=0 or unset, quarantine has been disabled!" 1 else eout "{scan} clamscan returned an error, check $clamscan_log for details!" 1 fi fi clamscan_fatal_error=`grep -m1 'no reply from clamd' $clamscan_results` if [ "$clamscan_fatal_error" ]; then quarantine_hits=0 eout "{scan} clamscan returned a fatal error in scan results, check $clamscan_log for details; quarantine has been disabled!" 1 fi echo "$(date +"%b %d %Y %H:%M:%S") $(hostname -s) clamscan end return $clamscan_return" >> $clamscan_log lbreakifs set for hit in `grep -E -v 'ERROR$|lstat()|no reply from clamd' $clamscan_results | sed -e 's/.UNOFFICIAL//' -e 's/ FOUND$//' | awk -F':' '{print$2":"$1}' | sed 's/.//'`; do file=`echo "$hit" | cut -d':' -f2` signame=`echo "$hit" | cut -d':' -f1` if [ ! -z "$(echo $signame | grep -E 'YARA')" ]; then signame=`echo "$signame" | sed 's/YARA\./{YARA}/'` elif [ -z "$(echo $signame | grep -E 'HEX|MD5')" ]; then signame="{CAV}$signame" fi ignore_hit=`echo $signame | grep -E -vf $ignore_sigs` if [ ! -z "$ignore_hit" ]; then record_hit "$file" "$signame" quarantine "$file" "$signame" if [ ! "$set_background" == "1" ]; then tot_hits=`$wc -l $scan_session | awk '{print$1}'` tot_cl=`$wc -l $sessdir/clean.$$ | awk '{print$1}'` if [ -z "$hscan" ]; then echo -en "\\033[${res_col}G" && echo -n "maldet($$): {scan} processing scan results for hits: $tot_hits hits $tot_cl cleaned" fi cnt="$tot_files" fi fi unset ignore_hit done lbreakifs unset echo "$(date +"%b %d %Y %H:%M:%S") $(hostname -s) clamscan end" >> $clamscan_log else if [ -z "$hscan" ]; then eout "{scan} scan of $hrspath ($tot_files files) in progress..." 1 fi lbreakifs set if [ ! -f "$scan_session" ]; then touch $scan_session fi while read rpath; do ((cnt++)) if [ -z "$hscan" ] && [ ! "$set_background" == "1" ] && [ -z "$single_filescan" ]; then tot_hit=`$wc -l $scan_session | awk '{print$1}'` cl_hit=`$wc -l $sessdir/clean.$$ | awk '{print$1}'` echo -en "\\033[${res_col}G" && echo -n "maldet($$): {scan} $cnt/$tot_files files scanned: $tot_hit hits $cl_hit cleaned" fi if [ -f "$rpath" ]; then scan_stage1 "$rpath" >> /dev/null 2>&1 fi done < $find_results lbreakifs unset fi if [ -z "$hscan" ]; then if [ ! "$set_background" == "1" ] && [ "$scan_clamscan" == "0" ] && [ -z "$single_filescan" ]; then echo fi fi scan_end_hr=`date +"%b %e %Y %H:%M:%S %z"` scan_end=`date +"%s"` scan_et=$[scan_end-scan_start] scan_et_nofl=$[scan_et-file_list_et] tot_hits=`$wc -l $scan_session | awk '{print$1}'` tot_cl=`$wc -l $sessdir/clean.$$ | awk '{print$1}'` gen_report if [ ! -z "$hscan" ]; then if [ ! "$tot_hits" == "0" ]; then echo "0 maldet: $hitname $spath" eout "{scan.hook} results returned FAIL hit found on $spath (id: $datestamp.$$)" else echo "1 maldet: OK" eout "{scan.hook} results returned OK on $spath (id: $datestamp.$$)" fi else if [ -z "$hscan" ]; then echo fi eout "{scan} scan completed on $hrspath: files $tot_files, malware hits $tot_hits, cleaned hits $tot_cl, time ${scan_et}s" 1 eout "{scan} scan report saved, to view run: maldet --report $datestamp.$$" 1 if [ "$quarantine_hits" == "0" ] && [ ! "$tot_hits" == "0" ]; then eout "{scan} quarantine is disabled! set quarantine_hits=1 in $cnffile or to quarantine results run: maldet -q $datestamp.$$" 1 fi fi if [ ! "$tot_hits" == "0" ]; then if [ "$email_ignore_clean" == "1" ] && [ ! "$tot_hits" == "$tot_cl" ]; then genalert file $nsess elif [ "$email_ignore_clean" == "0" ]; then genalert file $nsess fi if [ "$email_panel_user_alerts" == "1" ]; then genalert panel $nsess fi fi mv $scan_session $nsess_hits rm -f $find_results $scan_session $runtime_ndb $runtime_hdb $runtime_hexstrings $clamscan_results } scan_strlen() { type="$1" file="$2" if [ "$string_length_scan" == "1" ] && [ "$type" == "file" ]; then flen=`$wc -L $file 2> /dev/null | awk '{print$1}'` if [ "$flen" -ge "$string_length" ]; then eout "{strlen} malware string length hit on $file" quarantine "$file" "{SA}stat.strlength" fi elif [ "$string_length_scan" == "1" ] && [ "$type" == "list" ]; then list="$tmpdir/.strlen.flist.$$" cp $file $list sed -i -e "s/'/\\\\'/g" $list cat $list | xargs wc -L 2> /dev/null | grep -vw total >> $list.strlen awk "{if (\$1>=$string_length) print\$2}" $list.strlen >> $list.hits for i in `cat $list.hits`; do if [ -f "$i" ]; then eout "{strlen} malware string length hit on $i" quarantine "$i" "{SA}stat.strlength" fi done rm -f $list* fi } scan_stage1() { file="$1" clean_check="$2" hash="$($md5sum "$file" | awk '{print$1}')" if [ -z "$runtime_hexstrings" ]; then sigignore gensigs fi if [ ! -z "$hash" ]; then val_hash=`grep -m1 $hash $sig_user_md5_file $sig_md5_file` if [ ! -z "$val_hash" ]; then md5_hit="$hash" md5_hitname=`echo $val_hash | cut -d':' -f4` md5_hash="$hash" if [ "$clean_check" == "1" ]; then clean_failed=1 else record_hit "$file" "$md5_hitname" quarantine "$file" "$md5_hitname" fi unset val_hash md5_hit md5_hitname md5_hash else if [ -f "$file" ]; then scan_stage2 "$file" $clean_check >> /dev/null 2>&1 fi if [ -f "$file" ]; then scan_strlen file "$file" >> /dev/null 2>&1 fi fi else eout "{scan} error could not read or hash $file, do we have permission?" fi } scan_stage2() { file="$1" clean_check="$2" if [ -z "$ftype" ]; then if [ -p "$hex_fifo_path" ] && [ "$scan_hexfifo" == "1" ]; then if [ "$OSTYPE" == "FreeBSD" ]; then $od -v -N$scan_hexfifo_depth -tx1 "$file" | cut -c12-256 | tr -d ' \n' > $hex_fifo_path 2>&1 & else $od -v -w64 -N$scan_hexfifo_depth -tx1 "$file" | cut -c9-256 | tr -d '\n ' > $hex_fifo_path 2>&1 & fi val_hex=`$perl $hex_fifo_script $runtime_hexstrings` else if [ "$OSTYPE" == "FreeBSD" ]; then val_hex=`$perl $runtime_hexstrings $hex_string_script $($od -v -N$scan_hexdepth -tx1 "$file" | cut -c12-256 | tr -d ' \n')` else val_hex=`$perl $runtime_hexstrings $hex_string_script $($od -v -w$scan_hexdepth -N$scan_hexdepth -tx1 "$file" | tr -d '\n ')` fi fi if [ ! -z "$val_hex" ]; then hex_hit=`echo $val_hex | awk '{print$1}'` hex_hitname=`echo $val_hex | awk '{print$2}'` if [ "$clean_check" == "1" ]; then clean_failed=1 else record_hit "$file" "$hex_hitname" quarantine "$file" "$hex_hitname" fi unset val_hex hex_hit hex_hitname fi fi } gen_report() { if [ -f "$scan_session" ]; then tot_hits=`$wc -l $scan_session | awk '{print$1}'` nsess_hits="$sessdir/session.hits.$datestamp.$$" echo "$datestamp.$$" > $sessdir/session.last nsess="$sessdir/session.$datestamp.$$" tmpf="$nsess" . $email_template fi } trim_log() { log="$1" logtrim="$2" if [ -f "$log" ]; then log_size=`$wc -l $log | awk '{print$1}'` if [ "$log_size" -gt "$logtrim" 2> /dev/null ]; then trim=$[logtrim/10] printf "%s\n" "$trim,${log_size}d" w | ed -s $log 2> /dev/null fi elif [ ! -f "$log" ] && [ "$3" == "1" ]; then touch $log ; chmod 640 $log fi } genalert() { type="$1" file="$2" if [ "$email_alert" == "1" ] || [ "$type" == "digest" ] || [ "$type" == "daily" ]; then if [ "$type" == "file" ] && [ -f "$file" ]; then if [ -f "$mail" ]; then cat $file | $mail -s "$email_subj" $email_addr elif [ -f "$sendmail" ]; then if ! grep -q "SUBJECT: " "$file"; then echo -e "SUBJECT: $email_subj\n$(cat $file)" > $file fi cat $file | $sendmail -t $email_addr else eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled." fi if [ ! "$(whoami)" == "root" ] && [ -z "$(echo $2 | grep '\@')" ]; then if [ -z "$hscan" ]; then eout "{alert} sent scan report to config default $email_addr" 1 eout "{alert} send scan report to an alternate address with: maldet --report $datestamp.$$ you@domain.com" 1 else eout "{alert} sent scan report to config default $email_addr" fi else if [ -z "$hscan" ]; then eout "{alert} sent scan report to $email_addr" 1 fi fi elif [ "$type" == "panel" ] && [ -f "$file" ]; then eout "{panel} Detecting control panel and sending alerts..." 1 control_panel="" detect_control_panel if [ "$control_panel" == "error" ] || [ "$control_panel" == "unknown" ]; then eout "{panel} Failed to set control panel. Will not send alerts to control panel account contacts." 1 else # Sort malware hits from $file and map the detected files to their system user owner file_hits=$(awk '/FILE HIT LIST:/{flag=1;next}/^=======/{flag=0}flag{print $3}' $file) for hit in $file_hits; do hit_line=$(grep "$hit" $file) if [ -f "$hit" ]; then file_owner=$(stat -c "%U" $hit) elif ! [ -f "$hit" ] && [ "$quarantine_hits" == "1" ] && [[ "$hit_line" == *"=>"* ]]; then quarantined_file=$(echo $hit_line | awk '{print $NF}') file_owner=$(awk -F':' '/^[^#]/{print $1}' ${quarantined_file}.info) fi echo "$file_owner : $hit" >> $tmpdir/.panel_alert.hits done # Sort cleaned files too if [ "$quarantine_clean" == "1" ]; then for clean_file in $(cat $sessdir/clean.$$); do if [ -f $clean_file ]; then clean_owner=$(stat -c "%U" $clean_file) fi echo "$clean_owner : $clean_file" >> $tmpdir/.panel_alert.clean done fi # Determine control panel, noop if error or none detected eout "{panel} Detected control panel $control_panel. Will send alerts to control panel account contacts." 1 user_list=$(awk '{print $1}' $tmpdir/.panel_alert.hits | sort | uniq) if [ -n "$user_list" ]; then for sys_user in $user_list; do contact_emails="" get_panel_contacts $control_panel $sys_user grep "^$sys_user " $tmpdir/.panel_alert.hits | awk '{print $3}' > $tmpdir/.${sys_user}.hits user_tot_hits=$($wc -l $tmpdir/.${sys_user}.hits | awk '{print$1}') if [ -f $tmpdir/.panel_alert.clean ]; then grep "^$sys_user " $tmpdir/.panel_alert.clean | awk '{print $3}' > $tmpdir/.${sys_user}.clean user_tot_cl=$($wc -l $tmpdir/.${sys_user}.clean | awk '{print$1}') fi tmpf=$tmpdir/.${sys_user}.alert if [ -f "$sendmail" ]; then echo "TO: $contact_emails" > $tmpf echo "FROM: $email_panel_from" >> $tmpf echo "REPLY-TO: $email_panel_replyto" >> $tmpf echo -e "SUBJECT: $email_panel_alert_subj\n" >> $tmpf . $email_panel_alert_etpl cat $tmpf | $sendmail -t elif [ -f "$mail" ] && [ "$control_panel" == "cpanel" ]; then . $email_panel_alert_etpl cat $tmpf | $mail -r $email_panel_from -s $email_panel_alert_subj $contact_emails else eout "{panel} No compatible \$sendmail or \$mail binaries found, control panel account alerts disabled." fi done fi rm -f $tmpdir/.panel_alert.hits $tmpdir/.panel_alert.clean $tmpdir/.${sys_user}.hits $tmpdir/.${sys_user}.clean $tmpf fi elif [ "$type" == "daily" ] || [ "$type" == "digest" ]; then inotify_start_time=`ps -p $(ps -A -o 'pid cmd' | grep -E maldetect | grep -E inotifywait | awk '{print$1}' | head -n1) -o lstart= 2> /dev/null` scan_start_hr=`date -d "$inotify_start_time" +"%b %e %Y %H:%M:%S %z"` scan_start_elapsed=$(($(date +'%s')-$(date -d "$scan_start_hr" +'%s'))) inotify_run_time=`echo $(($scan_start_elapsed/86400))d:$(($(($scan_start_elapsed - $scan_start_elapsed/86400*86400))/3600))h:$(($(($scan_start_elapsed - $scan_start_elapsed/86400*86400))%3600/60))m:$(($(($scan_start_elapsed - $scan_start_elapsed/86400*86400))%60))s` rm -f $tmpdir/.digest.alert.hits $tmpdir/.digest.clean.hits $tmpdir/.digest.monitor.alert $tmpdir/.digest.susp.hits scanid="$datestamp.$$" scan_session=`cat $sessdir/session.monitor.current` $tlog $scan_session digest.alert > $tmpdir/.digest.alert.hits $tlog $clean_history digest.clean.alert > $tmpdir/.digest.clean.hits $tlog $monitor_scanned_history digest.monitor.alert > $tmpdir/.digest.monitor.alert $tlog $suspend_history digest.susp.alert > $tmpdir/.digest.susp.hits tot_hits=`$wc -l $tmpdir/.digest.alert.hits | awk '{print$1}'` tot_cl=`$wc -l $tmpdir/.digest.clean.hits | awk '{print$1}'` tot_files=`$wc -l $tmpdir/.digest.monitor.alert | awk '{print$1}'` tot_susp=`$wc -l $tmpdir/.digest.susp.hits | awk '{print$1}'` trim_log $monitor_scanned_history 50000 trim_log $clean_history 50000 trim_log $suspend_history 50000 $tlog $sessdir/session.hits.$datestamp.$$ digest.alert >> /dev/null 2>&1 $tlog $clean_history digest.clean.alert >> /dev/null 2>&1 $tlog $monitor_scanned_history digest.monitor.alert >> /dev/null 2>&1 $tlog $suspend_history digest.susp.alert >> /dev/null 2>&1 if [ ! -z "$(cat $tmpdir/.digest.alert.hits)" ]; then tmpf="$tmpdir/.alert.$RANDOM.$$" if [ "$tot_hits" -gt "$tot_files" ]; then tot_files="$tot_hits" fi . $email_template cp $tmpf $sessdir/session.$scanid grep -E '^{.*}' $sessdir/session.$scanid > $sessdir/session.hits.$scanid echo "$scanid" > $sessdir/session.last email_subj="${email_subj}: monitor summary" if [ -f "$mail" ]; then cat $tmpf | $mail -s "$email_subj" $email_addr eout "{alert} sent $type alert to $email_addr" elif [ -f "$sendmail" ]; then if ! grep -q "SUBJECT: " "$tmpf"; then echo -e "SUBJECT: $email_subj\n$(cat $tmpf)" > $tmpf fi cat $tmpf | $sendmail -t $email_addr eout "{alert} sent $type alert to $email_addr" else eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled." fi rm -f $tmpf $tmpdir/.digest.alert.hits $tmpdir/.digest.clean.hits $tmpdir/.digest.monitor.alert $tmpdir/.digest.susp.hits fi else eout "{alert} file input error, alert discarded." fi fi if [ "$slack_alert" == "1" ]; then if [ "$type" == "file" ] && [ -f "$file" ] && [ -f "$curl" ]; then slack_response=$($curl -s -F "token=$slack_token" -F "file=@$file" -F "filename=$slack_subj" -F "channels=$slack_channels" -X POST https://slack.com/api/files.upload | grep -oP '^{"ok":true') if [ "$slack_response" ]; then eout "{alert} scan report sent to slack channel(s): $slack_channels" 0 else eout "{alert} could not upload scan report to slack channel(s), alert discarded" 1 fi else eout "{alert} could not upload scan report to slack channel(s), alert discarded" 1 fi fi if [ "$telegram_alert" == "1" ]; then if [ "$type" == "file" ] && [ -f "$file" ]; then telegram_response=$(curl -F "document=@$file" -F "caption=$telegram_file_caption" "https://api.telegram.org/$telegram_bot_token/sendDocument?chat_id=$telegram_channel_id" | grep -oP '^{"ok":true') if [ "$telegram_response" ]; then eout "{alert} scan report sent to telegram channel: $telegram_channel_id" 0 else eout "{alert} could not upload scan report to telegram channel, alert discarded" 1 fi fi fi } monitor_kill() { touch $tmpdir/stop_monitor inotify_pid=`pgrep -f inotify.paths.[0-9]+` if [ -f "$tmpdir/monitor.pid" ]; then monitor_pid=`cat $tmpdir/monitor.pid` exit_code="0" else exit_code="1" fi monitor_pgid=`ps -p "$monitor_pid" -o pgid= | tr -d ' '` kill -9 -- $inotify_pid -$monitor_pgid >> /dev/null 2>&1 exit $exit_code } monitor_cycle() { if [ "$BASHPID" ]; then echo "$BASHPID" > $tmpdir/monitor.pid else pgrep maldet > $tmpdir/monitor.pid fi inotify_cycle_runtime=0 while [ ! -f "$tmpdir/stop_monitor" ]; do inotify_pid=`pgrep -f inotify.paths.[0-9]+` if [ -z "$inotify_pid" ]; then eout "{mon} no inotify process found, exiting (are we a zombie process?)" 1 exit fi log_size=`$wc -l $inotify_log | awk '{print$1}'` if [ "$log_size" -ge "$inotify_trim" ]; then trim=$(($log_size - 1000)) log_chars=`printf "%s\n" "1,${trim}p" | ed -s $inotify_log 2> /dev/null | wc -c` tlog_new=$(( `cat $inspath/tmp/inotify` - $log_chars )) echo $tlog_new > $inspath/tmp/inotify printf "%s\n" "1,${trim}d" w | ed -s $inotify_log 2> /dev/null eout "{mon} inotify log file trimmed" fi if [ "$inotify_cycle_runtime" -ge "$inotify_reloadtime" ] || [ -f "$inspath/reload_monitor" ]; then if [ -f "$inspath/reload_monitor" ]; then rm -f $inspath/reload_monitor fi source $cnf source $intcnf import_conf inotify_cycle_runtime=0 if [ -f "$ignore_file_ext" ]; then if [ ! "$(cat $ignore_file_ext)" == "" ]; then for i in `cat $ignore_file_ext`; do if [ "$ignore_fext" == "" ]; then ignore_fext="-not -iname \"*$i\"" else ignore_fext="$ignore_fext -not -iname \"*$i\"" fi done fi fi if [ "$scan_ignore_root" == "1" ]; then ignore_root="-not -uid 0 -not -gid 0" fi if [ "$scan_ignore_user" ]; then for i in `echo $scan_ignore_user | tr ', ' ' '`; do if [ "$ignore_user" == "" ]; then ignore_user="-not -user $i" else ignore_user="$ignore_user -not -user $i" fi done fi if [ "$scan_ignore_group" ]; then for i in `echo $scan_ignore_group | tr ', ' ' '`; do if [ "$ignore_group" == "" ]; then ignore_group="-not -group $i" else ignore_group="$ignore_group -not -group $i" fi done fi eout "{mon} reloaded configuration data" 1 fi sleep $inotify_sleep inotify_cycle_runtime=$[inotify_sleep+inotify_cycle_runtime] if [ -f "$ignore_sigs" ]; then ignore_sigs_current_md5=`md5sum $ignore_sigs | awk '{print$1}'` if [ ! "$ignore_sigs_current_md5" == "$ignore_sigs_last_md5" ]; then sigignore 1 gensigs ignore_sigs_current_md5=`md5sum $ignore_sigs | awk '{print$1}'` eout "{mon} regenerated signature files on ignore_sigs file change detected" 1 fi ignore_sigs_last_md5="$ignore_sigs_current_md5" fi if [ "$scan_clamscan" == "1" ]; then monitor_mode=1 clamselector fi monitor_check done rm -f $tmpdir/stop_monitor eout "{mon} monitoring terminated by user, inotify killed." exit } cleanup_scanlist() { # Checks for files that are already gone (temporary files, cache # files, ...) and removes them from $monitor_scanlist, so we don't # get errors and too many retries TMP_FILE=$(mktemp -p /var/lib/maldetect/tmp) lbreakifs set for FILE in $(cat $monitor_scanlist); do if [ -f "$FILE" ]; then echo "$FILE" >> $TMP_FILE fi done lbreakifs unset mv $TMP_FILE $monitor_scanlist } monitor_check() { monitor_scanlist="$tmpdir/.monitor.scan.${RANDOM}${RANDOM}" touch $monitor_scanlist ; chmod 600 $monitor_scanlist $tlog $inotify_log inotify | grep -E " CREATE| MODIFY| MOVED_TO" | awk -F" CREATE| MODIFY| MOVED_TO" '{print $1}' | sort -u | grep -vf $ignore_paths> $monitor_scanlist if [ "$scan_clamscan" == "1" ]; then clamscan_results="$tmpdir/.clamscan.result.${RANDOM}${RANDOM}" touch $clamscan_results ; chmod 600 $clamscan_results $nice_command $clamscan $clamopts --infected --no-summary -f $monitor_scanlist > $clamscan_results 2>> $clamscan_log || clamscan_return=$? if [ "$scan_clamd_remote" == "1" ] && [ -f "$remote_clamd_config" ]; then try=0 while [ $try -le $remote_clamd_max_retry ]; do cleanup_scanlist $monitor_scanlist $nice_command $clamscan $clamopts --infected --no-summary -f $monitor_scanlist > $clamscan_results 2>> $clamscan_log clamscan_return=$? if [ "$clamscan_return" == "2" ]; then ((try++)) echo "$(date +"%b %d %H:%M:%S") $(hostname -s) remote clamd error - retrying in $retry_sleep seconds ($try)" >> $clamscan_log sleep $remote_clamd_retry_sleep else break fi done else $nice_command $clamscan $clamopts --infected --no-summary -f $monitor_scanlist > $clamscan_results 2>> $clamscan_log || clamscan_return=$? fi if [ "$inotify_verbose" == "1" ]; then for file in `cat $monitor_scanlist | tr ' ' '%'`; do file=`echo $file | tr '%' ' '` eout "{mon} inotify clamav file scan on $file" done fi lbreakifs set for hit in `grep -E -v 'ERROR$|lstat()' $clamscan_results | sed -e 's/.UNOFFICIAL//' -e 's/ FOUND$//' | awk -F':' '{print$2":"$1}' | sed 's/.//'`; do file=`echo "$hit" | cut -d':' -f2` signame=`echo "$hit" | cut -d':' -f1` if [ ! -z "$(echo $signame | grep -E 'YARA')" ]; then signame=`echo "$signame" | sed 's/YARA\./{YARA}/'` elif [ -z "$(echo $signame | grep -E 'HEX|MD5')" ]; then signame="{CAV}$signame" fi ignore_hit=`echo $signame | grep -E -vf $ignore_sigs` if [ -f "$file" ] && [ ! -z "$ignore_hit" ]; then record_hit "$file" "$signame" quarantine "$file" "$signame" fi unset ignore_hit done lbreakifs unset scanned_count=`wc -l $monitor_scanlist | awk '{print$1}'` eout "{mon} scanned ${scanned_count} new/changed files with clamav engine" rm -f $clamscan_results $monitor_scanlist else for file in `cat $monitor_scanlist | tr ' ' '%'`; do file=`echo $file | tr '%' ' '` if [ -f "$file" ]; then for fscan in `$nice_command $find "$file" -maxdepth 1 $find_opts -type f -size +${scan_min_filesize}c -size -$scan_max_filesize -not -perm 000 $ignore_fext $ignore_root $ignore_user $ignore_group 2> /dev/null`; do if [ "$inotify_verbose" == "1" ]; then eout "{mon} inotify native file scan on $file" fi echo "$file" >> $monitor_scanned_history scan_stage1 "$fscan" >> /dev/null 2>&1 done fi done scanned_count=`wc -l $monitor_scanlist | awk '{print$1}'` eout "{mon} scanned ${scanned_count} new/changed files with native engine" rm -f $clamscan_results $monitor_scanlist fi } monitor_init() { inopt="$1" scan_session="$sessdir/session.hits.$datestamp.$$" touch $scan_session echo "$scan_session" > $sessdir/session.monitor.current if [ "$inopt" == "" ]; then eout "invalid usage of -m|--monitor, aborting." 1 exit fi if [ ! -f "$inotify" ]; then eout "{mon} could not find inotifywait command, install yum package inotify-tools or download from https://github.com/rvoicilas/inotify-tools/wiki/" 1 exit fi ksup=0 if [ -f "/boot/System.map-$(uname -r)" ]; then ksup=`grep inotify_ /boot/System.map-$(uname -r)` fi if [ -f "/boot/config-$(uname -r)" ]; then ksup=`grep -m1 CONFIG_INOTIFY /boot/config-$(uname -r)` fi if [ -z "$ksup" ]; then eout "{mon} kernel does not support inotify(), aborting." 1 exit fi inotify_pid=`pgrep -f inotify.paths.[0-9]+` if [ ! -z "$inotify_pid" ]; then eout "{mon} existing inotify process detected (try -k): $inotify_pid" 1 exit fi rm -f $tmpdir/stop_monitor $tmpdir/inotifywait.pid if [ -f "/proc/sys/fs/inotify/max_user_instances" ] && [ -f "/proc/sys/fs/inotify/max_user_watches" ]; then cur_user_watches=`cat /proc/sys/fs/inotify/max_user_watches` cur_user_instances=`cat /proc/sys/fs/inotify/max_user_instances` else eout "{mon} could not find fs.inotify.max_user_instances|watches tunable files, aborting." 1 exit fi users_tot=`cat /etc/passwd | grep -ic home` inotify_user_watches=$[inotify_base_watches*users_tot] if [ "$cur_user_instances" -lt "$inotify_user_instances" ]; then eout "{mon} set inotify max_user_instances to $inotify_user_instances" 1 echo $inotify_user_instances > /proc/sys/fs/inotify/max_user_instances fi if [ "$cur_user_watches" -lt "$inotify_user_watches" ]; then eout "{mon} set inotify max_user_watches to $inotify_user_watches" 1 echo $inotify_user_watches > /proc/sys/fs/inotify/max_user_watches fi icnt=0 inotify_fpaths="$sessdir/inotify.paths.$$" rm -f $inotify_fpaths touch $inotify_log chmod 640 $inotify_log if [ "$(echo $inopt | grep -iE 'user(s?)')" ]; then for i in `cat /etc/passwd | cut -d':' -f1,3,6 | sort`; do user=`echo $i | cut -d':' -f1` user_id=`echo $i | cut -d':' -f2` user_home=`echo $i | cut -d':' -f3` icnt=$[icnt+1] if [ "$user_id" -ge "$inotify_minuid" ]; then if [ ! -z "$inotify_docroot" ] && [ -d "$user_home" ]; then lbreakifs set for docroot in `echo $inotify_docroot | tr ', ' '\n'`; do if [ -d "$user_home/$docroot" ]; then echo "$user_home/$docroot" >> $inotify_fpaths eout "{mon} added $user_home/$docroot to inotify monitoring array" 1 fi done lbreakifs unset elif [ -d "$user_home" ]; then echo "$user_home" >> $inotify_fpaths eout "{mon} added $user_home to inotify monitoring array" 1 else eout "{mon} could not find any suitable user home paths" fi fi done if [ -d "/dev/shm" ]; then echo "/dev/shm" >> $inotify_fpaths eout "{mon} added /dev/shm to inotify monitoring array" 1 fi if [ -d "/var/tmp" ]; then echo "/var/tmp" >> $inotify_fpaths eout "{mon} added /var/tmp to inotify monitoring array" 1 fi if [ -d "/tmp" ]; then echo "/tmp" >> $inotify_fpaths eout "{mon} added /tmp to inotify monitoring array" 1 fi elif [ -f "$inopt" ]; then tot_paths=`$wc -l $inopt | awk '{print$1}'` if [ "$tot_paths" == "0" ]; then eout "{mon} no paths specified in $inopt, aborting." 1 exit fi for i in `cat $inopt`; do if [ -d "$i" ]; then eout "{mon} added $i to inotify monitoring array" 1 echo "$i" >> $inotify_fpaths else eout "{mon} ignored invalid path $i" 1 fi done elif [ -d "$inopt" ] || [ "$(echo $inopt | grep -E ".*,.*")" ]; then for i in `echo $inopt | tr ',' '\n'`; do if [ -d "$i" ]; then eout "{mon} added $i to inotify monitoring array" 1 echo "$i" >> $inotify_fpaths else eout "{mon} invalid path $i specified, ignoring." 1 fi done else eout "{mon} no valid option or invalid file/path provided, aborting." 1 exit fi if [ -f "$ignore_inotify" ] && [ -s "$ignore_inotify" ]; then for igfile in `cat $ignore_inotify | grep -vE '^$'`; do if [ "$igregexp" ]; then igregexp="$igregexp|$igfile" else igregexp="($igfile" fi done if [ "$igregexp" ]; then igregexp="$igregexp)" exclude="--exclude $igregexp" fi fi tot_paths=`$wc -l $inotify_fpaths | awk '{print$1}'` eout "{mon} starting inotify process on $tot_paths paths, this might take awhile..." 1 if [ ! "$inotify_cpunice" ]; then inotify_cpunice=19 fi if [ ! "$inotify_ionice" ]; then inotify_ionice=6 fi if [ -f "$nice" ]; then nice_command="$nice -n $inotify_cpunice" fi if [ -f "$ionice" ]; then nice_command="$nice_command $ionice -c2 -n $inotify_ionice" fi if [ -f "$cpulimit" ] && [ "$inotify_cpulimit" -gt 2> /dev/null "0" ]; then max_cpulimit=$[$(grep -E -w processor /proc/cpuinfo -c)*100] if [ "$inotify_cpulimit" -gt "$max_cpulimit" ]; then scan_cpulimit="0" else nice_command="$cpulimit -l $scan_cpulimit -- $nice_command" fi fi $nice_command $inotify -r --fromfile $inotify_fpaths $exclude --timefmt "%d %b %H:%M:%S" --format "%w%f %e %T" -m -e create,move,modify >> $inotify_log 2>&1 & sleep 2 inotify_pid=`pgrep -f inotify.paths.[0-9]+` if [ -z "$inotify_pid" ]; then eout "{mon} no inotify process found, check $inotify_log for errors." 1 exit else eout "{mon} inotify startup successful (pid: $inotify_pid)" 1 eout "{mon} inotify monitoring log: $inotify_log" 1 echo "$inotify_pid" > $tmpdir/inotifywait.pid fi monitor_cycle >> /dev/null 2>&1 & } checkout() { file="$1" host=ftp.rfxn.com user=anonymous@rfxn.com passwd=anonymous@rfxn.com upath=incoming cfile="$startdir/$file" if [ -f "$cfile" ]; then file="$cfile" fi if [ -f "$file" ]; then filename=`basename "$file" | tr -d '[:cntrl:]' | tr -d '[:space:]'` if [ -z "$filename" ]; then storename="$storename_prefix" else storename="$storename_prefix.$filename" fi eout "{checkout} uploading $file to $host" 1 (ftp -v -n -p -i $host || ftp -v -n -i $host) << EOT user $user@rfxn.com $passwd prompt cd $upath lcd $lcd binary put "$file" "$storename.bin" ascii put "$file" "$storename.ascii" bye EOT elif [ -d "$file" ]; then tmpf="$tmpdir/.co$$" find $file -type f > $tmpf cofiles=`wc -l $tmpf | awk '{print$1}'` if [ "$cofiles" -ge "25" ]; then eout "{checkout} path $file contains $cofiles, limit of 50 file uploads, aborting!" 1 rm -f $tmpf fi for i in `cat $tmpf`; do filename=`basename "$i" | tr -d '[:cntrl:]' | tr -d '[:space:]'` if [ -z "$filename" ]; then storename="$storename_prefix" else storename="$storename_prefix.$filename" fi (ftp -v -n -p -i $host || ftp -v -n -i $host) << EOT user $user $passwd prompt cd $upath lcd $lcd binary put "$i" "$storename.bin" ascii put "$i" "$storename.ascii" bye EOT done fi } gensigs() { runtime_ndb="$tmpdir/.runtime.user.$$.ndb" runtime_hdb="$tmpdir/.runtime.user.$$.hdb" runtime_hexstrings="$tmpdir/.runtime.hexsigs.$$" rm -f $runtime_ndb $runtime_hdb $runtime_hexstrings 2> /dev/null ln -fs $runtime_ndb $sigdir/lmd.user.ndb 2> /dev/null ln -fs $runtime_hdb $sigdir/lmd.user.hdb 2> /dev/null if [ -s "$sig_user_hex_file" ]; then cat "$sig_hex_file" "$sig_user_hex_file" | grep -vE '^\s*$' > $runtime_hexstrings else cat "$sig_hex_file" > $runtime_hexstrings fi for cp in $clamav_paths; do clamav_linksigs "$cp" done if [ "$scan_clamscan" == "1" ]; then if [ -s "$sig_user_hex_file" ]; then for i in `cat $sig_user_hex_file | sed 's/{HEX}//' | tr ':' '%' | grep -vE "^\s*$"`; do name=`echo $i | tr '%' ' ' | awk '{print$2}'` hex=`echo $i | tr '%' ' ' | awk '{print$1}'` if [ ! -z "$name" ] && [ ! -z "$hex" ]; then echo "{HEX}$name:0:*:$hex" >> $runtime_ndb fi done cat $sig_cav_hex_file >> $runtime_ndb else cp $sig_cav_hex_file $runtime_ndb fi if [ -s "$sig_user_md5_file" ]; then cat "$sig_user_md5_file" "$sig_md5_file" | grep -vE "^\s*$" | sort -u > $runtime_hdb else cp "$sig_cav_md5_file" "$runtime_hdb" fi fi } sigignore() { sil="$1" chk=`$wc -l $ignore_sigs | awk '{print$1}'` if [ ! "$chk" == "0" ]; then cat $sig_hex_file | grep -E -vf $ignore_sigs > $sig_hex_file.new mv $sig_hex_file.new $sig_hex_file cat $sig_md5_file | grep -E -vf $ignore_sigs > $sig_md5_file.new mv $sig_md5_file.new $sig_md5_file chmod 640 $sig_md5_file $sig_hex_file if [ "$sil" == "1" ] || [ "$hscan" == "1" ]; then eout "{glob} processed $chk signature ignore entries" else eout "{glob} processed $chk signature ignore entries" 1 fi fi } lmdup() { tmpwd="$tmpdir/.lmdup.$RANDOM.$$" upstreamver="$tmpwd/.lmdup_vercheck.$$" mkdir -p $tmpwd ; chmod 700 $tmpwd if [ "$lmdup_beta" ]; then lmd_hash_url="${lmd_hash_url}.beta" lmd_version_url="${lmd_version_url}.beta" lmd_current_tgzfile="maldetect-beta.tar.gz" fi eout "{update} checking for available updates..." 1 get_remote_file "$lmd_version_url" "update" "1" upstreamver="$return_file" if [ -s "$upstreamver" ]; then installedver=`echo $ver | tr -d '.'` if [ "$(echo $installedver | wc -L)" -eq "2" ]; then installedver="${installedver}0" fi upstreamver_readable=`cat $upstreamver` upstreamver=`cat $upstreamver | tr -d '.'` if [ "$(echo $upstreamver | wc -L)" -eq "2" ]; then upstreamver="${upstreamver}0" fi if [ "$upstreamver" -gt "$installedver" ]; then eout "{update} new version $upstreamver_readable found, updating..." 1 doupdate=1 elif [ "$lmdup_force" ]; then eout "{update} version update with --force requested, updating..." 1 doupdate=1 elif [ "$autoupdate_version_hashed" == "1" ]; then eout "{update} hashing install files and checking against server..." 1 $md5sum $inspath/maldet $intfunc | awk '{print$1}' | tr '\n' ' ' | tr -d ' ' > $lmd_hash_file upstreamhash="$tmpwd/.lmdup_hashcheck$$" get_remote_file "$lmd_hash_url" "update" "1" upstreamhash="$return_file" if [ -s "$upstreamhash" ]; then installed_hash=`cat $lmd_hash_file` current_hash=`cat $upstreamhash` if [ ! "$installed_hash" == "$current_hash" ]; then eout "{update} version check shows latest but hash check failed, forcing update..." 1 doupdate=1 else eout "{update} latest version already installed." 1 fi else eout "{update} could not download upstream hash file ($lmd_hash_url), please try again later." 1 cd $inspath ; rm -rf $tmpwd clean_exit exit 1 fi else eout "{update} no updates available, latest version already installed." 1 fi else eout "{update} could not download version file from server, please try again later." 1 cd $inspath ; rm -rf $tmpwd clean_exit exit 1 fi if [ "$doupdate" ]; then cd $tmpwd/ get_remote_file "${lmd_current_tgzbase_url}/${lmd_current_tgzfile}" "update" "1" "$tmpwd/${lmd_current_tgzfile}" get_remote_file "${lmd_current_tgzbase_url}/${lmd_current_tgzfile}.md5" "update" "1" "$tmpwd/${lmd_current_tgzfile}.md5" if [ -s "$tmpwd/${lmd_current_tgzfile}.md5" ] && [ -s "$tmpwd/${lmd_current_tgzfile}" ]; then upstream_md5=`cat $tmpwd/${lmd_current_tgzfile}.md5 | awk '{print$1}'` local_md5=`$md5sum "$tmpwd/${lmd_current_tgzfile}" | awk '{print$1}'` if [ ! "$upstream_md5" == "$local_md5" ]; then eout "{update} unable to verify md5sum of ${lmd_current_tgzfile}, update failed!" 1 cd $inspath ; rm -rf $tmpwd clean_exit exit 1 else eout "{update} verified md5sum of ${lmd_current_tgzfile}" 1 fi else eout "{update} could not download ${lmd_current_tgzfile} or .md5, please try again later." 1 cd $inspath ; rm -rf $tmpwd clean_exit exit 1 fi if [ -s "$tmpwd/${lmd_current_tgzfile}" ]; then tar xfz ${lmd_current_tgzfile} rm -f ${lmd_current_tgzfile} ${lmd_current_tgzfile}.md5 cd maldetect-${upstreamver_readable} chmod 750 install.sh sh -c './install.sh' >> /dev/null 2>&1 cp -f $inspath.last/sigs/custom.* $sigdir/ 2> /dev/null cp -f $inspath.last/clean/custom.* $inspath/clean/ 2> /dev/null eout "{update} completed update v$ver ${installed_hash:0:6} => v$upstreamver_readable ${upstream_md5:0:6}, running signature updates..." 1 $inspath/maldet --update 1 eout "{update} update and config import completed" 1 else eout "{update} could not download ${lmd_current_tgzfile}, please try again later." 1 cd $inspath ; rm -rf $tmpwd clean_exit exit 1 fi fi cd $inspath ; rm -rf $tmpwd } sigup() { eout "{sigup} performing signature update check..." 1 tmpwd="$tmpdir/.sigup.$RANDOM.$$" mkdir -p $tmpwd ; chmod 700 $tmpwd import_user_sigs if [ -z "$sig_version" ]; then eout "{sigup} could not determine signature version" 1 sig_version=0 else eout "{sigup} local signature set is version $sig_version" 1 fi get_remote_file "$sig_version_url" "sigup" "1" upstream_sigver="$return_file" if [ ! -f "$upstream_sigver" ] || [ ! -s "$upstream_sigver" ]; then eout "{sigup} could not download signature data from server, please try again later." 1 clean_exit exit 1 else nver=`cat $upstream_sigver` fi if [ -f "$sig_md5_file" ]; then lines_md5=`$wc -l $sig_md5_file | awk '{print$1}'` else lines_md5=0 fi if [ -f "$sig_hex_file" ]; then lines_hex=`$wc -l $sig_hex_file | awk '{print$1}'` else lines_hex="0" fi if [ ! -f "$sig_md5_file" ] || [ ! -f "$sig_hex_file" ]; then sig_version=2012010100000 eout "{sigup} signature files missing or corrupted, forcing update..." 1 elif [ "$lines_md5" -lt "1000" ] || [ "$lines_hex" -lt "1000" ]; then sig_version=2012010100000 eout "{sigup} signature files corrupted, forcing update..." 1 elif [ "$sigup_force" ]; then sig_version=2012010100000 eout "{sigup} signature update with --force requested, forcing update..." 1 fi if [ "$nver" != "$sig_version" ]; then cd $tmpwd/ tar=`command -v tar 2> /dev/null` eout "{sigup} new signature set $nver available" 1 eout "{sigup} downloading $sig_sigpack_url" 1 get_remote_file "$sig_sigpack_url" "sigup" "1" "$tmpwd/maldet-sigpack.tgz" get_remote_file "${sig_sigpack_url}.md5" "sigup" "1" "$tmpwd/maldet-sigpack.tgz.md5" eout "{sigup} downloading $sig_clpack_url" 1 get_remote_file "$sig_clpack_url" "sigup" "1" "$tmpwd/maldet-clean.tgz" get_remote_file "${sig_clpack_url}.md5" "sigup" "1" "$tmpwd/maldet-clean.tgz.md5" if [ -f "$tmpwd/maldet-sigpack.tgz.md5" ]; then sigpack_md5=`$md5sum maldet-sigpack.tgz | awk '{print$1}'` sigpack_goodmd5=`cat maldet-sigpack.tgz.md5 | awk '{print$1}'` if [ ! "$sigpack_md5" == "$sigpack_goodmd5" ]; then eout "{sigup} unable to verify md5sum of maldet-sigpack.tgz, please try again or contact proj@rfxn.com" 1 sigpackfail=1 else eout "{sigup} verified md5sum of maldet-sigpack.tgz" 1 if [ -f "$tmpwd/maldet-sigpack.tgz" ] && [ -s "$tmpwd/maldet-sigpack.tgz" ]; then tar xfz $tmpwd/maldet-sigpack.tgz 2> /dev/null if [ -d "$tmpwd/sigs" ]; then mkdir -p $sigdir.old 2> /dev/null rm -f $sigdir.old/* 2> /dev/null cp -f $sigdir/* $sigdir.old/ 2> /dev/null cp -f $tmpwd/sigs/* $sigdir 2> /dev/null eout "{sigup} unpacked and installed maldet-sigpack.tgz" 1 for cp in $clamav_paths; do clamav_linksigs "$cp" done killall -SIGUSR2 clamd 2> /dev/null else eout "{sigup} something went wrong unpacking $sig_sigpack_url, aborting!" 1 sigpackfail=1 fi else eout "{sigup} could not download $sig_sigpack_url" 1 sigpackfail=1 fi fi else eout "{sigup} could not download ${sig_sigpack_url}.md5" 1 sigpackfail=1 fi if [ -f "$tmpwd/maldet-clean.tgz.md5" ]; then clpack_md5=`$md5sum maldet-clean.tgz | awk '{print$1}'` clpack_goodmd5=`cat maldet-clean.tgz.md5 | awk '{print$1}'` if [ ! "$clpack_md5" == "$clpack_goodmd5" ]; then eout "{sigup} unable to verify md5sum of maldet-clean.tgz, please try again or contact proj@rfxn.com" 1 clpackfail=1 else eout "{sigup} verified md5sum of maldet-clean.tgz" 1 if [ -f "$tmpwd/maldet-clean.tgz" ] && [ -s "$tmpwd/maldet-clean.tgz" ]; then tar xfz $tmpwd/maldet-clean.tgz cp -f $tmpwd/clean/* $cldir eout "{sigup} unpacked and installed maldet-clean.tgz" 1 else eout "{sigup} error handling $sig_clpack_url, file is either missing or zero sized, aborting!" 1 clpackfail=1 fi fi else eout "{sigup} could not download ${sig_sigpack_url}.md5" 1 clpackfail=1 fi if [ "$sigpackfail" ]; then cd $inspath rm -rf $tmpwd clean_exit exit 1 else eout "{sigup} signature set update completed" 1 sigignore hex_sigs=`$wc -l $sig_hex_file | awk '{print$1}'` md5_sigs=`$wc -l $sig_md5_file | awk '{print$1}'` yara_sigs=`grep -E -c ^rule $sig_yara_file | awk '{print$1}'` if [ ! -f "$sig_user_md5_file" ]; then user_hex_sigs=0 else user_hex_sigs=`$wc -l $sig_user_hex_file | awk '{print$1}'` fi if [ ! -f "$sig_user_hex_file" ]; then user_md5_sigs=0 else user_md5_sigs=`$wc -l $sig_user_md5_file | awk '{print$1}'` fi user_sigs=$[user_hex_sigs+user_md5_sigs] tot_sigs=$[md5_sigs+hex_sigs+user_hex_sigs+user_md5_sigs+yara_sigs] eout "{sigup} $tot_sigs signatures ($md5_sigs MD5 | $hex_sigs HEX | $yara_sigs YARA | $user_sigs USER)" 1 fi cd $inspath rm -rf $tmpwd else eout "{sigup} latest signature set already installed" 1 cd $inspath rm -rf $tmpwd fi } postrun() { rm -f $find_results $scan_session $runtime_ndb $runtime_hdb $runtime_hexstrings $clamscan_results $tmpdir/.tmpf* 2> /dev/null if [ ! "$tot_hits" ]; then exit 0 elif [ "$tot_hits" == "0" ]; then exit 0 elif [ "$tot_hits" -ge "1" ]; then exit 2 fi }