Contenu de l'article
Présentation
ModSecurity est un pare-feu d'applications web (WAF) open-source qui protège les applications web contre diverses menaces et vulnérabilités. Fonctionnant comme un module pour des serveurs web populaires tels qu'Apache, Nginx et IIS, ModSecurity analyse les requêtes HTTP et détecte des attaques courantes comme les injections SQL, le cross-site scripting (XSS), et le cross-site request forgery (CSRF). En plus de bloquer les menaces, il offre des fonctionnalités avancées de journalisation et de surveillance, permettant aux administrateurs de surveiller l'activité des applications web et de réagir rapidement aux incidents de sécurité
Installation
- Installer le module Apache HTTPd ModSecurity 2.9 à partir des dépôts :
# dnf install mod_security
- Récupérer l'ensemble des règles de sécurité de base OWASP :
# cd /tmp
# git clone https://github.com/coreruleset/coreruleset.git - Installer les règles et leur fichier de configuration :
# cp ./coreruleset/rules/* /etc/httpd/modsecurity.d/activated_rules/
# cp ./coreruleset/crs-setup.conf.example /etc/httpd/modsecurity.d/crs-setup.conf - Vérifier dans le fichier de configuration de ModSecurity /etc/httpd/conf.d/mod_security.conf la bonne inclusion des fichiers de règles et de leur fichier de configuration.
- Redémarrer Apache HTTPd :
# systemctl restart httpd
- Vérifier dans /var/log/httpd/error_log que des lignes comme les suivantes apparaissent bien :
[Sat Jun 01 10:16:35.664927 2024] [:notice] [pid 58558:tid 58558] ModSecurity: APR compiled version="1.7.0"; loaded version="1.7.0"
[Sat Jun 01 10:16:35.664929 2024] [:notice] [pid 58558:tid 58558] ModSecurity: PCRE compiled version="8.44 "; loaded version="8.44 2020-02-12"
[Sat Jun 01 10:16:35.664930 2024] [:notice] [pid 58558:tid 58558] ModSecurity: LUA compiled version="Lua 5.4"
[Sat Jun 01 10:16:35.664931 2024] [:notice] [pid 58558:tid 58558] ModSecurity: YAJL compiled version="2.1.0"
[Sat Jun 01 10:16:35.664932 2024] [:notice] [pid 58558:tid 58558] ModSecurity: LIBXML compiled version="2.9.13"
Configuration
Paramètres généraux
Le fichier /etc/httpd/conf.d/mod_security rassemble un certain nombre de directives et de paramètres qui doivent être inclus dans la configuration de HTTPd :
SecRuleEngine On/Off/DetectionOnly | Cette directive active ou désactive le moteur de ModSecurityDetectionOnly : Active le moteur en mode détection seulement (ne bloque pas les requêtes, mais les journalise). |
SecRequestBodyAccess On/Off | Cette directive contrôle l'accès au corps des requêtes (utile pour analyser les données POST). |
SecResponseBodyAccess On/Off | Cette directive contrôle l'accès au corps des réponses (utile pour analyser les réponses des serveurs). |
SecRule | Cette directive est utilisée pour définir les règles de sécurité |
SecRequestBodyLimit 13107200 SecRequestBodyNoFilesLimit 131072 |
Ces directives définissent les limites de taille pour le corps des requêtes et pour le corps des requêtes sans fichiersSecRequestBodyLimit : Limite de taille pour le corps des requêtes (en octets).SecRequestBodyNoFilesLimit : Limite de taille pour le corps des requêtes sans fichiers (en octets). |
SecRespondeBodyLimit 524288 | Cette directive définit la limite de taille pour le corps des réponses (en octets). |
SecAuditEngine On/Off/RelevantOnly | Cette directive contrôle le moteur d'audit, qui enregistre les requêtes et réponses pour analyse.RelevantOnly : Active l'audit uniquement pour les événements pertinents. |
SecAuditLog /var/log/httpd/modsec_audit.log | Cette directive définit le fichier de log pour les audits. |
SecDebugLog /var/log/httpd/modsec_debug.log SecDebugLogLevel 3 |
Ces directives définissent le fichier de log de débogage et le niveau de débogage. Niveau de débogage (0 à 9, où 0 est désactivé et 9 est le plus verbeux). |
SecDefaultAction "phase:1,log,auditlog,pass" | Cette directive définit l'action par défaut pour toutes les règles suivantes. |
Include | Cette directive est utilisée pour inclure d'autres fichiers de configuration. |
Les règles
Les règles sont rassemblées de façon thématique dans des fichiers de règles. Par exemple, le fichier REQUEST-942-APPLICATION-ATTACK-SQLI.conf rassemble toutes les règles concernant les attaques par injection SQL. Pour que ModSecurity puisse fonctionner, il faut donc que les fichiers de règles ainsi que le fichier de configuration soient également inclus dans la configuration de HTTPd.
Ajuster les détections
Si nous installons ModSecurity sur un proxy inverse protégeant plusieurs applications, il est probable que nous souhaitions ajuster les détections pour éliminer des règles inutiles, désactiver certaines règles ou encore changer leur comportement. Pour cela, nous pouvons :
- désactiver certains fichiers de règles au niveau d'un virtualhost en nous appuyant sur l'inclusion ou non de ces fichiers
- désactiver certaines règles au niveau d'un virtualhost en utilisant la directive SecRuleRemoveById <rule_id>
- changer le comportement bloquant ou non d'un règle en la redéfinissant dans le virtualhost
- changer le comportement global bloquant ou non avec la directive SecRuleEngine On/Off/DetectionOnly
Remarques
ModSecurity limite le temps qu'il passe à vérifier les correspondances avec les regex de ses règles afin notamment de se prémunir contre des attaques DDOS. Mais parfois, ses limites par défaut étant assez restrictives, il s'arrête sans bonne raison et il logue alors l'erreur suivante :
Execution error - PCRE limits exceeded (-8)
SecPcreMatchLimit 150000 SecPcreMatchLimitRecursion 150000
Tester ModSecurity
On peut vérifier le bon fonctionnement de mod_security avec les deux tests simples suivants. Pour des tests plus évolués, on peut utiliser des outils tels que Nikto ou OWASP ZAP.
Test 1
Tester le blocage de la requête suivante par mod_security. L'erreur 403 ou 404 doit être retournée :
# curl -d "id=1 AND 1=1" http://yourserver.com
Test 2
- Créer une règle de test en ajoutant la ligne suivante à la fin du fichier /etc/httpd/conf.d/mod_security.conf :
SecRule ARGS:testparam "@contains test" "id:12345,deny,status:403,msg:'Test rule triggered'"
Cette règle déclenchera une erreur 403 si une requête contient un paramètretestparam
avec la valeurtest
. - Redémarrer Apache HTTPd :
# systemctl restart httpd
- Envoyer la requête suivante et constater le blocage :
# curl "http://localhost/?testparam=test"
Monitorer avec Elasticsearch
Configurer Filebeat
Notre but est d'envoyer les logs correspondant aux requêtes bloquées par ModSecurity sur le serveur Web vers un index Elasticsearch :
- Récupérer la version d'Elasticsearch utilisée, par exemple en lançant la requête suivante :
# curl -X GET "http://<adresse-du-cluster>:9200/"
- Sur le serveur Web, installer le package de l'agent Filebeat correspondant à la version de notre serveur Elasticsearch. Dans cet exemple, ce sera la version 8.8.2 :
# cd /tmp
# curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.8.2-x86_64.rpm
# rpm -vi ./filebeat-8.8.2-x86_64.rpm
# systemctl enable filebeat - Dans Kibana et < Management / Index Management / index Templates >, créer un template de type datastream. Dans notre exemple, nous créerons le template nommé modsecurity-template de type "datastream" avec un pattern "modsecurity*".
- Créer un fichier de configuration /etc/filebeat/filebeat.yml pour analyser le fichier de log de ModSecurity /var/log/httpd/modsec_audit.log et envoyer les données extraites à le serveur Elasticsearch. On pourra s'inspirer du fichier suivant :
# ============================== Filebeat inputs ===============================
filebeat.inputs:
- type: filestream
enabled: true
paths:
- /var/log/httpd/modsec_audit.log
parsers:
# ModSecurity éclate ses événements logués sur plusieurs lignes divisées
# en plusieurs sections. Le mode multiligne permet de regrouper toutes
# les lignes en un seul bloc à analyser. Cela permettre de ne créer qu'un
# document Elasticsearch pour un événement ModSecurity. Pour identifier
# le début et la fin du bloc, on s'appuie sur des patterns de ModSecurity
- multiline:
type: pattern
pattern: '^--[a-zA-Z0-9]+-A--$'
negate: true
match: after
flush_pattern: '^--[a-zA-Z0-9]+-Z--$'
# ============================== Elasticsearch output ==========================
output.elasticsearch:
hosts: ["http://192.168.30.14:9200"]
# username: "elastic"
# password: "your_elasticsearch_password"
index: "modsecurity"
setup.template:
name: "modsecurity-template"
pattern: "modsecurity*"
type: "data_stream"
# ============================== Filebeat modules ==============================
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
# =============================== Processors ===================================
# On utilise ici le processor "dissect" plutôt que les regex de Grok car il se
# montre plus performant et plus facile à configurer
processors:
# Extraction initiale de l'id et de la section - dissect: tokenizer: "%{}--%{id}-%{section}-A--%{}" field: "message" target_prefix: "modsec" # Traitement de la section A - dissect: tokenizer: "%{}[%{timestamp}] %{unique_id} %{src_ip} %{src_port} %{dest_ip} %{dest_port}\n%{}" field: "message" target_prefix: "modsec" # Traitement de la section B - dissect: tokenizer: "%{}-B--\n%{http_method} %{url} HTTP/%{http_version}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Host: %{host}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Connection: %{connection}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}sec-ch-ua: \"%{sec-ch-ua}\";%{sec-ch-ua-detail}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}sec-ch-ua-mobile: %{sec-ch-ua-mobile}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}User-Agent: %{user-agent}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}sec-ch-ua-platform: \"%{sec-ch-ua-platform}\"\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Origin: %{origin}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Referer: %{referer}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Accept-Language: %{accept-language};%{accept-language-detail}\n%{}" field: "message" target_prefix: "modsec" # Traitement de la section F - dissect: tokenizer: "%{}-F--\n%{} %{http_error} %{http_message}\n%{}" field: "message" target_prefix: "modsec" # Traitement de la section H - dissect: tokenizer: "%{}Message:%{}[file \"%{rule1_file}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}[line \"%{rule1_line}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}[id \"%{rule1_id}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}[msg %{rule1_msg}]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}[severity %{rule1_severity}]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}Message:%{}[file \"%{rule2_file}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}Message:%{}[line \"%{rule2_line}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}Message:%{}[id \"%{rule2_id}\"]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}Message:%{}[msg %{rule2_msg}]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Message:%{}Message:%{}[severity %{rule2_severity}]%{}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Apache-Handler: %{apache_handler}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Stopwatch: %{sw_timestamp} %{time_micro_sec} %{sw_detail}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Producer: %{producer_name} (%{producer_link}); %{producer_ruleset}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Server: %{web_server} (%{operating_system}) %{security_layer}\n%{}" field: "message" target_prefix: "modsec" - dissect: tokenizer: "%{}Engine-Mode: \"%{engine-mode}\"\n%{}" field: "message" target_prefix: "modsec"
# Supprimer le champ message pour éviter la redondance
- drop_fields:
fields: ["message"]
# On envoie vers Elasticsearch que les blocages, qu'ils soient effectifs ou non (On/Off/DetectionOnly)
- drop_event:
when:
not:
has_fields: ["modsec.rule1_id"]
# =============================== Logging =====================================
logging.level: debug
logging.selectors: ["*"]
logging.to_files: true
logging.files:
path: /var/log/filebeat
name: filebeat
keepfiles: 7
permissions: 0644
format: pretty - Vérifier la syntaxe du fichier de configuration :
# filebeat test config
- Relancer Filebeat et vérifier son statut :
# systemctl restart filebeat
# systemctl status filebeat
Tester l'envoi de log
- Ouvrir un terminal sur le serveur Web et faire un "tail -f" du fichier de log de ModSecurity :
# tail -f /var/log/httpd/modsec_audit.log
- Ouvrir un deuxième terminal et lancer l'analyseur de paquet tcpdump pour vérifier l'envoi des données vers le serveur Elasticsearch :
# tcpdump -i <interface_reseau> port 9200
- Lancer Kibana et aller dans Discover pour afficher les documents enregistrés durant les 15 dernières minutes :
- A partir d'un scanner de vulnérabilités tel que Nikto, lancer une recherche vers le serveur Web
- Constater que le fichier de log modsec_audit.log se remplit
- Au bout de plusieurs secondes ou dizaines de secondes, constater que tcpdump affiche les requêtes à destination du serveur Elasticsearch
- Dans Kibana, faire un refresh et constater que de nouveaux documents ont été enregistrés dans l'index
- Consulter et vérifier les champs des documents enregistrés. Si tout est OK, on sera alors en mesure de créer des dashboards sous Kibana ou Grafana pour visualiser en quasi temps réel les attaques et les blocages de requêtes opérés par ModSecurity
Géolocaliser les attaques
Pour avoir une idée d'où proviennent les requêtes loguées par ModSecurity, nous pouvons ajouter un traitement au niveau du noeud Elasticsearch (ingest-pipeline) afin d'ajouter juste avant l'indexation des informations de géolocalisation basées sur l'adresse IP source. Nous serons ainsi en mesure d'afficher dans un dashboard de Grafana ou Kibana une map monde d'activité du plus bel effet.
- Ajouter le paramètre suivant dans le fichier /etc/elasticsearch/elasticsearch.yml du noeud Elasticsearch si on veut configurer la mise à jour automatique de la base de données de géolocalisation gérée par Elasticsearh :
ingest.geoip.downloader.enabled: true
- Si le noeud doit passer par un proxy pour accéder au Web alors ajouter les paramètres proxy dans le fichier /etc/elasticsearch/jvm.options :
# Proxy de sortie
-Dhttp.proxyHost=<IP_Proxy>
-Dhttp.proxyPort=<Port_Proxy>
-Dhttps.proxyHost=<IP_Proxy>
-Dhttps.proxyPort=<Port_Proxy - Relancer le serveur Elasticsearch :
# systemctl restart elasticsearch
# systemctl status elasticsearch - Elasticsearch gère la mise à jour de façon automatique et il semble qu'il n'y ait aucun moyen de la forcer manullement immédiatement :-(. On peut toutefois vérifier dans Kibana la mise à jour effective de la base de données avec la requête suivante :
GET /_ingest/geoip/stats?pretty
- Maintenant, nous pouvons créer un ingest pipeline qui ajoute les informations de géolocalisation de nos trois fichiers :
PUT _ingest/pipeline/geoip_full
{
"description": "Enrichissement GeoIP basé sur le champ src_ip avec les bases GeoLite2-City, GeoLite2-Country et GeoLite2-ASN",
"processors": [
{
"geoip": {
"field": "modsec.src_ip",
"database_file": "GeoLite2-City.mmdb",
"target_field": "modsec.geoip_city"
}
},
{
"geoip": {
"field": "modsec.src_ip",
"database_file": "GeoLite2-Country.mmdb",
"target_field": "modsec.geoip_country"
}
},
{
"geoip": {
"field": "modsec.src_ip",
"database_file": "GeoLite2-ASN.mmdb",
"target_field": "modsec.geoip_asn"
}
}
]
} - Vérifier le bon fonctionnement de l'ingest pipeline avec la requête suivante :
POST /_ingest/pipeline/geoip_full/_simulate?pretty { "docs": [ { "_source": { "modsec": { "src_ip": "8.8.8.8" } } } ] }
Note : quand on écrit un document avec des champs imbriqués, il faut respecter l'encapsulation présentée dans l'exemple précédent. Si on référence le champ imbriqué comment dans une requête "match", on utilise alors la syntaxe en ligne (ex :modsec.src_ip) - On peut également s'inspirer des 2 requêtes suivantes pour envoyer un document de test dans un datastream en passant par un pipeline ingest GeoIP en le spécifiant soit dans l'URL soit dans le corps de la requête :
POST /filebeat-8.8.2/_doc?pipeline=geoip_full
Note : dans une requête bulk, chaque objet doit être spécifié sur une seule ligne
{
"@timestamp": "2023-06-18T12:34:56Z",
"message": "test1",
"modsec": {
"src_ip": "8.8.8.8"
}
}
POST /filebeat-8.8.2/_bulk { "create": { "_index": "filebeat-8.8.2", "pipeline": "geoip_full" } } { "@timestamp": "2024-06-20T19:12:25.366Z","modsec": { "src_ip": "8.8.8.8" },"message": "Test2" } - On peut ensuite rechercher le document (avec un critère) pour vérifier la présence des champs supplémentaires de géolocalisation :
GET /filebeat-8.8.2/_search
Note : "filebeat-8.8.2 correspond au nom du datastream de destination
{
"query": {
"match": {
"message": "test1"
}
}
} - Dans le fichier de configuration de Filebeat sur notre serveur Web, ajouter dans la rubrique output.elasticsearch, le nom de l'ingest pipeline à utiliser :
output.elasticsearch:
Note : on peut également spécifier un "default_pipeline" dans le modèle d'index du datastream, mais on ne constatera aucun effet tant qu'un nouvel index du datastream n'aura pas été créé. Pour constater un effet immédiat, il faut donc soit faire une requête API en spécifiant le nom du pipeline geoip en paramètre, soit spécifier le nom du pipeline dans la configuration de l'agent Beat utilisé.
hosts: ["http://192.168.30.14:9200"]
pipeline: "geoip_full" - Pour pouvoir déboguer plus facilement ce qu'envoie effectivement Filebeat vers Elasticsearch et les éventuels problèmes associés, on peut lancer Filebeat en mode debug plutôt qu'avec systemd :
# filebeat -e -d "*"
Filebeat affichera alors en temps réel toutes les opérations qu'il effectue. - Pour pousser un peu plus loin les possibilités de débogage, on peut placer un proxy entre Filebeat et Elasticsearch afin de capturer les requêtes telles qu'elles sont réellement envoyées. Pour cela, installer et lancer le proxy mitmproxy, et modifier Filebeat pour spécifier un proxy d'envoi :
# vim /etc/filebeat/filebeat.yml
Modifier :output.elasticsearch:
Relancer Filebeat :
hosts: ["http://192.168.30.14:9200"]
proxy_url: "http://127.0.0.1:8888"
pipeline: "geoip_full"# systemctl restart filebeat
Installer et lancer mitmproxy :
# systemctl status filebeat# dnf install mitmproxy
=> les requêtes envoyées devraient apparaitre dans mitmproxy avant d'être transmises à Elasticsearch :
# dnf install python3-pip
# dnf install libffi-devel redhat-rpm-config gcc openssl-devel python3-devel
# mitmproxy --mode regular --listen-port 8888 - Vérifier dans Kibana la réception des nouveaux documents issus de ModSecurity et incluant désormais les informations de géolocalisation. Il sera alors possible de créer un dashboard Grafana ou Kibana avec une map monde montrant la source des requêtes dangereuses.
Liens
https://docs.rockylinux.org/fr/guides/web/apache_hardened_webserver/modsecurity/
https://tecadmin.net/install-modsecurity-with-apache-on-centos-rhel/#google_vignette
! https://www.feistyduck.com/library/modsecurity-handbook-free/online/ch04-logging.html
! https://dissect-tester.jorgelbg.me/
https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/geoip-processor.html
https://fr.matomo.org/faq/how-to/how-do-i-get-a-license-key-for-the-maxmind-geolocation-database/