Esta web utiliza cookies, puedes ver nuestra política de cookies aquí. Si continuas navegando estás aceptándola

libmodsecurity bajo FreeBSD + Nginx


Como ya explicamos en artículos anteriores(1, 2, 3, 4) LibmodSecurity es un WAF(Web Application Firewall), este nos permite detectar ciertos tipos de ataques en base a unas reglas predefinidas, mediante estas firmas podremos detectar inyecciones SQL, XSS, LFI, RFI. En esta ocasión vamos a montar un sistema mediante FreeBSD/jails que nos alertará vía Telegram y bloqueará al atacante en el host padre mediante ACLs de IPFW.

 

Si ya tenemos instalado Nginx debemos consultar si fué compilado con soporte para libmodsecurity:

pkg info nginx|grep MODSECURITY3
    MODSECURITY3   : off

NOTA: LA versión full(nginx-full) tampoco lleva la opción habilitada.

Compilamos mediante los ports con las opciones que deseemos, tener Nginx compilado con esta opción nos permitirá cargar el módulo(conector) de libmodsecurity, esta opción no es el módulo, si no la posibilidad de poder cargarlo.

Antes de compilar e intalar Nginx desde los ports desinstalamos la versión actual.

pkg delete nginx

Preparamos el sistema de ports:

portsnap fetch extract
cd /usr/ports
make fetchindex

Realizamos una búsqueda:

make search name=nginx
Port:    nginx-1.16.1_12,2
Path:    /usr/ports/www/nginx
Info:    Robust and small WWW server
Maint:    joneum@FreeBSD.org
B-deps:    pcre-8.43_2
R-deps:    pcre-8.43_2
WWW:    https://nginx.org/

Configuramos las opciones:

cd /usr/ports/www/nginx
make config-recursive

Compilamos e instalamos:

make -j2
make install

Limpiamos ficheros temporales:

make clean

Bloqueamos el paquete, de este modo pkg lo ignorará en las actualizaciones, para mas info sobre como actualizar sistemas mixtos binarios/ports consultar este artículo:

pkg lock nginx

Instalamos la librería libmodsecurity y el conector(módulo) para Nginx:

pkg install -y modsecurity3 modsecurity3-nginx

Nos bajamos las reglas de OWASP:

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

Nos bajamos el fichero de configuración base de libmodsecurity:

mkdir /usr/local/etc/modsec
cd /usr/local/etc/modsec
fetch https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended
fetch https://raw.githubusercontent.com/SpiderLabs/ModSecurity/49495f1925a14f74f93cb0ef01172e5abc3e4c55/unicode.mapping
mv modsecurity.conf-recommended modsecurity.conf

Nos aseguramos de tener ciertos parámetros con los valores indicados:

vi /usr/local/etc/modsec/modsecurity.conf
SecRuleEngine On
SecAuditLogFormat json
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log

Incluimos en la configuración principal el fichero base y las reglas de OWASP:

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

Habilitamos libmodsecurity en la configuración de Nginx:

vim /usr/local/etc/nginx/alfaexploit.conf
server {
    modsecurity on;
    modsecurity_rules_file /usr/local/etc/modsec/main.conf;

Reiniciamos el servicio:

service nginx restart

Probamos cualquier tipo de ataque, en mi caso una inyección MySQL con el siguiente payload:

' or '1==1'; --

Veremos una entrada nueva en el log y la petición web habrá sido bloqueada con un 403 Forbidden:

tail -f /var/log/modsec_audit.log

Si vamos a generar muchas entradas en el log es preferible escribirlas en formato concurrent, de este modo no se logearán en un fichero de texto de forma serializada, si no que se hará en distintos ficheros de forma paralela:

vi /usr/local/etc/modsec/modsecurity.conf
#SecAuditLogType Serial
SecAuditLogType Concurrent
#SecAuditLog /var/log/modsec_audit.log
SecAuditLogStorageDir /opt/modsecurity/var/audit

Reiniciamos el servicio:

service nginx restart

Creamos el directorio donde almacenar las entradas de log:

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

Instalamos la herramienta jq, esta nos será útil para visualizar la información de los logs de forma mas cómoda:

pkg install -y jq

Consultamos el campo message de una de las entradas:

cat /opt/modsecurity/var/audit/20200321/20200321-0034/20200321-003454-158474729484.345827 | jq '.transaction.messages[].message'
"SQL Injection Attack Detected via libinjection"
"Inbound Anomaly Score Exceeded (Total Score: 5)"
"Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=5,XSS=0,RFI=0,LFI=0,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 5, 0, 0, 0"

NOTA: No es necsario filtrar si queremos ver la información completa simplemente empipamos la salida del cat a un jq sin parámetros.

Si en nuestra aplicación web hay alguna funcionalidad que requiera un comportamiento un poco fuera de lo normal podemos whitelistearla para que las reglas no salten cuando se trate de este sección:

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 /customWebPath" \
    "id:1001,\
    phase:1,\
    pass,\
    nolog,\
    ctl:ruleEngine=Off"

Si por otro lado queremos deshabilitar alguna regla en concreto podemos utilizar la directiva SecRuleRemoveById:

NOTA : Esta directiva debe ser especificada después de la regla que se esté deshabilitando.

En mi caso estaba teniendo problemas con la regla:

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

Al mostrar código por la web la alarma salta por pensar que se está filtrando código fuente de la propia página, añadimos el fichero de reglas deshabilitadas:

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

Indicamos que regla deshabilitar:

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

Este manual está pensado para ser aplicado sobre una jail de FreeBSD con networking en modo alias, esto implica que no podremos aplicar reglas de firewall directamente en la jail habrá que hacerlo en el host padre, para ello vamos a utilizar un redis donde almacenaremos la información extraída de los logs de libmodsecurity, esta información será leída por el padre, aplicará las reglas de firewall y avisará sobre el incidente vía Telegram.

Instalamos el servidor de redis en el padre:

pkg install -y redis

Lo bindeamos a su ip y asignamos un password:

vi /usr/local/etc/redis.conf
bind X.X.X.X
requirepass XXXXXXXXXX

Habilitamos y arrancamos el servicio:

sysrc redis_enable=yes
service redis start

Consultamos el estado del redis:

redis-cli -h X.X.X.X -a XXXXXXXXXX info
# Server
redis_version:5.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:20e0e25f6a7c7398
redis_mode:standalone
os:FreeBSD 12.1-RELEASE-p3 amd64
arch_bits:64
multiplexing_api:kqueue
.......

Programamos el script que correrá en el servidor web, para ello instalamos algunas dependencias:

pkg install -y py37-pip
pip install pyinotify
pip install redis
vi /root/.scripts/modsecurityNotifier.py
#!/usr/local/bin/python

import pyinotify
import os
import json
import requests
import redis

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

print("-- Ready to process log files")
class CommitFunction(pyinotify.ProcessEvent):
    def process_default(self, event):
        fullpath = event.path + '/' + event.name
        if os.path.isfile(fullpath):
            print('------------------------------------------------')
            print("Processing: %s" % fullpath)
            with open(fullpath) as fp:
                for line in fp:
                    try:
                        rawdata = json.loads(line)
                    except:
                        continue

                    for messageline in rawdata['transaction']['messages']:
                        message = messageline['message']
                        data = messageline['details']['data']
                        # Delete not matched rules messages and anomaly score checks
                        if message != "" and data != "":
                            try:
                                timestamp = rawdata['transaction']['time_stamp']
                            except:
                                timestamp = 'NULL'
                            try:
                                attacker = rawdata['transaction']['request']['headers']['X-Forwarded-For']
                            except:
                                attacker = rawdata['transaction']['client_ip']
                                #attacker = '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(">> Timestamp: %s", timestamp)
                            print("Attacker: %s" % attacker)
                            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("Cheking redis IP: %s" % attacker)
                            if redisconnection.get(attacker):
                                rediscounter = redisconnection.get(attacker)
                                print("rediscounter: %s" % rediscounter)
                                print("Incrementing redis key value for IP: %s" % attacker)
                                redisconnection.incr(attacker)
                            else:
                                print("Creating redis key for IP: %s" % attacker)
                                redisconnection.incr(attacker)
                                redisconnection.expire(attacker, 300)

                            print("Filling attacker data")
                            datastring = timestamp + '++' + attacker + '++' + useragent + '++' + message + '++' + host + '++' + url + '++' + method + '++' + payload
                            redisconnection.set('data' + attacker, datastring)
                            redisconnection.expire('data' + attacker, 300)
                            print("Done")

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)

Asignamos los permisos correctos al script:

chmod 700 /root/.scripts/modsecurityNotifier.py

Para demonizar el script creamos la siguiente configuración de RC:

vi /usr/local/etc/rc.d/modsecurityNotifierWeb
#!/bin/sh
#
# PROVIDE: modsecurityNotifierWeb
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name=modsecurityNotifierWeb
rcvar=modsecurityNotifierWeb_enable

command="/root/.scripts/modsecurityNotifier.py"
start_cmd="modsecurityNotifierWeb_start"
pidfile="/var/run/${name}.pid"

modsecurityNotifierWeb_start(){
    echo "starting modsecurityNotifierWeb."
    /usr/sbin/daemon -c -f -p ${pidfile} ${command}
}

load_rc_config $name
run_rc_command "$1"

Asignamos los permisos necesarios al script:

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

Habilitamos el servicio y lo arrancamos:

sysrc modsecurityNotifierWeb_enable=yes
service modsecurityNotifierWeb start

Comprobamos que esté funcionando:

ps aux|grep modsecurityNotifier
root   86778  0.0  0.1   10844   2280  -  IsJ  01:23   0:00.00 daemon: /root/.scripts/modsecurityNotifier.py[87291] (daemon)
root   87291  0.0  0.6   37544  24392  -  IJ   01:23   0:00.43 /usr/local/bin/python /root/.scripts/modsecurityNotifier.py (python3.7)

Ahora falta la parte que correrá en el padre, instalamos las dependencias necesarias:

pkg install -y py37-pip
pip install redis
vi /root/.scripts/modsecurityNotifier.py
#!/usr/local/bin/python

import redis
import requests
import subprocess
import time

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

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

while(True):
    for attacker in redisconnection.scan_iter():
        #print("-- Analyzing key: %s" % attacker)
        #print("First 4 chars of key name: %s" % attacker[0:4])
        if attacker[0:4] == 'data':
            #print("data key detected")
            continue
        else:
            rediscounter = redisconnection.get(attacker)
            print("Attacker: %s Counter: %s" % (attacker, rediscounter))
            if int(rediscounter) >= 5:
                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)
    
                bannedhosts = subprocess.run(["ipfw", "table", "libmodsecurity", "list"], stdout=subprocess.PIPE, text=True)
    
                print("Banned hosts: %s" % bannedhosts.stdout)
                blockattacker = 1
                for bannedhost in bannedhosts.stdout.split('/32 0\n'):
                    if bannedhost == '':
                        continue
                    print("Comparing %s --> %s" % (bannedhost, attacker))
                    if bannedhost == attacker:
                        print("Attacker already banned, aborting")
                        blockattacker = 0
                        break
    
                #print("blockattacker var value: %s" % blockattacker)
                if blockattacker == 1:
                    print("-- Banning time for: %s" % attacker)
                    cmd = 'ipfw table libmodsecurity add ' + attacker
                    cmdreturnvalue = subprocess.call(cmd, shell=True)
                    print("cmdreturnvalue: %s" % cmdreturnvalue)
                    if cmdreturnvalue != 0:
                        print("++ ERROR: Cant add attacker ip to ipfw libmodsecurity table")
    
                    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) + '\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
    time.sleep(5)

Asignamos los permisos correctos al script:

chmod 700 /root/.scripts/modsecurityNotifier.py

El bloqueo de atacantes se basa en la table libmodsecurity, añadimos ips a esa tabla e ipfw las bloqueará, la parte de configuración de IPFW que lo hace posible es esta:

# libmodsecurity integration table
N=$(ipfw table all list|grep libmodsecurity|wc -l)
if [ $N -eq 0 ]; then
    ipfw table libmodsecurity create
fi
$cmd 00020 deny tcp from "table(libmodsecurity)" to any
$cmd 00020 deny udp from "table(libmodsecurity)" to any
$cmd 00020 deny icmp from "table(libmodsecurity)" to any

Para demonizar el script creamos la siguiente configuración de RC:

vi /usr/local/etc/rc.d/modsecurityNotifierIpfw
#!/bin/sh
#
# PROVIDE: modsecurityNotifierIpfw
# REQUIRE: DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name=modsecurityNotifierIpfw
rcvar=modsecurityNotifierIpfw_enable

command="/root/.scripts/modsecurityNotifier.py"
start_cmd="modsecurityNotifierIpfw_start"
pidfile="/var/run/${name}.pid"

modsecurityNotifierIpfw_start(){
    echo "starting modsecurityNotifierIpfw."
    /usr/sbin/daemon -c -f -p ${pidfile} ${command}
}

load_rc_config $name
run_rc_command "$1"

Asignamos los permisos necesarios al script:

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

Habilitamos el servicio y lo arrancamos:

sysrc modsecurityNotifierIpfw_enable="YES"
service modsecurityNotifierIpfw start

Comprobamos que esté funcionando:

ps aux|grep modsecurityNotifier
root   86778   0.0  0.1   10844   2280  -  IsJ  01:23      0:00.00 daemon: /root/.scripts/modsecurityNotifier.py[87291] (daemon)
root   87291   0.0  0.6   37404  24396  -  IJ   01:23      0:00.44 /usr/local/bin/python /root/.scripts/modsecurityNotifier.py (python3.7)
root   90940   0.0  0.1   10844   2380  -  Is   01:24      0:00.00 daemon: /root/.scripts/modsecurityNotifier.py[91583] (daemon)
root   91583   0.0  0.8   41040  29668  -  S    01:24      0:03.38 /usr/local/bin/python /root/.scripts/modsecurityNotifier.py (python3.7)

Cuando se detecte un ataque recibiremos notificaciones vía Telegram del estilo:

Y se añadirá una entrada en la tabla libmodsecurity de ipfw:

ipfw table libmodsecurity list
X.X.X.X/32 0

Autor: Kr0m -- 22/03/2020 12:17:00