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.


En versiones anteriores a 03 Jun 2020 18:49:04 Nginx debía ser compilado con soporte para modsecurity3, tener la librería modsecurity3 y además instalar el conector modsecurity3-nginx, en versiones posteriores a dicha fecha si compilamos Nginx con soporte para modsecurity3 ya lleva incorporado el conector, pero debemos añadir la orden de carga del módulo en la configuración de Nginx, en la versión vieja este paso era innecesario.

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

Si instalamos modsecurity3-nginx con la versión nueva de Nginx y cargamos el módulo desde la configuación de Nginx veremos el siguiente 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

Además pkg nos advertirá de que el paquete modsecurity3-nginx y nginx entran en conflicto ya que los dos intentan instalar el mismo conector.


Compilamos mediante ports con las opciones que deseemos, nos aseguramos de habilitar modsecurity3 y recordad que una vez se dá el paso a ports hay que gestionar todos los paquetes desde ports, podéis encontrar mas información en este artículo anterior.

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.18.0_12,2
Path: /usr/ports/www/nginx
Info: Robust and small WWW server
Maint: joneum@FreeBSD.org
B-deps: pcre-8.44
R-deps: pcre-8.44
WWW: https://nginx.org/

Configuramos las opciones:

cd /usr/ports/www/nginx
make config

Compilamos e instalamos:

make -j2
make install

Limpiamos ficheros temporales:

make clean

Instalamos la librería libmodsecurity, recordad que ahora ya debemos gestionar todos los paquetes desde ports:

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

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 añadiendo como primera líneael load del módulo:

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;

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:

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

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

En mi caso correré el script de análisis de logs en el padre de la jail donde corre libmodsecurity, el análisis y el baneo está dividido en dos partes por si hubiese que mover el baneo a un balanceador de carga.

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
.......

Instalamos las dependencias de los scripts:

pkg install -y py37-pip
pip install watchdog
pip install redis

Programamos el script que analizará los logs:

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

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

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 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("Processing: %s" % event.src_path)
                with open(event.src_path) 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")

if __name__ == "__main__":
    path = '/zroot/iocage/jails/JAILNAME/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()

Le asignamos los permisos necesarios:

chmod 700 /root/.scripts/modsecurityAnalizer.py

Para demonizar el script seguimos los siguientes pasos:

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"
pidfile="/var/run/${name}.pid"

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

load_rc_config $name
run_rc_command "$1"

Asignamos permisos y propietario:

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

Habilitamos el servicio y lo arrancamos:

sysrc modsecurityAnalizer_enable="YES"
service modsecurityAnalizer start

Comprobamos que esté corriendo:

ps aux|grep modsecurityAnalizer

Ahora el script de notificación y baneo:

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

import redis
import requests
import subprocess
import time

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

try:
    redisconnection = redis.Redis(host="X.X.X.X", port=6379, db=0, password='XXXXXXXXXXXX', 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 = '/sbin/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 necesarios:

chmod 700 /root/.scripts/modsecurityNotifier.py

Demonizamos el script:

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"
pidfile="/var/run/${name}.pid"

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

load_rc_config $name
run_rc_command "$1"

Asignamos permisos:

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

Habilitamos y arrancamos el servicio:

sysrc modsecurityNotifier_enable="YES"
service modsecurityNotifier start

Comprobamos que esté arrancado:

ps aux|grep modsecurityNotifier

Todo el proceso de baneo se basa en la tabla libmodsecurity, se añadimos ips a esa tabla ipfw las bloqueará:

# 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

NOTA: Las reglas de ipfw se flushearán si reiniciamos el server, esto no importa ya que solo queremos banear al atacante mientras realiza el ataque, además de este modo la tablas simpre serán pequeñas y el chequeo de estas seguirá siendo un proceso liviano.

Para limpiar de forma rápida las ACLs podemos utilizar el siguiente script:

vi /root/.scripts/clearmodsecurityAcls
#! /usr/local/bin/bash
redis-cli -a XXXXXX -h X.X.X.X "FLUSHALL"
ipfw table libmodsecurity flush
chmod 700 /root/.scripts/clearmodsecurityAcls

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
Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Si tienes cualquier pregunta siempre puedes enviarme un Email o escribir en el grupo de Telegram de AlfaExploit.
Autor: kr0m -- 22/03/2020 12:17:00 -- Categoria: Seguridad FreeBSD