El mes pasado expliqué detalladamente cómo instalar y configurar ModSecurity en Apache sobre servidores Debian. En este artículo voy a compartir un muy útil script Bash para generar un resumen del log de auditoría de ModSecurity.

En principio estoy utilizando ModSecurity sólo en modo "DetectionOnly". Esto quiere decir que el módulo detecta posibles ataques, pero no los bloquea, sólo los reporta en su log de auditoría (por defecto /var/log/apache2/modsec_audit.log). Además he habilitado unas pocas reglas, para ir probando una a una hasta eliminar todos los falsos positivos.

El problema es que el log de auditoría de ModSecurity es extremadamente verboso. Por cada evento almacena una gran cantidad de información (algo a tener en cuenta al momento de activarlo en un servidor con mucha carga). Incluso, cuando una regla de ModSecurity se activa sobre un pedido, se vuelca en los logs tanto la solicitud (GET/POST) como la respuesta enviada por el servidor (a fin de poder realizar luego un análisis forense). Por esta razón (y al no haber encontrado una herramienta que haga el trabajo) decidí implementar mi propio script que genere un resumen o síntesis del log modsec_audit.log, para obtener sólo los eventos relevantes (intentos de ataque, descartando errores y otros) junto con una cantidad mínima de información.

De acuerdo a la documentación de ModSecurity, cada entrada en el log cuenta con diferentes secciones, llamadas A, B, C, hasta Z; siendo las más importantes B (encabezado del evento) y H (log de auditoría).

Esto significa que (a diferencia de la mayoría de los formatos de archivo de log, donde hay sólo una línea por cada evento) cada evento en el log de auditoría de ModSecurity cuenta con una cantidad variable de líneas, lo cual dificulta notablemente su análisis.

Sin embargo (a pesar de parecer extenso y complicado) el script, el cual he decidido llamar modsec_audit-parser.sh, es muy simple. Primero se encarga de obtener (desde los archivos modsec_audit.log y modsec_audit.log.1, pues asume que se está utilizando logrotate) sólo los eventos del día actual (o a partir de ayer, según cómo se lo configure). Luego obtiene todos los identificadores de eventos. Cada evento tiene un ID único que abre, en la cabezera (sección A), y luego cierra para marcar el fin (sección Z).

Una vez que conoce todos los ID de eventos, comienza a extraer sólo la información relevante de cada uno, dentro de un ciclo for. Para ello sólo utiliza herramientas básicas como cat, cut, grep y awk.

# nano modsec_audit-parser.sh
#!/bin/bash

#=============================================================================
# Title       : modsec_audit-parser.sh
# Description : Script to summarize/digest ModSecurity's audit log
# Author      : Emiliano Marini
# Date        : 2015-08-31
# Notes       : Assume section C not present in audit logs. Ignore E, I, J, K.
# Usage       : ./modsec_audit-parser.sh [LOGFILE]
# github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#secauditlogparts
#=============================================================================

# Variables
LOG=/var/log/apache2/modsec_audit.log
LINES[0]=""
MODSECMSG=""

# Temporary files
TMPFILE0=/tmp/.modsec_audit-parser0.tmp
TMPFILE1=/tmp/.modsec_audit-parser1.tmp
TMPFILE2=/tmp/.modsec_audit-parser2.tmp

# Get today's date in the form "[d/m/Y:" (for example: "[07/Sep/2015:")
TODAY=$(date +\\[%d\\/%b\\/%Y:)
YESTERDAY=$(date --date="1 days ago" +\\[%d\\/%b\\/%Y:)

# Get today's events only
cat $LOG.1 $LOG | awk '/'${YESTERDAY}'/{flag=1}flag' > $TMPFILE0

# Get all event IDs
IDS=$(grep "^--" $TMPFILE0 | cut -d'-' -f3 | uniq)

# Process each ID
for ID in $IDS; do

  # Get lines related to current ID
  cat $TMPFILE0 | awk "/${ID}-A/{flag=1}/${ID}-Z/{flag=0}flag" > $TMPFILE1

  # IMPORTANT: some events can share the same ID

  #=============================#
  # Section A: audit log header #
  #=============================#

  # Extract datetime and source address
  cat $TMPFILE1 | awk "/${ID}-B/{flag=0}flag;/${ID}-A/{flag=1}" > $TMPFILE2

  INDEX=0
  # Print datetime
  for DATE in $(cat $TMPFILE2 | cut -d']' -f1 | cut -d'[' -f2 | cut -d' ' -f1); do
    # Shorten datetime (strip year and second)
    DATE1=$(echo $DATE | cut -d':' -f1 | cut -d'/' -f1,2)
    TIME=$(echo $DATE | cut -d'/' -f3 | cut -d':' -f2,3)
    LINES[$INDEX]=$(echo -n $ID" "$DATE1 $TIME)
    INDEX=$((INDEX+1))
  done

  # Save lines count
  NLINES=$INDEX

  INDEX=0
  # Print source address
  for ADDR in $(cat $TMPFILE2 | cut -d']' -f2 | cut -d' ' -f3); do
    LINES[$INDEX]=$(echo -n ${LINES[$INDEX]}" ["$ADDR"]")
    INDEX=$((INDEX+1))
  done

  # Each item in LINES contains an event for the current ID

  #===========================#
  # Section B: request header #
  #===========================#

  # Extract host header and resource
  cat $TMPFILE1 | awk "/${ID}-F/{flag=0}flag;/${ID}-B/{flag=1}" > $TMPFILE2

  INDEX=0
  # Print host header
  for HOST in $(cat $TMPFILE2 | grep "Host:" | cut -d':' -f2); do
    LINES[$INDEX]=$(echo -n ${LINES[$INDEX]} $HOST)
    INDEX=$((INDEX+1))
  done

  INDEX=0
  # Print resource (GET/POST request)
  for RES in $(cat $TMPFILE2 | grep "^GET\|^HOST" | cut -d' ' -f2); do
    LINES[$INDEX]=$(echo -n ${LINES[$INDEX]}$RES)
    INDEX=$((INDEX+1))
  done

  #===============================#
  # Section C: assume not present #
  #===============================#

  #============================#
  # Section D: not implemented #
  #============================#

  #==========================================================#
  # Section F: response headers (stop at H or E, if present) #
  #==========================================================#

  # Commented out: pointless

  ## Extract HTTP status code
  #cat $TMPFILE1 | awk "/${ID}-H/{flag=0}/${ID}-E/{flag=0}flag;/${ID}-F/{flag=1}" > $TMPFILE2

  #INDEX=0
  ## Print HTTP status code
  #for RET in $(cat $TMPFILE2 | grep "^HTTP" | cut -d' ' -f2); do
  #  LINES[$INDEX]=$(echo -n ${LINES[$INDEX]} $RET)
  #  INDEX=$((INDEX+1))
  #done

  #============================#
  # Section G: not implemented #
  #============================#

  #======================#
  # Section H: audit log #
  #======================#

  # Extract Apache's error msg and/or ModSecurity audit message (if any)
  cat $TMPFILE1 | awk "/${ID}-I/{flag=0}/${ID}-J/{flag=0}/${ID}-K/{flag=0}/${ID}-Z/{flag=0}flag;/${ID}-H/{flag=1}" | grep "^Message" > $TMPFILE2

  FLAG=""
  MODSECMSG=""

  # Print msg and severity
  while read -r MSG; do

    TMPMSG=$(echo $MSG | grep -o -P '(?<=msg ).*' | cut -d']' -f1 | cut -d'"' -f2)

    # If many events share the same ID, in this way (grepping all messages for the current ID)
    # it's not possible to relate messages to events, only messages to IDs

    # This block aggregates all messages in a single line (excluding duplicates)
    if [[ $MODSECMSG == "" ]]; then
      SEVERITY=$(echo $MSG | grep -o -P '(?<=severity ).*' | cut -d']' -f1 | cut -d'"' -f2)
      if [ ! -z "$SEVERITY" ]; then
        FLAG="1"
        MODSECMSG="$TMPMSG ($SEVERITY)"
      fi
    elif [[ $MODSECMSG != *"$TMPMSG"* ]]; then
      SEVERITY=$(echo $MSG | grep -o -P '(?<=severity ).*' | cut -d']' -f1 | cut -d'"' -f2)
      if [ ! -z "$SEVERITY" ]; then
        FLAG="1"
        MODSECMSG="$MODSECMSG / $TMPMSG ($SEVERITY)"
      fi
    fi
  done < $TMPFILE2

  # Print current lines (only if there are ModSecurity messages)
  if [ ! -z "$FLAG" ]; then

    # Show only the first event for the current ID
    echo -n "${LINES[0]]} --"

    # Show all ModSecurity messages for the current ID (every events)
    echo " $MODSECMSG"
  fi

  # Forget about LINES
  unset LINES

done

# Cleanup
rm $TMPFILE0 $TMPFILE1 $TMPFILE2 >/dev/null 2>&1

 

El script está en una versión alpha, aún no soporta parámetros y funciona sólo en sistemas Debian y derivados. Mi idea es que a futuro sea portable (funcione en cualquier distribución GNU/Linux) y soporte opciones y parámetros de línea de comandos como: control de verbosidad (-v), control de fecha de eventos (hoy, ayer o incluso x cantidad de días atrás), uso de archivos (habilitar lectura de logs comprimidos mediante zcat), y por supuesto configuración de la ruta a los logs de ModSecurity.

En cuanto monte un repositorio en GitHub subiré éste y otros de mis scripts actualizados.

Ejemplo de ejecución:

# ./modsec_audit-parser.sh 
775cb27f 07/Sep 06:09 [157.55.39.253] www.linuxito.com/index.php?buscar=prueba; -- SQL Injection Attack: Common Injection Testing Detected (CRITICAL)
a38cd14e 07/Sep 14:22 [10.10.45.15] www.linuxito.com/includes/form.php?i=a&r=85&g=5&b=6222&add=E-mail:  -- Detects classic SQL injection probings 2/2 (CRITICAL)
7dbb2e45 07/Sep 16:22 [10.10.45.15] www.linuxito.com/includes/form.php?i=a&r=85&g=5&b=6222&add=E-mail:  -- Detects classic SQL injection probings 2/2 (CRITICAL)
7b38925c 07/Sep 06:21 [207.46.13.0] www.linuxito.com/index.php?buscar=prueba; -- SQL Injection Attack: Common Injection Testing Detected (CRITICAL)
ee7e685f 07/Sep 10:20 [192.168.2.34] www.linuxito.com/images/?test=1%27%20-- -- Detects MySQL comments, conditions and ch(a)r injections (CRITICAL)
5c45b830 07/Sep 10:21 [192.168.2.33] www.linuxito.com/images/?test=1%27;%20INSERT%20INTO%20users%20(name,passwd)%20VALUES%20(%27admin%27,%271234%27);%20-- -- SQL Injection Attack (CRITICAL) / Detects MySQL comment-/space-obfuscated injections and backtick termination (CRITICAL) / Detects chained SQL injection attempts 1/2 (CRITICAL) / Detects MySQL UDF injection and other data/structure manipulation attempts (CRITICAL) / Detects concatenated basic SQL injection and SQLLFI attempts (CRITICAL) / Detects classic SQL injection probings 2/2 (CRITICAL)

El resultado es sólo una línea por cada evento, con la cantidad de información mínima relacionada al mismo: ID, fecha, hora, IP fuente, host/recurso, mensaje de ModSecurity.

Gracias a este script pude comenzar a detectar los primeros falsos positivos. Por ejemplo, los robots de Microsoft (207.46.13.0) agregan un punto y coma al final del GET en algunos pedidos HTTP, lo cual es detectado como intento de inyección de código SQL por una de las reglas que incluye el paquete libapache2-modsecurity.

Esto marca claramente que es necesario pulir las reglas que se incluyen por defecto con ModSecurity, de lo contrario habrá una gran cantidad de falsos positivos.


Tal vez pueda interesarte


Compartí este artículo