NAXSI, un WAF open source pour Nginx

GNU/Linux Magazine n° 152 | septembre 2012 | Thibault Koechlin
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
La sécurité web, on peut en rire, ou en pleurer, mais il semble difficile de ne pas s'en soucier au risque de s'en mordre les doigts. La réalité du niveau de sécurité des applicatifs web étant ce qu'elle est, les pare-feu applicatifs deviennent des palliatifs indispensables.

1. WAF, Pare-feu applicatif, kézako ?

Un WAF, Web Application Firewall (pare-feu applicatif web), est une brique logicielle, généralement placée en amont d'un applicatif web, et qui a pour but d'analyser les requêtes HTTP arrivant à ce dernier. L'objectif est de détecter/prévenir l'exploitation d'éventuelles vulnérabilités présentes dans ledit applicatif, telles que celles décrites par le top 10 OWASP1, à savoir les XSS, SQLi, file upload, etc.

Ce logiciel peut prendre de multiples formes, comme :

- placé en amont de l'applicatif, sous forme d'un reverse proxy (par exemple dans le cas d'appliances) ;

- sous forme d'un composant du serveur HTTP lui-même (par exemple mod_security pour Apache) ;

- intégré directement au code de l'applicatif (OWASP ESAPI, PHP IDS).

La très grande majorité des WAF fonctionnent à la manière des antivirus, en s'appuyant sur une base de signatures, censés représenter les différents motifs d'attaques possibles.

Cette philosophie impose un certain nombre de limites :

- impossibilité de bloquer les attaques inconnues (non présente dans les signatures) ;

- risque de contournements via des attaques obfusquées ;

- besoin de mise à jour régulier des bases de signatures ;

- les performances peuvent être fortement impactées par le nombre de signatures.

Un avantage certain, à l'inverse, est que cette approche génère un faible nombre de faux positifs.

Les pare-feu applicatifs sont aussi parfois utilisés pour réaliser du « virtual patching ». À l'inverse d'un filtrage global, il s'agit ici de protéger l'applicatif uniquement contre certaines vulnérabilités connues, en appliquant une liste blanche sur la partie spécifique du code posant problème.

Une approche par liste blanche, moins répandue, est aussi possible. Cette approche remonte parfoirs plus de faux positifs, principale raison de son faible taux d'adoption. Mais en contrepartie, elle bénéficie de performances accrues et d'une sécurité renforcée car non dépendante d'une base de signatures.

C'est dans cette optique que j'ai conçu NAXSI.

2. NAXSI : une approche différente

N.A.X.S.I signifie Nginx Anti Xss & SQL Injection.

La majorité des pare-feu appliquant un « modèle négatif » (exclusion des mauvaises requêtes) reposent sur une base d’expressions régulières généralement appelées signatures. Ces expressions régulières (ou regex) sont ensuite recherchées dans le contenu de la requête HTTP afin de détecter une éventuelle attaque et de bloquer la requête.

Le reproche récurrent de ce type de fonctionnement est que ces expressions régulières sont souvent complexes et très nombreuses (souvent plusieurs centaines), ce qui peut entraîner un ralentissement de l’application. De plus, elles ont besoin d’être mises à jour régulièrement pour être efficaces, à la manière d'un antivirus.

Un autre problème, conceptuel, de cette approche est qu'il est nécessaire de « limiter » le champ de recherche de ces fameuses expressions régulières, ou d’appliquer des transformations avant de les effectuer. Dans le cas contraire, les performances seraient encore plus dégradées. Ces modifications entraînent l'apparition de risques de contournement liés aux attaques obfusquées. Ce sujet fut très bien illustré lors du « challenge mod_security » lancé en 20112.

L’objectif de NAXSI étant de s'affranchir de ces limites (performances & pérennité de la sécurité), il a fallu abandonner le système de signature « classique », pour s’approcher d’un mécanisme par liste blanche.

NAXSI ne recherche donc pas de signatures complexes d’attaques, mais se concentre sur des primitives peu nombreuses (moins de 40), souvent réduites à des motifs d’un caractère ou deux, nécessaires à la majorité des attaques. Cette approche permet notamment de limiter le temps de traitement nécessaire, ainsi que de le garder – dans une certaine mesure – prévisible.

Le prix de ce mode opératoire qui limite le nombre de signatures, plus rapide et plus efficace contre les risques d’attaques offusquées/complexes, est une utilisation plus importante des listes blanches (ou exception, selon la terminologie).

Pour pallier ce souci, NAXSI dispose d’un mode d’apprentissage, qui est au centre de son fonctionnement.

Un autre bénéfice de NAXSI est fourni par le fait qu'il repose sur Nginx. Grâce à cette architecture, NAXSI peut être utilisé aussi bien en tant que reverse proxy, qu'en tant de module sur le serveur web lui-même, sans pour autant impacter de manière significative les performances du serveur HTTP (moins d'1% de ressources).

Pour l'architecture logicielle interne de NAXSI, le choix a été fait de plusieurs composants indépendants et remplaçables qui vont être décrits dans les chapitres suivants.

2.1. Le core

Ce module Nginx (développé en C) est le seul à « agir » en runtime. Il inspecte les requêtes HTTP en fonction des règles spécifiées dans la configuration Nginx, généralement /etc/nginx/naxsi_core.rules3.

Ce fichier est fourni avec le logiciel et n'a pas vocation a être édité par l'utilisateur. Il contient une quarantaine de règles comme :

MainRule "str:<" "msg:html open tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1302

Les règles de NAXSI prennent toujours la même forme :

- Un pattern de recherche (ici le caractère <). Il peut être spécifié sous la forme d'une string ou d'une regex (expression régulière). Les strings sont préférables, car moins couteuses en termes de traitement.

- Un messagedescriptif, associé à la signature, ici html open tag.

- Les zonesdans lesquelles le motif doit être recherché : ARGS (les arguments GET), URL (l'URL), BODY (les arguments POST/PUT), $HEADERS_VAR:Cookie (le champ Cookie du header HTTP).

- Un scorede catégorie, généralement associé à une menace, ici on dit que la règle tombe dans la catégorie de XSS (Cross Site Scripting).

- Et finalement, un ID unique, qui sera notamment utilisé pour les listes blanches.

Ainsi, si le caractère incriminé est trouvé, le score XSS de la requête sera augmenté de 8. L'utilisateur peut ensuite définir, par location4, des scores limites pour chaque type de menace, ainsi que définir des listes blanches et des actions à appliquer aux requêtes bloquées. (Nous en reparlerons par la suite).

La configuration par défaut pour un emplacement se présente comme suit :

#LearningMode;

SecRulesEnabled;

DeniedUrl "/RequestDenied";

## check rules

CheckRule "$SQL >= 8" BLOCK;

CheckRule "$RFI >= 8" BLOCK;

CheckRule "$TRAVERSAL >= 4" BLOCK;

CheckRule "$EVADE >= 4" BLOCK;

CheckRule "$XSS >= 8" BLOCK;

La première directive LearningMode est très importante, puisqu'elle détermine si NAXSI sera en mode apprentissage ou non. Lorsque NAXSI est en mode apprentissage, il ne bloquera aucune requête, et si la requête devait être bloquée, il se contenterait de « copier » la requête vers l'emplacement défini par la directive DeniedUrl, ainsi que de loguer dans les fichiers de logs de Nginx la signature de la requête.

La directive SecRulesEnabled permet d'activer/désactiver simplement NAXSI. Si elle est commentée, NAXSI est complètement désactivé pour cette location.

DeniedUrl : Cette directive permet de déterminer où NAXSI devra « envoyer » les requêtes bloquées. En effet, lorsqu'une requête est bloquée par NAXSI, il va la rediriger vers l'emplacement défini par la configuration de l'utilisateur. Cette location sera aussi utilisée pendant le mode apprentissage, puisque NAXSI va y « copier » la requête qui aurait dû être bloquée, afin qu'elle puisse être analysée à la volée.

CheckRule : Ces règles permettent de définir les scores limites pour chaque type de menace, avant qu'une requête soit bloquée. Ici, l'action BLOCK est utilisée, mais l'utilisateur peut aussi utiliser la directive LOG pour simplement loguer la signature de la requête.

Pour résumer, le core va lire (parser) les requêtes HTTP, comparer leur contenu avec les signatures NAXSI, puis, en fonction de la configuration faite par l'utilisateur et du/des scores de la requête, éventuellement rediriger la requête vers une location tierce afin de protéger l'applicatif.

2.2. Le démon d’interception « nx_intercept »

Le démon d'interception (script minimaliste en python, basé sur twisted) est en charge de stocker les exceptions. Une exception correspond au déclenchement d'une ou plusieurs règles.

Cette acquisition peut être faite depuis les fichiers de logs de Nginx (puisque NAXSI enregistre aussi les signatures d'attaques dans les error_log de Nginx), ou depuis les requêtes dupliquées reçues lorsque NAXSI est en mode apprentissage.

Les données sont ensuite stockées dans une base de données (mysql ou sqlite3).

2.3. Le démon d'extraction nx_extract

Ce démon est lui aussi en python (et lui aussi basé sur twisted). En s'appuyant sur la base de données alimentée par le démon d'interception (à partir des exceptions interceptées), il est en charge de générer les listes blanches.

Il est aussi utilisé pour générer des statistiques sur les interceptions réalisées. À la différence de nx_intercept, il a pour vocation d'être utilisé (lancé) par l'utilisateur.

3. NAXSI en pratique

3.1. Installation de Nginx+NAXSI

NAXSI est intégré dans les dépôts officiels de Debian (merci davromaniak), FreeBSD (merci sbz), ou encore NetBSD (merci iMil). Il est aussi disponible pour CentOS/Redhat (via les dépôts de axivo5, merci Floren), et packagé par dotdeb6.

Si vous voulez l'installer depuis les sources, vous pouvez vous référer au wiki du projet7.

Sous Debian, NAXSI est composé de deux paquets :

nginx-naxsi : Nginx avec le module NAXSI (le core), ainsi que les deux fichiers de configuration évoqués plus haut :

- /etc/nginx/naxsi_core.rules : le core rule set de NAXSI ;

- /etc/nginx/naxsi.rules : un exemple de configuration NAXSI pour un emplacement défini.

nginx-naxsi-ui : ce paquet contient les deux démons du mode d'apprentissage, nx_intercept et nx_extract.

Cependant, comme l'interface web de NAXSI est une partie du projet qui évolue très vite et que debian est en package freeze, nous allons préférer une installation manuelle pour la partie interface :

$ svn checkout http://naxsi.googlecode.com/svn/trunk/contrib/naxsi-ui naxsi-ui

(On peut aussi prendre la dernière archive release sur l'espace de téléchargement du googlecode).

Pour fonctionner correctement, les deux démons du mode d'apprentissage requièrent l'installation de twisted (python-twisted) ainsi que les bindings MySQL pour python (python-mysqldb).

3.2. Configuration de Nginx+NAXSI

Pour commencer nos tests, nous allons placer Nginx+NAXSI en tant que reverse proxy, ce qui nous permettra de tester et configurer NAXSI sans impacter les utilisateurs légitimes. Ceci peut être réalisé grâce à une modification du /etc/hosts afin de faire pointer www.monsite.comvers l'adresse IP du couple Nginx+NAXSI (ici 127.0.0.1).

Pour activer NAXSI, il faut tout d'abord inclure les core rules au serveur HTTP (cette directive est commentée par défaut).

/etc/nginx/nginx.conf :

http {

...

Ensuite, on configure Nginx pour agir en tant que reverse proxy vers un site précis.

/etc/nginx/sites-enabled/default :

server {

 proxy_set_header Proxy-Connection "";

listen *:80;

access_log /var/log/nginx_access.log;

error_log /var/log/nginx_error.log;

location / {

  include /etc/nginx/naxsi.rules;

  proxy_pass http://x.x.x.x;

  proxy_set_header Host www.monsite.com;

  }

 location /RequestDenied {

  proxy_pass http://127.0.0.1:8080;

  }

}

Et enfin, dans notre /etc/nginx/naxsi.rules, la configuration de NAXSI :

LearningMode;

SecRulesEnabled;

DeniedUrl "/RequestDenied";

## check rules

CheckRule "$SQL >= 8" BLOCK;

CheckRule "$RFI >= 8" BLOCK;

CheckRule "$TRAVERSAL >= 4" BLOCK;

CheckRule "$EVADE >= 4" BLOCK;

CheckRule "$XSS >= 8" BLOCK;

3.3. Configuration du démon d'interception : nx_intercept

Le démon d'interception ne doit pas nécessairement se trouver sur la même machine que NAXSI, cependant ce sera le choix retenu ici, pour plus de simplicité. Comme précisé, les démons supportent les bases de données de type SQLite ou MySQL.

Pour cette première prise en main, nous allons opter – par simplicité - pour SQLite. Il faut cependant savoir que les performances de SQLite dans un contexte d'accès concurrents sont beaucoup moins bonnes. On préférera donc MySQL si l'on fait de l'apprentissage « en live ».

La configuration des deux démons d'apprentissage est faite via le même fichier de configuration. Vous trouverez plusieurs exemples dans le répertoire contrib/naxsi-ui si vous avez téléchargé le tarball ou depuis le SVN :

[nx_extract]

username = naxsi_web

password = test

port = 8081

rules_path = /etc/nginx/naxsi_core.rules

[nx_intercept]

port = 8080

[sql]

dbtype = sqlite

username = root

password =

hostname = 127.0.0.1

dbname = naxsi_sig

Le démon d'interception peut être utilisé de deux manières :

- En « runtime » : lorsque NAXSI est en mode apprentissage, et que des requêtes auraient dû être bloquées, elles seront copiées vers la location définie par la directive RequestDenied. Cette location, dans notre cas, contient un proxy_pass pointant vers notre démon d'interception. (Ici, en écoute sur le port 8080 du loopback).

- En « offline » : quel que soit son mode de fonctionnement, NAXSI enregistrera dans les journaux d'erreur de Nginx les signatures des requêtes bloquées. Ces signatures sont suffisantes pour générer les listes blanches. Dans un tel cas d'utilisation, nx_intercept ne sera pas en écoute sur le réseau, mais lancé avec un fichier de log en paramètre.

3.4. Modification du /etc/hosts

L'objectif étant ici de ne pas impacter la production, nous avons placé NAXSI en reverse proxy sur notre machine locale. Il ne nous reste plus qu’a leurrer notre machine de test. Modifions le /etc/hosts pour ajouter :

127.0.0.1 www.monsite.com

Ainsi, lorsque l'on tentera de se connecter sur www.monsite.com, on arrivera en réalité sur notre Nginx local (qui écoute lui aussi sur le port 80), qui fera le filtrage, et passera les requêtes à notre « vrai » site.

3.5. L'épreuve du feu

Le moment où, en général, rien ne marche comme prévu ;)

On va tout d'abord lancer Nginx + NAXSI :

# /etc/init.d/nginx start

Si l'on se connecte (avec un navigateur) sur http://127.0.0.1, on devrait bien voir notre site www.monsite.com.

On va ensuite lancer le démon d'interception, en lui donnant en paramètre le fichier de configuration décrit plus haut :

$ python nx_intercept.py -c naxsi-ui-learning.conf

Si l'on se connecte de nouveau sur http://127.0.0.1, on verra toujours notre site web et si l'on navigue un peu, on verra la sortie suivante affichée par nx_intercept :

[+] drop and creating new tables

+ /

+ /foobar

Ces lignes signifient que nx_intercept a reçu des exceptions générées par NAXSI.

Nous allons donc maintenant pouvoir lancer le démon d'extraction (nx_extract) pour récupérer nos listes blanches et en savoir plus sur la cause des exceptions.

3.6. Récupération des listes blanches

Nous allons lancer nx_extract avec le même fichier de configuration que nx_intercept. Ainsi, l'extraction sera faite depuis les données interceptées par nx_intercept.

$ python nx_extract.py

On peut ensuite accéder à 127.0.0.1:8081 avec notre navigateur, et on arrivera sur l'interface web de NAXSI.

On se rend directement dans la partie « Generate Whitelist ». Dans mon cas, le site pour lequel je configure NAXSI est basé sur WordPress, avec notamment Google Analytics. Comme vous allez vite le constater, NAXSI est très restrictif par défaut. La présence de Google Analytics (qui va insérer des caractères non usuels dans le cookie) suffira à générer des exceptions.

En se rendant sur la page d'exception (j'ai simplement chargé la page d'accueil 5 fois dans mon cas), on verra des lignes comme ceci s'afficher :

# total_count:5 (8.77%), peer_count:1 (100.0%) | double encoding !

BasicRule wl:1315 "mz:$URL:/foobar|$HEADERS_VAR:cookie";

Le commentaire placé au-dessus de la ligne est là pour vous assister dans la compréhension.

On apprend ici que la règle 1315 (c'est son ID, rappelez-vous du naxsi_core.rules), qui correspond en fait à du double encodage (tel qu'indiqué à la fin du commentaire), a été identifiée dans le header HTTP Cookie lors d'un accès à la page /foobar.

On apprend aussi, et c'est le plus important, que cette exception, survenue 5 fois, représente 8,77 % du total des exceptions interceptées. De même, une seule IP source (peer) l'a déclenché, ce qui représente 100 % du total des peers ayant déclenché des exceptions (puisque je suis seul à faire mes tests).

Cette ligne est une liste blanche qui peut être incluse directement dans notre configuration NAXSI (naxsi.rules), afin que NAXSI, la prochaine fois qu'il voit cette exception, l'ignore purement et simplement.

Avec un apprentissage minimaliste, vous verrez un grand nombre de listes blanches générées. Cependant, un grand nombre d'exceptions peuvent être factorisées. Dans ce cas précis (un élément dans le cookie), il est quasiment sûr que l'on retrouvera cette exception sur chaque page, puisque le cookie est persistant.

On peut donc attendre de la part de nx_intercept qu'il fasse ce travail pour nous. Dans ce cas précis, comme le nombre d'exceptions générées est trop faible, NAXSI ne fera pas de lui-même la factorisation, mais on peut l'y forcer. Comme décrit dans la documentation, il existe un paramètre rules_hit à la page de génération des listes blanches, qui est par défaut à « 10 ».

J'obtiens, dans mon cas, 41 listes blanches générées, l'écrasante majorité correspondant à la présence de caractères inhabituels dans les cookies.

Si je force ce paramètre rules_hit à 5 (http://127.0.0.1:8081/get_rules?rules_hit=5), j'obtiendrais la sortie suivante :

########### Optimized Rules Suggestion ##################

# total_count:14 (24.56%), peer_count:1 (100.0%) | double encoding !

BasicRule wl:1315 "mz:$HEADERS_VAR:cookie";

# total_count:14 (24.56%), peer_count:1 (100.0%) | parenthesis, probable sql/xss

BasicRule wl:1011 "mz:$HEADERS_VAR:cookie";

# total_count:14 (24.56%), peer_count:1 (100.0%) | parenthesis, probable sql/xss

BasicRule wl:1010 "mz:$HEADERS_VAR:cookie";

# total_count:14 (24.56%), peer_count:1 (100.0%) | mysql keyword (|)

BasicRule wl:1005 "mz:$HEADERS_VAR:cookie";

# total_count:1 (1.75%), peer_count:1 (100.0%) | sql keywords

BasicRule wl:1000 "mz:$URL:/wp-content/plugins/sitepress-multilingual-cms/res/css/language-selector.css|URL";

On a ici forcé NAXSI à extrapoler les règles, afin qu'il réalise la factorisation. Comme on l'attendait, il a généralisé les règles afin qu'elles s'appliquent sur le cookie, indépendamment de l'URL.

En effet, la syntaxe des liste blanches de NAXSI est telle que, moins on précise d'éléments, moins elle est restrictive. Si aucune URL n'est précisée dans la liste blanche, elle s'applique à toutes les URL, etc.

Dans notre cas, nous pouvons prendre ces listes blanches, les inclure dans notre /etc/nginx/naxsi.rules, et reloader Nginx.

3.7. Vérifier la bonne application des listes blanches

Votre fichier de configuration pour NAXSI doit maintenant contenir les listes blanches que vous avez générées. Il s'agit de vérifier que celles-ci sont correctement appliquées. Le moyen le plus simple pour ce faire est de supprimer (ou changer) votre base SQLite, de laisser activé le mode d'apprentissage, et de relancer nx_intercept et Nginx.

Vous pouvez ensuite recommencer votre session de navigation. Si vous voyez des exceptions similaires apparaître dans la sortie de nx_intercept ou lorsque vous relancez nx_extract, cela signifie que vos listes blanches ne sont pas appliquées. (Dans ce cas, il faut que vous vérifiez votre configuration).

3.8. Désactiver le learning mode

Une fois que vos sessions de navigation ne déclenchent plus d'exceptions, vous pouvez désactiver le learning mode de NAXSI. Une fois ce mode désactivé, les requêtes contenant des caractères étranges et ne possédant pas de liste blanches associées seront bloquées. Quand on dit ici bloquées, on veut en fait dire qu'elles seront redirigées vers l'emplacement spécifié par la directive RequestDenied.

Il faut donc modifier notre configuration dans cet emplacement, afin qu'elle ne redirige plus vers nx_intercept, mais qu'elle renvoie une page d'erreur à l'utilisateur :

location /RequestDenied {

return 500;

}

sans oublier de commenter la ligne LearningMode; dans votre naxsi.rules.

Après avoir appliqué les modifications et reloadé Nginx, si vous demandez par exemple une page comme http://127.0.0.1/<>, vous recevrez une page d'erreur 500. En revanche, vous devriez pouvoir réaliser votre navigation normale sans problème, votre site est protégé !

3.9. Les alternatives

Si vous avez du temps et/ou du trafic sur votre site, vous pouvez vous dispenser de réaliser l'apprentissage vous-même. Comme précisé plus haut, nx_intercept peut être « nourri » depuis les fichiers de log de Nginx (qui contiennent les signatures de NAXSI).

Vous pouvez donc tout simplement activer le learning mode, ne pas mettre en place de proxy_pass dans le /RequestDenied (mais un simple return), et laisser vos utilisateurs continuer à utiliser le site comme si de rien n'était.

Comme nous sommes en mode apprentissage, aucune requête ne sera bloquée (donc pas d'impact sur l'expérience utilisateur), et NAXSI étant très rapide, le temps de traitement des requêtes sera invisible pour les utilisateurs. (Si vous constatez une augmentation supérieure à 1 ms, merci de me contacter).

Au bout de quelques heures ou jours, vous pouvez injecter les error_log de Nginx dans nx_intercept. Par exemple, j'ai un redmine qui tourne, avec 8 utilisateurs actifs. J'ai donc configuré Nginx+NAXSI en learning mode, sans utiliser de nx_intercept. Au bout de 24h, j'injecte le error_log de Nginx dans nx_intercept (en prenant soin d'avoir une nouvelle base de données pour ne pas tout mélanger).

$ python nx_intercept.py -c naxsi-ui-learning.conf -l /var/log/Nginx/redmine_error.log

Je peux ensuite lancer nx_extract et récupérer mes whitelists de la même manière.

3.10. Statistiques et reporting

C'est bien de bloquer des attaques, c'est encore mieux de savoir ce qu'il se passe vraiment !

Le reporting prend tout son sens une fois que vous avez fini l'apprentissage. Il vous permettra de surveiller et de voir évoluer les tentatives d'attaques.

L'interface permet par exemple de voir la répartition des types d'attaques :

ou encore de voir leur évolution en termes de volume dans le temps :

La méthodologie recommandée consiste à injecter, dans une base fraîche, un ou plusieurs fichiers de error_log de Nginx (avec ou sans learning mode), correspondant à la période que vous voulez grapher.

3.11. Le mot de la fin

Cet article est une simple introduction à NAXSI. En effet, NAXSI, en tant que module de Nginx, profite de la souplesse de ce dernier, et offre donc de nombreuses possibilités d'utilisation. Dans le second article sur NAXSI, nous aborderons notamment les problématiques suivantes :

- learning passif ;

- virtual patching et whitelist spécifiques ;

- utilisation de NAXSI en tant que NIDS ;

- utilisation de NAXSI avec fail2ban ;

- gestion multi-sites ;

- et une multitude d'autres tricks.

Je tiens tout particulièrement à remercier NBS System, dont le support a été crucial à la réussite de ce projet.

NAXSI soufflera sa première bougie le 22 août 2012, et j'en profite donc pour faire un appel à contributeurs :(pen)testeurs, développeurs, feedback, etc.

Tags : firewall, Naxsi, Nginx, WAF