#!/bin/bash if [ "$1" == 'debug' ];then set -x; shift; fi if [ "$1" == 'debug2' ];then set -xvT; shift; fi #------------------------------------------------- # - WatchLG - # Watch for login break-in attempts # Record in database ... and inject into firewall # after maximum 'affairs' was reached #------------------------------------------------- REALPATH=`realpath $0` WHERE=`dirname $REALPATH` ME=`basename $REALPATH` cd $WHERE . ../../system.conf . ../../watcher.conf # needed for debugging . ../../common.conf . ../../common.bashlib #----------------------- . $ME.conf . private.bashlib : echo $PATH THIS_IP=`dig +short $HOSTNAME` # BASH knows HOSTNAME trap refresh HUP # React on a 'HUP' for the filter refresh trap stopservice TERM INT # React on a 'TERM' or CTL-C when debugging trap rerun KILL 9 # React on crash trap 'errorexit $? $_' EXIT load_filter() { # $1 .. filter status # $2 .. compress flag; i.e. compress it not empty local funtag="[${FUNCNAME[0]}]" unset -f filter [[ -f filter ]] && mv filter filter.old [[ -f filter.compress ]] && rm -f filter.compress ./mkfilter $1 if [ -f filter.old ] then diff -c filter filter.old > filter-diffs else [[ -f filter-diffs ]] && rm -f filter-diffs fi # If the 'compress filter' flag is set compress filter; # i.e. remove comments and empty lines for runtime if [ ! -z "$2" ] then trace "$funtag Using filter compressed" compress_filter . filter.compress else trace "$funtag Using filter plain" . filter fi # Create/refresh superfluous_map as well SUPERFLUOUS=`mk_superfluous` } refresh() { local funtag="[${FUNCNAME[0]}]" trace "$funtag Reloading filters for $ME ..." logger "$ME[$$]: $funtag Reloading filters for $ME ..." load_filter "$funtag `date --iso=seconds`" $COMPRESS_FILTER } errorexit() { local funtag="[${FUNCNAME[0]}]" logger "$ME[$$]: $funtag trapped with exit code '$1', Cmd: '$2' ... Loop: $loop" trace "$funtag $ME trapped with exit code '$1', Cmd: '$2' ... Loop: $loop" exit 255 } stopservice() { local funtag="[${FUNCNAME[0]}]" logger "$ME[$$]: $funtag Terminating on request ..." trace "$funtag Terminating on request ..." exit } rerun() { local funtag="[${FUNCNAME[0]}]" log "$ME: $funtag Abnormal termination, exit status: $?, Loop; $loop" # log "Rule was: $RULE" # log "Bandit : $BANDIT" logger "$ME[$$]: Restarting in a minute ..." trace "$funtag $ME Restarting in a minute ..." echo "$WHERE/$ME" | at "now + 1 minute" exit } # # - inject - # ... is directly called by a 'rule' and the global variable $RULE is set though. # The log line is in the global variable $REPLY. So nothing must get passed. # All the dirty work is done here. # inject() { local funtag="[${FUNCNAME[0]}]" local penalty=${1:-1} local retcode=0 local affairs local haveit local isdropped trace "$funtag Triggered by rule [$RULE] '$Pattern'" BANDIT=`get_bandit` if ipset -q test whitelist "$BANDIT" then return 3 # Flag whitelisted IP address fi validate_IP "$BANDIT" if [ $? -ne 0 ] then trace "$funtag: Junk IP address for bandit '$BANDIT' ... bailing out" return 255 fi # See if we have an NXDOMAIN or FAKEHOST here # CLASS=`get_class $BANDIT` #PORT=`echo "$REPLY" | grep -o 'port [0-9]*' |grep -o '[0-9]*'` #trace "$funtag $CLASS $BANDIT Port: $PORT" trace "$funtag $CLASS $BANDIT" # # OK ... 'observed candidate' ... or ... 'new candidate' ?? # haveit=`$SQL "select IP from bandits where IP='$BANDIT'"` if [ -z "$haveit" ] then # # Introduce the bandit (insert in to database) # # Note: # NXDOMAINs & FAKEHOSTS are killed on 2nd attempt; i.e. they get 'MAX_AFFAIRS - 1' # on 1st attempt. The first attempt is needed to detect and identify the bandit # if [[ "NXDOMAIN FAKEHOST" =~ $CLASS ]] then penalty=$((MAX_AFFAIRS - 1)) fi AFFAIRS=$penalty STATE=INTRO ACTION="Initial $AFFAIRS/$MAX_AFFAIRS, Penalty: $penalty" if [ ! -z "$GEOTRACK" ]; then do_geotrack; fi $SQL "insert into $TABLE (affairs,IP,type,state,date_intro,date_event,class,comment) values ($AFFAIRS,'$BANDIT','$THIS_TYPE','$STATE', datetime(current_timestamp, 'localtime'), datetime(current_timestamp, 'localtime'), '$CLASS', '$THIS_TYPE,$CLASS');" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 else # Already got a DROP? # If so kick to bin permanently isdropped=`$SQL "select state from $TABLE where IP='$BANDIT' and state='DROP';"` if [ ! -z "$isdropped" ] then trace "$funtag DROPed culprit $BANDIT re-occured ... taken into custody" $IPSET -exist add custody $BANDIT comment "$THIS_TYPE,$CLASS" # Take the short way out ... return 1 fi # # ... A known bandit (UPDATE) # AFFAIRS=`$SQL "select affairs from $TABLE where IP='$BANDIT';"` ((AFFAIRS++)) if [ $AFFAIRS -lt $MAX_AFFAIRS ] then STATE=COUNT ACTION="Count $AFFAIRS/$MAX_AFFAIRS" $SQL "update $TABLE set affairs=$AFFAIRS, class='$CLASS', state='$STATE', comment='Login,$ACTION', date_event=datetime(current_timestamp, 'localtime') where IP='$BANDIT';" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 else # Mark a DROP in the database drop if MAX_AFFAIRS is reached ... STATE=DROP ACTION="Dropped @ $AFFAIRS" $SQL "update $TABLE set affairs=$AFFAIRS, state='$STATE', date_event=datetime(current_timestamp, 'localtime'), class='$CLASS', comment='$THIS_TYPE,$CLASS' where IP='$BANDIT';" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 fi fi write_affairs $BANDIT $AFFAIRS $RULE clog "$funtag" "$CLASS" "$BANDIT" "'$ACTION'" #log "$funtag $CLASS $BANDIT '$ACTION'" trace "$funtag $CLASS $BANDIT '$ACTION'" return $retcode } # ----------------------- Main program --------------------------- dump_runtime ln -sf $WHERE/LGinjector $TOOLS_LINK trace "--- $ME started on $HOSTNAME ... $SYSTEM $SYSVERS" trace "Watcher: $PRODUCT, $REVISION" if [ -z "$TIME_SLICE" ]; then TIME_SLICE=60; fi touch $LOG chmod 600 $LOG touch $ME.trace chmod 600 $ME.trace if [ ! -p $PIPE ]; then mkfifo -m 600 $PIPE; fi # # Load generated 'filter' function # load_filter initial $COMPRESS_FILTER # # Start scanning ... # logger "$ME[$$]: Started listening on pipe $PIPE using dynamic filters" log "Started listening on pipe $PIPE using dynamic filters" trace "Started listening on pipe $PIPE using dynamic filters" renice -10 $$ trace "Starting scanner ..." while : # Only a crash can stop us do read < $PIPE LOOP_START=`date +%s%3N` read DYN_ADDR < $LOG_DIR/DYN_IP ((loop++)) # Count loops : echo "--------[ Loop: $loop ]----------" if [ -z "$REPLY" ] then : echo "Eeek! Empty line ..." continue fi # ------ Internal Rule set -------------- # # Kick off any junk ... # # Must come FIRST of all RULE=Superfluous # SUPER=`grep -oE "$SUPERFLUOUS" <<< $REPLY` # if [ ! -z "$SUPER" ] [[ "$REPLY" =~ ($SUPERFLUOUS) ]] if [ -n "${BASH_REMATCH[0]}" ] then : echo "Superfluous stuff ... ignored ..." continue fi RULE=Freehost if [[ "$REPLY" =~ "@Freehost:" ]] then DEL_ADDR=`get_bandit` : trace "$ME/$RULE Deleting $DEL_ADDR from $TABLE" freehost $DEL_ADDR continue fi RULE=Self if [[ "$REPLY" =~ "$THIS_IP" ]] then : trace "$ME/$RULE Oops ... me $THIS_IP" continue fi RULE=DynIP if [[ "$REPLY" =~ "$DYN_ADDR" ]] then : trace "$ME/$RULE Oops ... my dyn IP $DYN_ADDR" continue fi # Careful here $TRUSTED is a list of IP addresses RULE=Trusted if [[ "$REPLY" =~ ($TRUSTLIST) ]] then : trace "$ME/$RULE Oops ... ${BASH_REMATCH[0]} in trusted hosts ... skipping" continue fi # Log lines w/o IP make no sense RULE=IPpresent BANDIT=`get_bandit` if [ -z "$BANDIT" ] then : trace "[No IP addr] ... skipped" continue fi #-------------------------------------- dump_loadrate trace "[Loop: $loop] '$REPLY'" FILT_START=`date +%s%3N` filter # Returns with $BANDIT, $CLASS, $TAG, $ACTION and $RULE set FILT_RESULT=$? # Keep retcode save for the post processor FILT_END=`date +%s%3N` FILT_TIME=$(( FILT_END-FILT_START )) LOOP_TIME=$(( FILT_END-LOOP_START )) # # Post processor # case $FILT_RESULT in 0) trace "[postpro] $RULE ... fall through, $LOOP_TIME/$FILT_TIME ms" ;; 1) trace "[postpro] Finished for rule [$RULE], $LOOP_TIME/$FILT_TIME ms" continue ;; 2) trace "[postpro] Handled outside of $ME" continue ;; 3) trace "[postpro] $BANDIT whitelisted ... nothing to do ..." continue ;; 254) trace "[postpro] Ignored ... [$RULE] '$Pattern'" continue ;; 255) trace "[postpro] !!! Nasty things happend by rule '$RULE' for '$BANDIT'!!!" ;; *) trace "[postpro] Illegal return code '$FILT_RESULT' ... fall through" ;; esac #-------------[ Catch all before loop finishes ]----------- RULE=UNTREATED log "[$RULE] '$REPLY'" trace "[$RULE] '$REPLY'" done # DEAD-End ... Reaching here triggers a trap that restarts us rerun