This page looks best with JavaScript enabled

libModsecurity on Apache/Raspbian

 ·  🎃 kr0m

The Raspberrypi platform relies on an ARM microprocessor, which is not bad but imposes some limitations on software. Not all packages are compiled for this architecture and even some software does not work, such as libModSecurity3. It compiles and loads the module, but when Apache is restarted, the process crashes. This does not mean that we cannot use it, but we will have to settle for version 2, which is packaged for Raspbian.

As we explained in a previous article here , there are currently two versions of libmodsecurity, version 2 and 3. The latter is preferable as it is more optimized and we will get better performance, especially if our web server is not Apache. However, in Raspbian we only have the package for version 2 and the compiled version 3 crashes Apache, displaying the following error when restarting it:

apachectl[4941]: Segmentation fault

We proceed with the installation of version 2:

apt install libapache2-mod-security2
cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf

We configure modsecurity by modifying the following parameters:

vi /etc/modsecurity/modsecurity.conf

SecRuleEngine On
SecAuditLogFormat json
SecAuditEngine On
SecAuditLogType Concurrent
SecAuditLogStorageDir /opt/modsecurity/var/audit/
mkdir -p /opt/modsecurity/var/audit/
chown -R root:www-data /opt/modsecurity/var/audit/
chmod 775 /opt/modsecurity/var/audit/

We download the OWASP rules:

mv /usr/share/modsecurity-crs /usr/share/modsecurity-crs_ori
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git /usr/share/modsecurity-crs
cp /usr/share/modsecurity-crs/crs-setup.conf.example /usr/share/modsecurity-crs/crs-setup.conf

We configure Apache to load the rules:

vi /etc/apache2/mods-enabled/security2.conf

<IfModule security2_module>
        # Default Debian dir for modsecurity's persistent data
        SecDataDir /var/cache/modsecurity

        # Include all the *.conf files in /etc/modsecurity.
        # Keeping your local configuration in that directory
        # will allow for an easy upgrade of THIS file and
        # make your life easier
        IncludeOptional /etc/modsecurity/*.conf

        # Include OWASP ModSecurity CRS rules if installed
        #IncludeOptional /usr/share/modsecurity-crs/owasp-crs.load
        IncludeOptional /usr/share/modsecurity-crs/*.conf
        IncludeOptional /usr/share/modsecurity-crs/rules/*.conf
</IfModule>

We restart Apache:

/etc/init.d/apache2 restart

We change the operation mode from Anomaly Scoring mode to Self-contained mode since it is lighter and more convenient for a Raspberry Pi:

vi /usr/share/modsecurity-crs/crs-setup.conf

#SecDefaultAction "phase:1,log,auditlog,pass"
#SecDefaultAction "phase:2,log,auditlog,pass"
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
SecDefaultAction "phase:2,log,auditlog,deny,status:403"

We restart Apache:

/etc/init.d/apache2 restart

For the parsing script to work, a Redis server is required:

apt install redis-server

We configure the Redis access password:

vi /etc/redis/redis.conf

requirepass XXXXXXXXXXXXXXXXX

We restart the service:

/etc/init.d/redis-server restart

We install the necessary libraries for the parsing script to work:

pip install redis

The parser differs from the previous article’s as it has to process logs from version 2:

vi /usr/local/modsecurityNotifier.py

#!/usr/bin/python

import pyinotify
import os
import json
import requests
import redis
import iptc

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

try:
        redisconnection = redis.Redis(host="127.0.0.1", port=6379, db=0, password='XXXXXXXXXXXXXXXXXXX')
    redisconnection.ping()
except:
        print '++ ERROR: Cant connect to redis server'
    quit()

class CommitFunction(pyinotify.ProcessEvent):
    def process_default(self, event):
        fullpath = event.path + '/' + event.name
        if os.path.isfile(fullpath):
            print '------------------------------------------------'
            print "Processing: " + str(fullpath)
            with open(fullpath) as fp:
                for line in fp:
                    try:
                        rawdata = json.loads(line)
                    except:
                        continue
    
                    if rawdata != "":
                        try:
                            timestamp = rawdata['transaction']['time']
                        except:
                            timestamp = 'NULL'
                        try:
                            attacker = rawdata['transaction']['remote_address']
                        except:
                            attacker = 'NULL'
                        try:
                            useragent = rawdata['request']['headers']['User-Agent']
                        except:
                            useragent = 'NULL'
                        try:
                            host = rawdata['request']['headers']['Host']
                        except:
                            host = 'NULL'
                        try:
                            url = rawdata['request']['request_line']
                        except:
                            url = 'NULL'
                        try:
                            message = rawdata['audit_data']['messages'][0].split("[msg \"")[1].split("\"]")[0]
                        except:
                            #message = 'NULL'
                            continue
                        try:
                            payload = rawdata['audit_data']['messages'][0].split("[data \"")[1].split("\"]")[0]
                        except:
                            #payload = 'NULL'
                            continue

                        print '>> Timestamp: ' + str(timestamp)
                        print 'Attacker: ' + str(attacker)
                        print 'UserAgent: ' + str(useragent)
                        print 'Host: ' + str(host)
                        print 'URL: ' + str(url)
                        print 'Message: ' + str(message)
                        print 'Payload: ' + str(payload)

                        print 'Checking redis IP: ' + str(attacker)
                        if redisconnection.get(attacker):
                            rediscounter = redisconnection.get(attacker)
                            #print 'rediscounter: ' + str(rediscounter)
                            if int(rediscounter) >= 5:
                                blacklistpath = '/etc/.blacklists/modsecurity.list'
                                if os.path.isfile(blacklistpath):
                                    # Check if attacker is in blacklist
                                    attackerfound = 0
                                    print 'Checking if attacker is in blacklist'
                                    with open(blacklistpath, "r") as blacklistcontent:
                                        for blacklistline in blacklistcontent:
                                            blacklistline = blacklistline.strip('\n')
                                            print 'Comparing: ' + str(blacklistline) + ' against ' + str(attacker)
                                            if blacklistline == attacker:
                                                print 'Matched'
                                                attackerfound = 1
                                                break
                                    # Add attacker to blacklist
                                    if attackerfound == 0:
                                        blacklistfile = open(blacklistpath,"a")
                                        blacklistfile.write(attacker+'\n')
                                        blacklistfile.close()
                                        print 'Writting attacker ip to blacklist: ' + str(attacker)
                                else:
                                    blacklistfile = open(blacklistpath,"w+")
                                    blacklistfile.write(attacker+'\n')
                                    blacklistfile.close()

                                # Check if attacker is in running iptables rules
                                print 'Checking in running iptables rules'
                                iptablesdata = iptc.easy.dump_table('filter', ipv6=False)
                                attackerfoundiptables = 0
                                for rule in iptablesdata['INPUT']:
                                    if 'src' in rule and 'target' in rule:
                                        if rule['src'] == attacker+'/32' and rule['target'] == 'DROP':
                                            attackerfoundiptables = 1
                                            print 'Matched'
                                            break

                                # Add attacker to running iptables rules
                                if attackerfoundiptables == 0:
                                    print 'Inserting in running iptables rules'
                                    rule = iptc.Rule()
                                    rule.src = attacker
                                    rule.create_target("DROP")
                                    rule.target = iptc.Target(rule, "DROP")
                                    chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
                                    chain.insert_rule(rule)

                                    msg = 'Banning time for: ' + str(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) + '\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
                            else:
                                print 'Incrementing redis key value for IP: ' + str(attacker)
                                redisconnection.incr(attacker)
                        else:
                            print 'Creating redis key for IP: ' + str(attacker)
                            redisconnection.incr(attacker)
                            redisconnection.expire(attacker, 60)
                        

wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm)
wm.add_watch('/opt/modsecurity/var/audit/', pyinotify.IN_CREATE, rec=True, auto_add=True, proc_fun=CommitFunction())
notifier.loop(daemonize=False, callback=None)

We assign the necessary permissions:

chmod 700 /usr/local/modsecurityNotifier.py

We create a systemd unit:

vi /etc/systemd/system/modsecurityNotifier.service

[Unit]
Description=Modsecurity Notifier
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/modsecurityNotifier.py

[Install]
WantedBy=default.target

We assign permissions to the unit file:

chmod 664 /etc/systemd/system/modsecurityNotifier.service

We reload the systemd configuration:

systemctl daemon-reload
systemctl start modsecurityNotifier.service
systemctl enable modsecurityNotifier.service

“Now, every time an alert is generated, we will receive it via Telegram :)”

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