This page looks best with JavaScript enabled

libmodsecurity under FreeBSD + Nginx

 ·  🎃 kr0m

 
As we explained in previous articles, libmodsecurity is a WAF (Web Application Firewall) that allows us to detect certain types of attacks based on predefined rules. Through these signatures, we can detect SQL injections, XSS, LFI, RFI. This time we are going to set up a system in which we will be notified via Telegram when an attack is detected, PF rules will be applied, and a website will be displayed informing about the reason for the ban.

The article is divided into several sections:


Compiling Nginx

If we already have Nginx installed, we must check if it was compiled with support for libmodsecurity:

pkg info nginx|grep MODSECURITY3

 MODSECURITY3 : off

NOTE: The full version (nginx-full) also does not have the option enabled.


In versions prior to June 03, 2020 18:49:04, Nginx had to be compiled with support for modsecurity3, have the modsecurity3 library installed, and also install the modsecurity3-nginx connector. In versions after that date, if we compile Nginx with support for modsecurity3, the connector is already included, but we must add the module loading command in the Nginx configuration. In the old version, this step was unnecessary.

r537834 | joneum | 2020-06-03 20:49:04 +0200 (Wed, 03 Jun 2020) | 11 lines

Merge r532727 from www/nginx-devel:

Convert the following third-party modules to dynamic:

o) accept_language
o) modsecurity3-nginx

Fix the third-party auth_krb5 module build.

Sponsored by: Netzkommune GmbH

If we install modsecurity3-nginx with the new version of Nginx and load the module from the Nginx configuration, we will see the following error:

2020/06/06 14:46:36 [emerg] 24011#101851: module "/usr/local/libexec/nginx/ngx_http_modsecurity_module.so" is not binary compatible in /usr/local/etc/nginx/nginx.conf:4

In addition, pkg will warn us that the modsecurity3-nginx package and nginx conflict with each other since both try to install the same connector.


We compile using ports with the options we want, making sure to enable modsecurity3. Remember that once we switch to ports, we have to manage all packages from ports. You can find more information in this article earlier.

Before compiling and installing Nginx from ports, we uninstall the current version.

pkg delete nginx

We prepare the ports system:

portsnap fetch extract
cd /usr/ports
make fetchindex

We perform a search:

make search name=nginx

Port:	nginx-1.22.1_5,3
Path:	/usr/ports/www/nginx
Info:	Robust and small WWW server
Maint:	joneum@FreeBSD.org
B-deps:	pcre-8.45_3
R-deps:	pcre-8.45_3
WWW:	https://nginx.com/

We configure the options:

cd /usr/ports/www/nginx
make config

We compile and install:

make -j2
make install

We clean temporary files:

make clean


Installing libmodsecurity

We install the libmodsecurity library:

cd /usr/ports/security/modsecurity3
make config
make
make install
make clean

We download the OWASP rules:

cd /usr/local
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
cd owasp-modsecurity-crs
cp crs-setup.conf.example crs-setup.conf

We download the base configuration file for libmodsecurity:

We make sure to have certain parameters with the indicated values:

vi /usr/local/etc/modsec/modsecurity.conf

SecRuleEngine On
SecAuditLogFormat json
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log

We include the base file and OWASP rules in the main configuration:

vi /usr/local/etc/modsec/main.conf

# Include the recommended configuration
Include /usr/local/etc/modsec/modsecurity.conf

# OWASP CRS v3 rules
Include /usr/local/owasp-modsecurity-crs/crs-setup.conf
Include /usr/local/owasp-modsecurity-crs/rules/*.conf

We enable libmodsecurity in the Nginx configuration by adding the module load as the first line:

vi /usr/local/etc/nginx/nginx.conf

load_module /usr/local/libexec/nginx/ngx_http_modsecurity_module.so;
vi /usr/local/etc/nginx/alfaexploit.conf
server {
 ...
 modsecurity on;
 modsecurity_rules_file /usr/local/etc/modsec/main.conf;

We restart the service:

service nginx restart

We test any type of attack, in my case a MySQL injection with the following payload:

' or '1==1'; --

We will see a new entry in the log and the web request will have been blocked with a 403 Forbidden:

tail -f /var/log/modsec_audit.log

If we are going to generate many entries in the log, it is preferable to write them in concurrent format, so that they will not be logged in a serialized text file, but rather in different files in parallel:

vi /usr/local/etc/modsec/modsecurity.conf

#SecAuditLogType Serial
SecAuditLogType Concurrent
#SecAuditLog /var/log/modsec_audit.log
SecAuditLogStorageDir /opt/modsecurity/var/audit

We restart the service:

service nginx restart

We create the directory where to store the log entries:

mkdir -p /opt/modsecurity/var/audit/
chown -R www:www /opt/modsecurity/var/audit/
chmod 775 /opt/modsecurity/var/audit/

We install the jq tool, which will be useful for us to view the log information more comfortably:

cd /usr/ports/textproc/jq
make config
make
make install
make clean

We check the message field of one of the entries:

cat /opt/modsecurity/var/audit/20230225/20230225-0902/20230225-090220-167731214042.620793 | jq ‘.transaction.messages[].message’

"SQL Injection Attack Detected via libinjection"
"Inbound Anomaly Score Exceeded (Total Score: 5)"

NOTE: It is not necessary to filter if we want to see the complete information, we simply pipe the output of cat to a jq without parameters.


Whitelisting URLs

If there is any functionality in our web application that requires a slightly unusual behavior, we can whitelist it so that the rules do not trigger when that section is being processed:

cp /usr/local/owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example /usr/local/owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
vi /usr/local/owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

SecRule REQUEST_URI "@beginsWith /CUSTOM_WEB_PATH" \
    "id:1001,\
    phase:1,\
    pass,\
    nolog,\
    ctl:ruleEngine=Off"

Disabling rules

If, on the other hand, we want to disable a specific rule, we can use the SecRuleRemoveById directive.

NOTE: This directive must be specified after the rule being disabled.

In my case, I was having problems with the rule:

CGI source code leakage
"ruleId": "950140",
          "file": "/usr/local/owasp-modsecurity-crs/rules/RESPONSE-950-DATA-LEAKAGES.conf",
          "lineNumber": "66",

When displaying code on the web, the alarm goes off because it thinks that the source code of the page itself is being filtered. We add the file of disabled rules:

vi /usr/local/etc/modsec/main.conf

# Include the recommended configuration
Include /usr/local/etc/modsec/modsecurity.conf

# OWASP CRS v3 rules
Include /usr/local/owasp-modsecurity-crs/crs-setup.conf
Include /usr/local/owasp-modsecurity-crs/rules/*.conf

# Disabled rules
Include /usr/local/etc/modsec/disabledRules.conf

We indicate which rule to disable:

vi /usr/local/etc/modsec/disabledRules.conf

SecRuleRemoveById 950140

However, if we want to disable it only for an IP (necessary if we enable the check in a Ha-Proxy):

vi /usr/local/etc/modsec/disabledRules.conf

SecRule REMOTE_ADDR "@ipMatch IP_ADDRESS" "id:1,phase:1,t:none,nolog,pass,ctl:ruleRemoveById=920280"
service nginx restart

Cleaning logs

A good idea is to delete the libmodsecurity logs every X time, for which I have the following CRON enabled:

00 11 * * * /bin/rm -rf /usr/local/bastille/jails/MetaCortex/root/opt/modsecurity/var/audit/* >/dev/null 2>&1

Log analysis and banning system

The system consists of a Redis server and several Python scripts:

Depending on our topology, the scripts will be executed on one server or another. In my case, all services are mounted on the same physical server, so I will execute the modsecurityAnalizer.py and modsecurityNotifier.py scripts on this one, since by blocking through PF I will be filtering for all servers simultaneously.
In case of having a more distributed infrastructure, we must execute modsecurityAnalizer.py on the Nginx where the attacks are received, modsecurityNotifier.py on the Ha-Proxy and modsecurityPF.py on the rest of the servers we want to protect.

All services on the same parent host Separated services

The steps for attack banning are:

  • Generate logs using libmodsecurity on the Nginx server.
  • Read this information from the parent server or the Nginx server and insert it into Redis.
  • Read the Redis data from the parent server or Ha-Proxy and configure the rules of PF , also keeping an ACL and a Ha-Proxy list file synchronized.
  • Ha-Proxy sends attackers that match the “badguys” ACL to a special backend.
  • Attackers are served with a web app that shows information about the attacker, the reason, and the remaining ban time.
  • If the infrastructure is distributed, we will execute the modsecurityPF.py script on the rest of the servers, which will simply read from Redis and apply PF rules.

Installing redis

To allow the different scripts to communicate, we will install a Redis server:

pkg install -y redis

We bind it to its IP and assign a password:

vi /usr/local/etc/redis.conf

bind 192.168.69.2
requirepass XXXXXXXXXX

We enable and start the service:

sysrc redis_enable=yes
service redis start

We check the Redis status:

redis-cli -h 192.168.69.2 -a XXXXXXXXXX info

# Server
redis_version:7.0.8
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:8e3f49544e856f48
redis_mode:standalone
os:FreeBSD 13.1-RELEASE-p5 amd64
.......

Script modsecurityAnalizer.py

With this script, we will analyze the logs generated by libmodsecurity and generate the associated keys in Redis.

vi /root/.scripts/modsecurityAnalizer.py

#!/usr/local/bin/python

import sys
import time
from datetime import timedelta
import os
import json
import requests
import redis
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

apikey = "XXXXXXXXXX"
telegramurl = "https://api.telegram.org/bot{}/sendMessage".format(apikey)
userid = "XXXXXXXXXX"
haproxy_ip = '192.168.69.19'

try:
    redisconnection = redis.Redis(host="192.168.69.2", port=6379, db=0, password='XXXXXXXXXX')
    redisconnection.ping()
except:
    print('++ ERROR: Cant connect to redis server')
    msg = 'ERROR ' + os.uname().nodename + '(modsecurityAnalizer.py): Cant connect to redis server'
    data = {"chat_id":userid, "text":msg}
    try:
        r = requests.post(telegramurl,json=data)
    except:
        print("++ Error sending telegram message")
    quit()

print("-- Ready to process log files")
class EventHandler(FileSystemEventHandler):
    def on_any_event(self, event):
        #print("Event type: %s" % event.event_type)
        #print("Event file: %s" % event.src_path)
        if event.event_type == 'created':
            if os.path.isfile(event.src_path):
                print('')
                print('------------------------------------------------')
                print("Processing: %s" % event.src_path)
                with open(event.src_path) as fp:
                    for line in fp:
                        #print('INSIDE LOOP1: Log lines')
                        try:
                            rawdata = json.loads(line)
                        except:
                            print('Exception happened loading json data')
                            continue

                        for messageline in rawdata['transaction']['messages']:
                            #print('INSIDE LOOP2: messages')
                            message = messageline['message']
                            # Each alert generates two messages one of the real alert another with 'Out/In bound Anomaly Score Exceeded (Total Score: 5)'
                            if message == 'Outbound Anomaly Score Exceeded (Total Score: 5)' or message == 'Inbound Anomaly Score Exceeded (Total Score: 5)':
                                continue
                            print('=====================================')
                            #print('Message: %s' % message)
                            data = messageline['details']['data']
                            #print('data: %s' % data)
                            # Delete not matched rules messages and anomaly score checks
                            if message != "":
                                try:
                                    timestamp = rawdata['transaction']['time_stamp']
                                except:
                                    timestamp = 'NULL'
                                try:
                                    attacker = rawdata['transaction']['request']['headers']['x-forwarded-for']
                                except:
                                    attacker = rawdata['transaction']['client_ip']
                                    if attacker == haproxy_ip:
                                        continue
                                    #attacker = 'NULL'
                                try:
                                    useragent_platform = rawdata['transaction']['request']['headers']['sec-ch-ua-platform']
                                except:
                                    useragent_platform = 'NULL'
                                try:
                                    useragent = rawdata['transaction']['request']['headers']['user-agent']
                                except:
                                    useragent = 'NULL'
                                try:
                                    host = rawdata['transaction']['request']['headers']['host']
                                except:
                                    host = 'NULL'
                                try:
                                    url = rawdata['transaction']['request']['uri']
                                except:
                                    url = 'NULL'
                                try:
                                    method = rawdata['transaction']['request']['method']
                                except:
                                    method = 'NULL'
                                try:
                                    payload = messageline['details']['data']
                                except:
                                    payload = 'NULL'
                                
                                print('')
                                print(">> Timestamp: %s" % timestamp)
                                print("Attacker: %s" % attacker)
                                print("UserAgentPlatform: %s" % useragent_platform)
                                print("UserAgent: %s" % useragent)
                                print("Message: %s" % message)
                                print("Host: %s" % host)
                                print("URL: %s" % url)
                                print("Method: %s" % method)
                                #print("Payload: %s" % payload)

                                print(">> Checking redis IP: %s" % attacker)
                                print("Incrementing redis key value for IP: %s" % attacker)
                                if redisconnection.get(attacker):
                                    redisconnection.incr(attacker)
                                else:
                                    redisconnection.incr(attacker)
                                    redisconnection.expire(attacker, timedelta(seconds=3600))

                                rediscounter = redisconnection.get(attacker)
                                print("rediscounter: %s" % rediscounter)

                                print(">> Filling attacker data")
                                datastring = timestamp + '++' + attacker + '++' + useragent_platform + '++' + useragent + '++' + message + '++' + host + '++' + url + '++' + method + '++' + payload
                                redisconnection.set('data' + attacker, datastring)
                                redisconnection.expire('data' + attacker, timedelta(seconds=3600))
                                db_datastring = redisconnection.get('data' + attacker)
                                #print("- datastring: %s" % datastring)
                                #print("- db_datastring: %s" % db_datastring)
                                print(">> Done")

if __name__ == "__main__":
    path = '/usr/local/bastille/jails/MetaCortex/root/opt/modsecurity/var/audit/'

    event_handler = EventHandler()
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

We give it execution permissions:

chmod 700 /root/.scripts/modsecurityAnalizer.py

The service management RC script would be as follows:

vi /usr/local/etc/rc.d/modsecurityAnalizer

#!/bin/sh
#
# PROVIDE: modsecurityAnalizer
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name=modsecurityAnalizer
rcvar=modsecurityAnalizer_enable

command="/root/.scripts/modsecurityAnalizer.py"
start_cmd="modsecurityAnalizer_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
pidfile="/var/run/${name}.pid"

modsecurityAnalizer_start(){
    echo "Starting service: ${name}."
    /usr/sbin/daemon -c -f -p ${pidfile} ${command}
}

modsecurityAnalizer_stop(){
    if [ -f ${pidfile} ]; then
        echo "Stopping service: ${name}"
        kill -s TERM $(cat ${pidfile})
        sleep 3
    else
        echo "It appears ${name} is not running."
    fi
}

modsecurityAnalizer_status(){
    if [ -f ${pidfile} ]; then
        echo "${name} running with PID: $(cat ${pidfile})"
    else
        echo "It appears ${name} is not running."
    fi
}

load_rc_config $name
run_rc_command "$1"

We assign permissions:

chmod 555 /usr/local/etc/rc.d/modsecurityAnalizer
chown root:wheel /usr/local/etc/rc.d/modsecurityAnalizer

We enable the service and start it:

sysrc modsecurityAnalizer_enable=“yes”
service modsecurityAnalizer start


Script modsecurityNotifier.py

This script will query Redis, notify via Telegram of the received attacks, configure PF rules, and maintain an ACL and a Ha-Proxy list with the attackers’ IPs.
The reason why we will configure ACLs and lists simultaneously is that the ACLs will be inserted by Ha-Proxy’s SocketAdmin, and when Ha-Proxy is restarted, the ACLs disappear. To avoid this, we will also maintain the /usr/local/etc/bagguys.list list so that in case of a restart, the list will contain the same IPs as those in the ACL.

vi /root/.scripts/modsecurityNotifier.py
#!/usr/local/bin/python

import redis
import requests
import subprocess
import time
import os
import re
from haproxyadmin import haproxy

apikey = "XXXXXXXXXX"
telegramurl = "https://api.telegram.org/bot{}/sendMessage".format(apikey)
userid = "XXXXXXXXXX"
HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/bagguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = ".*\(/usr/local/etc/bagguys\.list\) pattern loaded from file '/usr/local/etc/bagguys\.list'.*"

try:
    redisconnection = redis.Redis(host="192.168.69.2", port=6379, db=0, password='XXXXXXXXXX', charset="utf-8", decode_responses=True)
    redisconnection.ping()
except:
    print('++ ERROR: Cant connect to redis server')
    msg = 'ERROR ' + os.uname().nodename + '(modsecurityNotifier.py): Cant connect to redis server'
    data = {"chat_id":userid, "text":msg}
    try:
        r = requests.post(telegramurl,json=data)
    except:
        print("++ Error sending telegram message")
    quit()

while(True):
    print('')
    print('------------------------------------------------')
    print('>> Checking REDIS content')
    print('------------------------------------------------')
    for attacker in redisconnection.scan_iter():
        #print("-- Analyzing key: %s" % attacker)
        if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
            continue
        else:
            rediscounter = redisconnection.get(attacker)
            print("Attacker: %s Counter: %s" % (attacker, rediscounter))
            if int(rediscounter) >= 5 and redisconnection.get('data' + attacker):
                data = redisconnection.get('data' + attacker)
                #print("Data: %s" % data)
                timestamp = data.split('++')[0]
                print("timestamp: %s" % timestamp)
                attacker = data.split('++')[1]
                print("attacker: %s" % attacker)
                useragent_platform = data.split('++')[2]
                print("useragent_platform: %s" % useragent_platform)
                useragent = data.split('++')[3]
                print("useragent: %s" % useragent)
                message = data.split('++')[4]
                print("message: %s" % message)
                host = data.split('++')[5]
                print("host: %s" % host)
                url = data.split('++')[6]
                print("url: %s" % url)
                method = data.split('++')[7]
                print("method: %s" % method)
                payload = data.split('++')[8]
                #print("payload: %s" % payload)
    
                print('------------------------------------------------')
                print(">> Trying to ban attacker ip readed from Redis: %s" % attacker)
                print('------------------------------------------------')
                bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "show"], stdout=subprocess.PIPE, text=True)
                print("PF Banned hosts: \n%s" % bannedhosts.stdout)
                blockattacker = 1
                for bannedhost in bannedhosts.stdout.split('   '):
                    bannedhost = bannedhost.replace("\n", "")
                    if bannedhost == '':
                        continue
                    print("Comparing  Redis:%s --> PF:%s" % (attacker, bannedhost))
                    if attacker == bannedhost:
                        print("Attacker already banned, aborting")
                        blockattacker = 0
                        break
    
                #print("blockattacker var value: %s" % blockattacker)
                if blockattacker == 1:
                    print("-- Banning time for: %s" % attacker)
                    cmd = '/sbin/pfctl -t badguys -T add ' + attacker
                    cmdreturnvalue = subprocess.call(cmd, shell=True)
                    print("cmdreturnvalue: %s" % cmdreturnvalue)
                    if cmdreturnvalue != 0:
                        print("++ ERROR: Cant add attacker ip to PF badguys table")

                    print("-- Killing established connections: %s" % attacker)
                    cmd = '/sbin/pfctl -k ' + attacker
                    cmdreturnvalue = subprocess.call(cmd, shell=True)
                    print("cmdreturnvalue: %s" % cmdreturnvalue)
                    if cmdreturnvalue != 0:
                        print("++ ERROR: Cant kill established connections")

                    print('-- Adding to HA-Proxy badguys blacklist file')
                    HaProxyBlacklist = open(HaProxyBlacklistFile, "a")
                    HaProxyBlacklist.write(attacker+"\n")
                    HaProxyBlacklist.close()

                    print('-- Adding to HA-Proxy ACL via AdminSocket')
                    try:
                        hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
                        canConnectToAdminSocket = True
                    except:
                        print('ERRRO: Cant connect to AdminSocket: %S so its impossible to add attacker IP to blacklist ACL' % HaProxyAdminSocketDir) 
                        canConnectToAdminSocket = False

                    if canConnectToAdminSocket:
                        aclCounter = 0
                        badguysAclFound = 0
                        for acl in hap.show_acl():
                            if acl[0] == '#':
                                continue
                            #print("ACL: %s" % acl)
                            #print("Content: %s" % hap.show_acl(aclCounter))
                            #print("Comparing ACL: %s -> RegExp: %s" % (acl,BadguysAclPattern))
                            if re.match(BadguysAclPattern, acl):
                                print("> MATCH - BadGuys Acl: %s" % aclCounter)
                                badguysAclFound = 1
                                break
                            aclCounter = aclCounter + 1

                        if badguysAclFound == 1:
                            print('Current Content: %s' % hap.show_acl(aclCounter))
                            hap.add_acl(aclCounter, attacker)
                            print('Post ADD Content: %s' % hap.show_acl(aclCounter))
                        else:
                            print('>> ERROR: Cant locate ACL so its impossible to add attacker IP to blacklist')

                        msg = "-- Banning time for: " + attacker
                        data = {"chat_id":userid, "text":msg}
                        try:
                            r = requests.post(telegramurl,json=data)
                        except:
                            print("++ Error sending telegram message")
                            continue
    
                        print("-- Sending telegram alert")
                        msg = 'Date: ' + str(timestamp) + '\nAttacker: ' + str(attacker) + '\nUserAgentPlatform: ' + str(useragent_platform) + '\nUserAgent: ' + str(useragent) + '\nHost: ' + str(host) + '\nUrl: ' + str(url) + '\nAlert: ' + str(message) + '\nPayload: ' + str(payload)
                        data = {"chat_id":userid, "text":msg}
                        try:
                            r = requests.post(telegramurl,json=data)
                        except:
                            print("++ Error sending telegram message")
                            continue
            print('------------------------------------------------')
 
    print('')
    print('------------------------------------------------')
    print('>> Clearing old PF ips')
    print('------------------------------------------------')
    bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "show"], stdout=subprocess.PIPE, text=True)
    for bannedhost in bannedhosts.stdout.split('   '):
        bannedhost = bannedhost.replace("\n", "")
        if bannedhost == '':
            continue
        redis_found = False
        for attacker in redisconnection.scan_iter():
            if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
                continue
            else:
                print("Comparing  PF:%s --> Redis:%s" % (bannedhost, attacker))
                if bannedhost == attacker:
                    redis_found = True
                    break
        if redis_found == False:
            print('-- Unbaning: %s' % bannedhost)
            cmd = '/sbin/pfctl -t badguys -T delete ' + bannedhost
            cmdreturnvalue = subprocess.call(cmd, shell=True)
            print("cmdreturnvalue: %s" % cmdreturnvalue)
            if cmdreturnvalue != 0:
                print("++ ERROR: Cant unban attacker ip from PF badguys table")
        else:
            print('Attacker already present in Redis, preserving banning: %s' % bannedhost)
        print('------------------------------------------------')

    print('')
    print('------------------------------------------------')
    print('>> Clearing old blacklisted HA-Proxy file')
    print('------------------------------------------------')
    if os.path.exists(HaProxyBlacklistFile):
        bannedhosts = []
        HaProxyBlacklist = open(HaProxyBlacklistFile, 'r')
        for bannedhost in HaProxyBlacklist.readlines():
            bannedhost = bannedhost.replace("\n", "")
            #print(bannedhost)
            redis_found = False
            for attacker in redisconnection.scan_iter():
                if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
                    continue
                else:
                    print("Comparing  HA:%s --> Redis:%s" % (bannedhost, attacker))
                    if bannedhost == attacker:
                        redis_found = True
                        break
            if redis_found == False:
                print('-- Unblacklisting: %s' % bannedhost)
            else:
                print('Attacker already present in Redis, preserving blacklisting: %s' % bannedhost)
                bannedhosts.append(bannedhost)
            print('------------------------------------------------')
            print('bannedhosts: %s' % bannedhosts)
        with open(HaProxyBlacklistFile, 'w') as HaProxyBlacklist:
            for bannedhost in bannedhosts:
                HaProxyBlacklist.write(bannedhost+'\n')

    print('')
    print('------------------------------------------------')
    print('>> Clearing old blacklisted HA-Proxy ACL')
    print('------------------------------------------------')
    try:
        hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
        canConnectToAdminSocket = True
    except:
        print('ERRRO: Cant connect to AdminSocket: %s so its impossible to delete attacker IP from blacklist ACL' % HaProxyAdminSocketDir)
        canConnectToAdminSocket = False

    if canConnectToAdminSocket:
        aclCounter = 0
        badguysAclFound = 0
        for acl in hap.show_acl():
            if acl[0] == '#':
                continue
            if re.match(BadguysAclPattern, acl):
                print("> MATCH - BadGuys Acl: %s" % aclCounter)
                badguysAclFound = 1
                break
            aclCounter = aclCounter + 1
        if badguysAclFound == 1:
            print('Current Content: %s' % hap.show_acl(aclCounter))
            for aclAttackerRaw in hap.show_acl(aclCounter):
                aclAttacker = aclAttackerRaw.split(' ')[1]
                redis_found = False
                for attacker in redisconnection.scan_iter():
                    if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
                        continue
                    else:
                        print("Comparing  ACL:%s --> Redis:%s" % (aclAttacker, attacker))
                        if aclAttacker == attacker:
                            redis_found = True
                            break
                if redis_found == False:
                    print('-- Unblacklisting: %s' % aclAttacker)
                    hap.del_acl(aclCounter, aclAttackerRaw)
                    print('Post DEL Content: %s' % hap.show_acl(aclCounter))
                else:
                    print('Attacker already present in Redis, preserving blacklisting: %s' % aclAttacker)
                print('------------------------------------------------')
        else:
            print('>> ERROR: Cant locate ACL so its impossible to del attacker IP from blacklist')

    time.sleep(5)

We give it execution permissions:

chmod 700 /root/.scripts/modsecurityNotifier.py

The service management RC script would be as follows:

vi /usr/local/etc/rc.d/modsecurityNotifier

#!/bin/sh
#
# PROVIDE: modsecurityNotifier
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name=modsecurityNotifier
rcvar=modsecurityNotifier_enable

command="/root/.scripts/modsecurityNotifier.py"
start_cmd="modsecurityNotifier_start"
stop_cmd="modsecurityNotifier_stop"
status_cmd="modsecurityNotifier_status"
pidfile="/var/run/${name}.pid"

modsecurityNotifier_start(){
    echo "Starting service: ${name}."
    /usr/sbin/daemon -c -f -p ${pidfile} ${command}
}

modsecurityNotifier_stop(){
    if [ -f ${pidfile} ]; then
        echo "Stopping service: ${name}"
        kill -s INT $(cat ${pidfile})
        sleep 3
    else
        echo "It appears ${name} is not running."
    fi
}

modsecurityNotifier_status(){
    if [ -f ${pidfile} ]; then
        echo "${name} running with PID: $(cat ${pidfile})"
    else
        echo "It appears ${name} is not running."
    fi
}

load_rc_config $name
run_rc_command "$1"

We assign permissions:

chmod 555 /usr/local/etc/rc.d/modsecurityNotifier
chown root:wheel /usr/local/etc/rc.d/modsecurityNotifier

We enable and start the service:

sysrc modsecurityNotifier_enable=yes
service modsecurityNotifier start


Script modsecurityPF.py

We will only use the following script if we have a distributed infrastructure. We will run it on the rest of the servers that do not depend on Ha-Proxy, such as web servers, databases, etc.

vi /root/.scripts/modsecurityPF.py
#!/usr/local/bin/python

import redis
import subprocess
import time
import os

apikey = "XXXXXXXXXX"
telegramurl = "https://api.telegram.org/bot{}/sendMessage".format(apikey)
userid = "XXXXXXXXXX"

try:
    redisconnection = redis.Redis(host="192.168.69.2", port=6379, db=0, password='XXXXXXXXXX', charset="utf-8", decode_responses=True)
    redisconnection.ping()
except:
    print('++ ERROR: Cant connect to redis server')
    msg = 'ERROR ' + os.uname().nodename + '(modsecurityPF.py): Cant connect to redis server'
    data = {"chat_id":userid, "text":msg}
    try:
        r = requests.post(telegramurl,json=data)
    except:
        print("++ Error sending telegram message")
    quit()

while(True):
    print('')
    print('------------------------------------------------')
    print('>> Checking REDIS content')
    print('------------------------------------------------')
    for attacker in redisconnection.scan_iter():
        #print("-- Analyzing key: %s" % attacker)
        if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
            continue
        else:
            rediscounter = redisconnection.get(attacker)
            print("Attacker: %s Counter: %s" % (attacker, rediscounter))
            if int(rediscounter) >= 5 and redisconnection.get('data' + attacker):
                data = redisconnection.get('data' + attacker)
                #print("Data: %s" % data)
                timestamp = data.split('++')[0]
                print("timestamp: %s" % timestamp)
                attacker = data.split('++')[1]
                print("attacker: %s" % attacker)
                useragent = data.split('++')[2]
                print("useragent: %s" % useragent)
                message = data.split('++')[3]
                print("message: %s" % message)
                host = data.split('++')[4]
                print("host: %s" % host)
                url = data.split('++')[5]
                print("url: %s" % url)
                method = data.split('++')[6]
                print("method: %s" % method)
                payload = data.split('++')[7]
                #print("payload: %s" % payload)
    
                print('------------------------------------------------')
                print(">> Trying to ban attacker ip readed from Redis: %s" % attacker)
                print('------------------------------------------------')
                bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "show"], stdout=subprocess.PIPE, text=True)
                print("PF Banned hosts: \n%s" % bannedhosts.stdout)
                blockattacker = 1
                for bannedhost in bannedhosts.stdout.split('   '):
                    bannedhost = bannedhost.replace("\n", "")
                    if bannedhost == '':
                        continue
                    print("Comparing  Redis:%s --> PF:%s" % (attacker, bannedhost))
                    if attacker == bannedhost:
                        print("Attacker already banned, aborting")
                        blockattacker = 0
                        break
    
                #print("blockattacker var value: %s" % blockattacker)
                if blockattacker == 1:
                    print("-- Banning time for: %s" % attacker)
                    cmd = '/sbin/pfctl -t badguys -T add ' + attacker
                    cmdreturnvalue = subprocess.call(cmd, shell=True)
                    print("cmdreturnvalue: %s" % cmdreturnvalue)
                    if cmdreturnvalue != 0:
                        print("++ ERROR: Cant add attacker ip to PF badguys table")

                    print("-- Killing established connections: %s" % attacker)
                    cmd = '/sbin/pfctl -k ' + attacker
                    cmdreturnvalue = subprocess.call(cmd, shell=True)
                    print("cmdreturnvalue: %s" % cmdreturnvalue)
                    if cmdreturnvalue != 0:
                        print("++ ERROR: Cant kill established connections")
            print('------------------------------------------------')
 
    print('')
    print('------------------------------------------------')
    print('>> Clearing old PF ips')
    print('------------------------------------------------')
    bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "show"], stdout=subprocess.PIPE, text=True)
    for bannedhost in bannedhosts.stdout.split('   '):
        bannedhost = bannedhost.replace("\n", "")
        if bannedhost == '':
            continue
        redis_found = False
        for attacker in redisconnection.scan_iter():
            if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
                continue
            else:
                print("Comparing  PF:%s --> Redis:%s" % (bannedhost, attacker))
                if bannedhost == attacker:
                    redis_found = True
                    break
        if redis_found == False:
            print('-- Unbaning: %s' % bannedhost)
            cmd = '/sbin/pfctl -t badguys -T delete ' + bannedhost
            cmdreturnvalue = subprocess.call(cmd, shell=True)
            print("cmdreturnvalue: %s" % cmdreturnvalue)
            if cmdreturnvalue != 0:
                print("++ ERROR: Cant unban attacker ip from PF badguys table")
        else:
            print('Attacker already present in Redis, preserving banning: %s' % bannedhost)
        print('------------------------------------------------')

    time.sleep(5)

We give it execution permissions:

chmod 700 /root/.scripts/modsecurityPF.py

The service management RC script would be as follows:

vi /usr/local/etc/rc.d/modsecurityPF

#!/bin/sh
#
# PROVIDE: modsecurityPF
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name=modsecurityPF
rcvar=modsecurityPF_enable

command="/root/.scripts/modsecurityPF.py"
start_cmd="modsecurityPF_start"
stop_cmd="modsecurityPF_stop"
status_cmd="modsecurityPF_status"
pidfile="/var/run/${name}.pid"

modsecurityPF_start(){
    echo "Starting service: ${name}."
    /usr/sbin/daemon -c -f -p ${pidfile} ${command}
}

modsecurityPF_stop(){
    if [ -f ${pidfile} ]; then
        echo "Stopping service: ${name}"
        kill -s INT $(cat ${pidfile})
        sleep 3
    else
        echo "It appears ${name} is not running."
    fi
}

modsecurityPF_status(){
    if [ -f ${pidfile} ]; then
        echo "${name} running with PID: $(cat ${pidfile})"
    else
        echo "It appears ${name} is not running."
    fi
}

load_rc_config $name
run_rc_command "$1"

We assign permissions:

chmod 555 /usr/local/etc/rc.d/modsecurityPF
chown root:wheel /usr/local/etc/rc.d/modsecurityPF

We enable and start the service:

sysrc modsecurityPF_enable=yes
service modsecurityPF start


PF filtering rules

To block traffic from PF, we will use a table called badguys. All traffic coming from these IPs will be blocked, except for port 80/443 if it is Ha-Proxy. In the rest of the servers, it would be exactly the same but removing the lines:

# Continue attending badguys in HA-Proxy, we have a surprise for them:
pass in quick proto tcp from <badguys> to 192.168.69.19 port 80
pass in quick proto tcp from <badguys> to 192.168.69.19 port 443
vi /etc/pf.conf
ext_if = "nfe0"

set block-policy return
scrub in on $ext_if all fragment reassemble
set skip on lo

table <badguys> persist
table <jails> persist

nat on $ext_if from <jails> to any -> ($ext_if:0)
rdr-anchor "rdr/*"

antispoof for $ext_if inet

block log all
pass out quick

pass in proto tcp to 192.168.69.2 port 7777

# SMTP -> HellStorm
pass in proto tcp to 192.168.69.17 port 25

# HTTP/HTTPS -> Atlas
pass in proto tcp to 192.168.69.19 port 80
pass in proto tcp to 192.168.69.19 port 443

# Continue attending badguys in HA-Proxy, we have a surprise for them:
pass in quick proto tcp from <badguys> to 192.168.69.19 port 80
pass in quick proto tcp from <badguys> to 192.168.69.19 port 443

# Xbox -> Paradox
pass in proto tcp from 192.168.69.196 to 192.168.69.18 port 80
# TARS -> Paradox
pass in proto tcp from 192.168.69.198 to 192.168.69.18 port 80
# Garrus -> Paradox, testing purpose
pass in proto tcp from 192.168.69.4 to 192.168.69.18 port 80

pass in proto tcp to any port 22

# Block all traffic from badguys, except 80,443 that was allowed previously:
block in from <badguys>

Ha-Proxy Configuration

As we have already mentioned, the idea is to keep the HA-Proxy ACLs synchronized in running-config using the AdminSocket and a list of IPs in a file in the file system. This way, if the Ha-Proxy is restarted, everything will continue to work, as it will read the file that also has the list of IPs.

The Ha-Proxy configuration would be as follows:

vi /usr/local/etc/haproxy.conf

global
    daemon
    maxconn 5000
    log 192.168.69.19:514 local1
    user nobody
    group nobody
    stats socket /var/run/haproxy.sock user nobody group nobody mode 660 level admin

    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    
defaults
    timeout connect 10s
    timeout client 30s
    timeout server 30s
    mode http
    # ENABLE LOGGING:
    # TCP connections
    #log global
    # Backend connections
    #option httplog

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 5s
    stats auth kr0m:8XEn3EL1aYIeKI

frontend HTTP
    bind :80
    # Allow LetsEncrypt certificate renew and redirect to HTTPS all other requests:
    acl letsencrypt path_beg /.well-known/acme-challenge/
    http-request redirect scheme https unless letsencrypt
    use_backend letsencrypt-backend if letsencrypt

frontend HTTP-SSL
    bind :443 ssl crt /usr/local/etc/haproxy_certs.pem
    http-request add-header X-Forwarded-Proto https

    acl mail hdr_dom(host) mail.alfaexploit.com
    acl prometheus hdr_dom(host) prometheus.alfaexploit.com
    acl grafana hdr_dom(host) grafana.alfaexploit.com
    acl gdrive hdr_dom(host) gdrive.alfaexploit.com
    acl www_alfaexploit hdr_dom(host) www.alfaexploit.com
    acl badguys_alfaexploit hdr_dom(host) badguys.alfaexploit.com
    acl alfaexploit hdr_dom(host) alfaexploit.com

    http-request deny deny_status 400 if !mail !prometheus !grafana !gdrive !www_alfaexploit !badguys_alfaexploit !alfaexploit

    # Show banned list to anyone who wants to view it
    use_backend badguys if badguys_alfaexploit

    # Route Badguys traffic:
    acl badguy_ips src -f /usr/local/etc/bagguys.list
    use_backend badguys if badguy_ips

    # Order matters because acl alfaexploit -> alfaexploit.com is the most generic that covers all of them
    use_backend mail if mail
    use_backend prometheus if prometheus
    use_backend prometheus if grafana
    use_backend gdrive if gdrive
    use_backend alfaexploit if www_alfaexploit
    use_backend alfaexploit if alfaexploit

backend letsencrypt-backend
    server letsencrypt 192.168.69.19:88

backend mail
    server HellStorm 192.168.69.17:80 check send-proxy-v2

backend prometheus
    server RECLog 192.168.69.21:80 check send-proxy-v2

backend gdrive
    server Paradox 192.168.69.18:8080 check send-proxy-v2

backend alfaexploit
    server MetaCortex 192.168.69.20:80 check send-proxy-v2

backend badguys
    http-request set-path / unless { path_beg /images }
    server Atlas 192.168.69.19:8888 check send-proxy-v2

Attackers Dashboard

On the server where we will have the attackers dashboard, we will install Nginx and PHP:

pkg install nginx php82 php82-pecl-redis

We enable and start the PHP service:

sysrc php_fpm_enable=yes
service php-fpm start

We include our configuration from the global Nginx file:

vi /usr/local/etc/nginx/nginx.conf

http {
    include       badguys.conf;
vi /usr/local/etc/nginx/badguys.conf
server {
    listen 192.168.69.19:8888 proxy_protocol;
    server_name badguys.alfaexploit.com;

    set_real_ip_from 192.168.69.19;
    real_ip_header proxy_protocol;

    root   /usr/local/www/nginx;
    index index.php;

    location ~ \.php$ {
      try_files $uri =404;
      include fastcgi_params;
      fastcgi_index index.php;
      fastcgi_split_path_info ^(.+\.php)(.*)$;
      fastcgi_keep_conn on;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_pass 127.0.0.1:9000;
    }
}

We enable and start the Nginx service:

sysrc nginx_enable=yes
service nginx start

We create the directory to store the logos of the operating systems:

mkdir /usr/local/www/nginx/images

We download the logos of the operating systems:

cd /usr/local/www/nginx/images
wget https://alfaexploit.com/files/libmodSecurityFreeBSD/images.tar.gz
tar xvzf images.tar.gz<>

The dashboard code would be as follows:

vi /usr/local/www/nginx/index.php

<?php
$os_array = [
    'windows nt 10'                              =>  'WindowsNT',
    'windows nt 6.3'                             =>  'WindowsNT',
    'windows nt 6.2'                             =>  'WindowsNT',
    'windows nt 6.1|windows nt 7.0'              =>  'WindowsNT',
    'windows nt 6.0'                             =>  'WindowsNT',
    'windows nt 5.2'                             =>  'WindowsNT',
    'windows nt 5.1'                             =>  'WindowsNT',
    'windows xp'                                 =>  'WindowsXP',
    'windows nt 5.0|windows nt5.1|windows 2000'  =>  'WindowsNT',
    'windows me'                                 =>  'WindowsME',
    'windows nt 4.0|winnt4.0'                    =>  'WindowsNT',
    'windows ce'                                 =>  'WindowsCE',
    'windows 98|win98'                           =>  'Windows98',
    'windows 95|win95'                           =>  'Windows95',
    'win16'                                      =>  'Windows16',
    'mac os x 10.1[^0-9]'                        =>  'MacOS',
    'macintosh|mac os x'                         =>  'MacOS',
    'mac_powerpc'                                =>  'MacOS',
    'ubuntu'                                     =>  'Linux',
    'iphone'                                     =>  'iPhone',
    'ipod'                                       =>  'iPod',
    'ipad'                                       =>  'iPad',
    'android'                                    =>  'Android',
    'blackberry'                                 =>  'BlackBerry',
    'webos'                                      =>  'WebOS',
    'linux'                                      =>  'Linux',

    '(media center pc).([0-9]{1,2}\.[0-9]{1,2})'=>'WindowsMediaCenter',
    '(win)([0-9]{1,2}\.[0-9x]{1,2})'=>'Windows',
    '(win)([0-9]{2})'=>'Windows',
    '(windows)([0-9x]{2})'=>'Windows',

    'Win 9x 4.90'=>'WindowsME',
    '(windows)([0-9]{1,2}\.[0-9]{1,2})'=>'Windows',
    'win32'=>'Windows',
    '(java)([0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2})'=>'Java',
    '(Solaris)([0-9]{1,2}\.[0-9x]{1,2}){0,1}'=>'Solaris',
    'dos x86'=>'DOS',
    'Mac OS X'=>'MacOS',
    'Mac_PowerPC'=>'MacOS',
    '(mac|Macintosh)'=>'MacOS',
    '(sunos)([0-9]{1,2}\.[0-9]{1,2}){0,1}'=>'SunOS',
    '(beos)([0-9]{1,2}\.[0-9]{1,2}){0,1}'=>'BeOS',
    '(risc os)([0-9]{1,2}\.[0-9]{1,2})'=>'RISCOS',
    'unix'=>'Unix',
    'os/2'=>'OS2',
    'freebsd'=>'FreeBSD',
    'openbsd'=>'OpenBSD',
    'netbsd'=>'NetBSD',
    'irix'=>'IRIX',
    'plan9'=>'Plan9',
    'aix'=>'AIX',
    'GNU Hurd'=>'GNUHurd',
    '(fedora)'=>'Linux',
    '(kubuntu)'=>'Linux',
    '(ubuntu)'=>'Linux',
    '(debian)'=>'Linux',
    '(CentOS)'=>'Linux',
    '(Mandriva).([0-9]{1,3}(\.[0-9]{1,3})?(\.[0-9]{1,3})?)'=>'Linux',
    '(SUSE).([0-9]{1,3}(\.[0-9]{1,3})?(\.[0-9]{1,3})?)'=>'Linux',
    '(Dropline)'=>'Linux',
    '(ASPLinux)'=>'Linux',
    '(Red Hat)'=>'Linux',
    '(linux)'=>'Linux',
    '(amigaos)([0-9]{1,2}\.[0-9]{1,2})'=>'AmigaOS',
    'amiga-aweb'=>'AmigaOS',
    'amiga'=>'AmigaOS',
    'AvantGo'=>'PalmOS',
    '[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}'=>'Linux',
    '(webtv)/([0-9]{1,2}\.[0-9]{1,2})'=>'WebTV',
    'Dreamcast'=>'DreamcastOS',
    'GetRight'=>'Windows',
    'go!zilla'=>'Windows',
    'gozilla'=>'Windows',
    'gulliver'=>'Windows',
    'ia archiver'=>'Windows',
    'NetPositive'=>'Windows',
    'mass downloader'=>'Windows',
    'microsoft'=>'Windows',
    'offline explorer'=>'Windows',
    'teleport'=>'Windows',
    'web downloader'=>'Windows',
    'webcapture'=>'Windows',
    'webcollage'=>'Windows',
    'webcopier'=>'Windows',
    'webstripper'=>'Windows',
    'webzip'=>'Windows',
    'wget'=>'Wget',
    'Java'=>'Java',
    'flashget'=>'FlashGet',

    'MS FrontPage'=>'Windows',
    '(msproxy)/([0-9]{1,2}.[0-9]{1,2})'=>'Windows',
    '(msie)([0-9]{1,2}.[0-9]{1,2})'=>'Windows',
    'libwww-perl'=>'Perl',
    'UP.Browser'=>'WindowsCE',
    'NetAnts'=>'Windows',
];

function user_agent_os($user_agent, $os_array) {
    foreach ($os_array as $regex => $value) {
        if (preg_match('{\b('.$regex.')\b}i', $user_agent)) {
            return $value;
            #$key = array_rand($os_array);
            #$value = $os_array[$key];
            #return $value;
        }
    }
    return 'Unknown';
}

function secs_to_str ($duration) {
    $periods = array(
        'day' => 86400,
        'hour' => 3600,
        'minute' => 60,
        'second' => 1
    );
 
    $parts = array();
 
    foreach ($periods as $name => $dur) {
        $div = floor($duration / $dur);
 
        if ($div == 0)
            continue;
        else
            if ($div == 1)
                $parts[] = $div . " " . $name;
            else
                $parts[] = $div . " " . $name . "s";
        $duration %= $dur;
    }
 
    $last = array_pop($parts);
 
    if (empty($parts))
        return $last;
    else
        return join(', ', $parts) . " and " . $last;
}

if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip_address = $_SERVER['HTTP_CLIENT_IP'];
}
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
else {
    $ip_address = $_SERVER['REMOTE_ADDR'];
}

$redis = new Redis();
$redis->connect('192.168.69.2', 6379);
$redis->auth('XXXXXXXX');

$allKeys = $redis->keys('*');

echo "<!DOCTYPE html>";
echo "<html>";
echo "<head>";

echo "<style>";
echo ".attacker_data {";
echo "position: relative;";
echo "top: -6px;";
echo "padding-left: 8px;";
echo "}";

echo "table, td, th {";
echo "border: 1px solid black;";
echo "margin-bottom: 15px;";
echo "}";

echo "table {";
echo "  border-collapse: collapse;";
echo "  width: 100%;";
echo "}";

echo "td {";
echo "  text-align: center;";
echo "}";
echo "</style>";

echo "</head>";
echo "<body>";

echo "<table>";
echo "<tr>";
echo "<td>Date</td>";
echo "<td>IP</td>";
echo "<td>I'm watching you</td>";
echo "<td>Attack</td>";
echo "<td>Unban remaining time</td>";
echo "</tr>";
foreach ($allKeys as $key) {
    $attacker_data = array();
    if (str_starts_with($key, 'data')) {
        $counter_key = substr($key, 4);
        $counter = $redis->get($counter_key);
        if ($counter >= 5){
            $ttl = $redis->ttl($key);
            $ttl_human = secs_to_str($ttl);
            $raw_data = $redis->get($key);
            $data = explode("++", $raw_data);
            $i = 0;
            # We are iterating Redis data: 0-> Date, 1 -> attacker_ip, 2 -> useragent_platform, 3 -> useragent, 4 -> message
            $get_os_from_useragent = False;
            foreach ($data as $field) {
                $field = trim($field);
                if ($i == 0) {
                    $attacker_data[] = $field;
                } elseif ($i == 1) {
                    $attacker_ip = $field;

                    ## We have limits requesting /ip-api.com url so we try to cache it
                    if ($redis->exists('geoipdata'.$attacker_ip)) {
                        $geoipdata = ($redis->get('geoipdata'.$attacker_ip));
                        $attacker_data[] = $geoipdata;
                    } else {
                        $ips = array();
                        $ips[] = $field;
                        $endpoint = 'http://ip-api.com/batch';
                        $options = [
                            'http' => [
                                    'method' => 'POST',
                                    'user_agent' => 'AlfaExploit/1.0',
                                    'header' => 'Content-Type: application/json',
                                    'content' => json_encode($ips)
                            ]
                        ];
                        
                        if (($response = @file_get_contents($endpoint, false, stream_context_create($options))) === false) {
                            $attacker_data[] = 'UNKNOWN';
                            # Cache data: 3600s -> 1h
                            $redis->set('geoipdata'.$attacker_ip, 'UNKNOWN');
                            $redis->expire('geoipdata'.$attacker_ip, 3600);
                        } else {
                            $array = json_decode($response, true);
                            $status = $array[0]['status'];
                            if ($status == 'fail' ){
                               $attacker_data[] = $array[0]['message'];
                               # Cache data: 3600s -> 1h
                               $redis->set('geoipdata'.$attacker_ip, $array[0]['message']);
                               $redis->expire('geoipdata'.$attacker_ip, 3600);
                            } else {
                               $country = $array[0]['country'];
                               $regionName = $array[0]['regionName'];
                               $lat = $array[0]['lat'];
                               $lon = $array[0]['lon'];
                               $isp = $array[0]['isp'];
                               $attacker_data[] = $country.'/'.$regionName.'('.$lat.' '.$lon.')'. $isp;
                               # Cache data: 3600s -> 1h
                               $redis->set('geoipdata'.$attacker_ip, $country.'/'.$regionName.'('.$lat.' '.$lon.')'. $isp);
                               $redis->expire('geoipdata'.$attacker_ip, 3600);
                            }
                        }
                   }
                } elseif ($i == 2) {
                    # useragent_platform is NULL, continue, we will try to get it from user-agent
                    if ($field == 'NULL') {
                        $get_os_from_useragent = True;
                    } else {
                        $attacker_data[] = str_replace('"', '', $field);;
                    }
                } elseif ($i == 3 && $get_os_from_useragent) {
                    # Try to get OS from user-agent
                    $attacker_data[] = user_agent_os($field, $os_array);
                } elseif ($i == 4) {
                    $attacker_data[] = $field;
                }
                $i = $i + 1;
            }

            # Print table data
            echo "<tr>";
            # DATE:
            echo "<td>$attacker_data[0]</td>";
            # IP:
            if ($ip_address == $attacker_ip) {
                echo "<td><span style=\"color:#E20D41;\">$attacker_ip</span></td>";
            } else {
                echo "<td>$attacker_ip</td>";
            }
            # ATTACKER DATA
            echo "<td><img src=/images/$attacker_data[2].png><span class='attacker_data'>$attacker_data[1]</span></td>";
            echo "<td>$attacker_data[3]</td>";
            echo "<td>$ttl_human</td>";
            echo "</tr>";
        }
    }
}
echo "</table>";
echo '<a href="https://t.me/AlfaExploia">Telegram channel</a> Email: echo \'a3IwbSAoYXQpIGFsZmFleHBsb2l0IChkb3QpIGNvbQo=\' | base64 -d';
echo '<form action="https://alfaexploit.com">';
echo '<button type="submit">AlfaExploit</button>';
echo '</form>';
echo "</body>";
echo "</html>";

Useful Scripts

This script will allow us to quickly see the banned attackers. This script is designed to be executed on the physical server or if it is a distributed infrastructure in the Ha-Proxy. If we want to run it on the rest of the servers, we just have to delete the lines:

print('')
print('----------------')
print('>> HA-Proxy FILE')
print('----------------')
if os.path.exists(HaProxyBlacklistFile):
    HaProxyBlacklist = open(HaProxyBlacklistFile, 'r')
    for bannedhost in HaProxyBlacklist.readlines():
        bannedhost = bannedhost.replace("\n", "")
        print(bannedhost)

print('')
print('----------------')
print('>> HA-Proxy ACL')
print('---------------')
try:
    hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
    canConnectToAdminSocket = True
except:
    print('ERRRO: Cant connect to AdminSocket: %s so its impossible to delete attacker IP from blacklist ACL' % HaProxyAdminSocketDir)
    canConnectToAdminSocket = False

if canConnectToAdminSocket:
    aclCounter = 0
    for acl in hap.show_acl():
        if acl[0] == '#':
            continue
        if re.match(BadguysAclPattern, acl):
            badguysAclFound = 1
            break
        aclCounter = aclCounter + 1
    if badguysAclFound == 1:
        for aclAttackerRaw in hap.show_acl(aclCounter):
            aclAttacker = aclAttackerRaw.split(' ')[1]
            print(aclAttacker)
vi /root/.scripts/showBadGuys.py
#!/usr/local/bin/python

import redis
import os
import subprocess
import re
import time
import datetime
from haproxyadmin import haproxy

HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/bagguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = ".*\(/usr/local/etc/bagguys\.list\) pattern loaded from file '/usr/local/etc/bagguys\.list'.*"

try:
    redisconnection = redis.Redis(host="192.168.69.2", port=6379, db=0, password='XXXXXXXXXXXXXXX', charset="utf-8", decode_responses=True)
    redisconnection.ping()
except:
    print('++ ERROR: Cant connect to redis server')
    quit()

print('')
print('---------------')
print('>> REDIS')
print('---------------')
for attacker in redisconnection.scan_iter():
    if attacker[0:4] == 'data' or attacker[0:9] == 'geoipdata':
        continue
    else:
        rediscounter = redisconnection.get(attacker)
        attackerTTL = redisconnection.ttl(attacker)
        remainingTime = str(datetime.timedelta(seconds=attackerTTL))
        print("> Attacker: %s Counter: %s" % (attacker, rediscounter))
        print("TTL: %s RemainingTime: %s" % (attackerTTL, remainingTime))
        if int(rediscounter) >= 5:
            print('Banned: YES')
        else:
            print('Banned: NO')
        if redisconnection.get('data' + attacker):
            data = redisconnection.get('data' + attacker)
            #print("Data: %s" % data)
            timestamp = data.split('++')[0]
            print("timestamp: %s" % timestamp)
            attacker = data.split('++')[1]
            #print("attacker: %s" % attacker)
            useragent_platform = data.split('++')[2]
            print("useragent_platform: %s" % useragent_platform)
            useragent = data.split('++')[3]
            print("useragent: %s" % useragent)
            message = data.split('++')[4]
            print("message: %s" % message)
            host = data.split('++')[5]
            #print("host: %s" % host)
            url = data.split('++')[6]
            #print("url: %s" % url)
            method = data.split('++')[7]
            #print("method: %s" % method)
            payload = data.split('++')[8]
            #print("payload: %s" % payload)

print('')
print('---------------')
print('>> PF')
print('---------------')
bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "show"], stdout=subprocess.PIPE, text=True)
print("PF Banned hosts: \n%s" % bannedhosts.stdout)

print('')
print('----------------')
print('>> HA-Proxy FILE')
print('----------------')
if os.path.exists(HaProxyBlacklistFile):
    HaProxyBlacklist = open(HaProxyBlacklistFile, 'r')
    for bannedhost in HaProxyBlacklist.readlines():
        bannedhost = bannedhost.replace("\n", "")
        print(bannedhost)

print('')
print('----------------')
print('>> HA-Proxy ACL')
print('---------------')
try:
    hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
    canConnectToAdminSocket = True
except:
    print('ERRRO: Cant connect to AdminSocket: %s so its impossible to delete attacker IP from blacklist ACL' % HaProxyAdminSocketDir)
    canConnectToAdminSocket = False

if canConnectToAdminSocket:
    aclCounter = 0
    for acl in hap.show_acl():
        if acl[0] == '#':
            continue
        if re.match(BadguysAclPattern, acl):
            badguysAclFound = 1
            break
        aclCounter = aclCounter + 1
    if badguysAclFound == 1:
        for aclAttackerRaw in hap.show_acl(aclCounter):
            aclAttacker = aclAttackerRaw.split(' ')[1]
            print(aclAttacker)

We assign the necessary permissions:

chmod 700 /root/.scripts/showBadGuys.py

When executed, we will see the following output:

---------------
>> REDIS
---------------
> Attacker: 192.168.69.202 Counter: 19
TTL: 3522 RemainingTime: 0:58:42
Banned: YES
timestamp: Sat Feb 25 12:56:26 2023
useragent_platform: "Linux"
useragent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
message: SQL Injection Attack Detected via libinjection

---------------
>> PF
---------------
PF Banned hosts: 
   192.168.69.202


----------------
>> HA-Proxy FILE
----------------
192.168.69.202

----------------
>> HA-Proxy ACL
---------------
192.168.69.202

Another really useful script is to flush all bans. This script is designed to be executed on the physical server or if it is a distributed infrastructure in the Ha-Proxy. If we want to run it on the rest of the servers, we just have to delete the lines:

print('')
print('----------------')
print('>> HA-Proxy FILE')
print('----------------')
if os.path.exists(HaProxyBlacklistFile):
    try:
        open(HaProxyBlacklistFile, 'w').close()
    except IOError:
        print('Failure')

print('')
print('----------------')
print('>> HA-Proxy ACL')
print('---------------')
try:
    hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
    canConnectToAdminSocket = True
except:
    print('ERRRO: Cant connect to AdminSocket: %s so its impossible to delete attacker IP from blacklist ACL' % HaProxyAdminSocketDir)
    canConnectToAdminSocket = False

if canConnectToAdminSocket:
    aclCounter = 0
    for acl in hap.show_acl():
        if acl[0] == '#':
            continue
        if re.match(BadguysAclPattern, acl):
            badguysAclFound = 1
            break
        aclCounter = aclCounter + 1
    if badguysAclFound == 1:
        for aclAttackerRaw in hap.show_acl(aclCounter):
            hap.del_acl(aclCounter, aclAttackerRaw)
vi /root/.scripts/flushBadGuys.py
#!/usr/local/bin/python

import redis
import os
import subprocess
import re
import datetime
from haproxyadmin import haproxy

HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/bagguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = ".*\(/usr/local/etc/bagguys\.list\) pattern loaded from file '/usr/local/etc/bagguys\.list'.*"

try:
    redisconnection = redis.Redis(host="192.168.69.2", port=6379, db=0, password='XXXXXXX', charset="utf-8", decode_responses=True)
    redisconnection.ping()
except:
    print('++ ERROR: Cant connect to redis server')
    quit()

print('')
print('---------------')
print('>> REDIS')
print('---------------')
try:
    redisconnection.flushdb()
except:
    print('Failure')

print('')
print('---------------')
print('>> PF')
print('---------------')
try:
    bannedhosts = subprocess.run(["pfctl", "-t", "badguys", "-T", "flush"], stdout=subprocess.PIPE, text=True)
except:
    print('Failure')

print('')
print('----------------')
print('>> HA-Proxy FILE')
print('----------------')
if os.path.exists(HaProxyBlacklistFile):
    try:
        open(HaProxyBlacklistFile, 'w').close()
    except IOError:
        print('Failure')

print('')
print('----------------')
print('>> HA-Proxy ACL')
print('---------------')
try:
    hap = haproxy.HAProxy(socket_dir=HaProxyAdminSocketDir)
    canConnectToAdminSocket = True
except:
    print('ERRRO: Cant connect to AdminSocket: %s so its impossible to delete attacker IP from blacklist ACL' % HaProxyAdminSocketDir)
    canConnectToAdminSocket = False

if canConnectToAdminSocket:
    aclCounter = 0
    for acl in hap.show_acl():
        if acl[0] == '#':
            continue
        if re.match(BadguysAclPattern, acl):
            badguysAclFound = 1
            break
        aclCounter = aclCounter + 1
    if badguysAclFound == 1:
        for aclAttackerRaw in hap.show_acl(aclCounter):
            hap.del_acl(aclCounter, aclAttackerRaw)

We assign the necessary permissions:

chmod 700 /root/.scripts/flushBadGuys.py

When executed, we will see the following output:

---------------
>> REDIS
---------------

---------------
>> PF
---------------
1 addresses deleted.

----------------
>> HA-Proxy FILE
----------------

----------------
>> HA-Proxy ACL
---------------

Final Result

The Telegram notifications will look like this:

And the dashboard will look like this:

If you liked the article, you can treat me to a RedBull here