ModSecurity - Configuration de base
Alasta 30 Mai 2014 security Apache Linux ModSecurity Open Source Reverse Proxy Security
Description : Nous allons voir une installation de base de ModSecurity, un WAF OpenSource pour protéger une application.
Architecture cible :
IP du WAF : 192.168.5.40 IP de l'application : 192.168.5.41 Les différentes machines sont en Centos 6.x.
Installation :
- Installation d'Apache comme serveur web/reverse proxy
yum install httpd
- Installation de ModSecurity
yum install mod_security
- Récupération des Core Rules Set
cd /etc/httpd wget https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/master.zip unzip master.zip #==> la décompression va créer le dossier "owasp-modsecurity-crs-master"
Configuration de base :
- Configuration d'Apache en mode reverse proxy pour l'application
<VirtualHost *:80> ServerName alasta.lab ServerAlias www.alasta.lab #Pour les pages qui sont en locales ProxyPass /waf_pages ! #Application a proteger - mode reverse proxy ProxyPass / http://192.168.5.41/ ProxyPassReverse / http://192.168.5.41/ #Pages d erreurs ErrorDocument 403 /waf_pages/errors/err_page_403.php </VirtualHost>
strong>- Redémarrage d'Apache pour prendre en compte les changements :
service https restart
- Test de bon fonctionnement du mode reverse proxy d'Apache via un navigateur :
Si cela ne fonctionne pas, vérifier que les modules proxy sont bien chargés :
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so
- Configuration de la page d'erreur 403 renvoyée lors de blocages ModSecurity :
<html> <head> <meta charset="utf-8"> <title>Error 403</title> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-responsive.min.css" rel="stylesheet"> </head> <body> <!--login modal--> <div id="loginModal" class="modal show" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h1 class="text-center">Status Code HTTP 403</h1> </div> <div class="modal-body"> <div class="form-group"> <h3>Your request :</h3> <?php echo $_SERVER["SERVER_NAME"]; echo $_SERVER["REQUEST_URI"]; ?> </div> <div class="form-group"> <h3>Your post data were :</h3> <?php foreach ($_POST as $key => $v) print( "<h3>".htmlspecialchars($key)."</h3><br /><textarea cols='100' rows='5' name='input'>".htmlspecialchars($v)."</textarea>" ); ?> </div> </form> </div> <div class="modal-footer"> <div class="col-md-12 text-center"> Debug ID : <div class="alert alert-danger"><?php echo $_SERVER["UNIQUE_ID"]?></div> </div> </div> </div> </div> </div> <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script> </body> </html>
PS : là j'ai fait une page custom avec le framework bootstrap avec des informations sur la requêtes mais en production il faut juste afficher un message de blocage. A la fin de la popup il y a un message contenant un ID unique qui permet d'identifier le blocage dans les logs.
- Préparation pour la configuration de ModSecurity :
cd /etc/httpd/modsecurity.d/activated_rules ln -s ../../owasp-modsecurity-crs-2.2.8/activated_rules/modsecurity_crs_10_setup.conf modsecurity_crs_10_setup.conf ln -s ../../owasp-modsecurity-crs-2.2.8/experimental_rules/modsecurity_crs_11_slow_dos_protection.conf modsecurity_crs_11_slow_dos_protection.conf cd /etc/httpd/modsecurity.d ln -s ../owasp-modsecurity-crs-2.2.8/base_rules base_rules
Chaque lien symbolique en .conf sera chargé par modsecurity.conf.
Nous aurons une configuration basique avec une protection contre le slow loris DDOS.
- Configuration basique de ModSecurity :
LoadModule security2_module modules/mod_security2.so <IfModule !mod_unique_id.c> LoadModule unique_id_module modules/mod_unique_id.so </IfModule> <IfModule mod_security2.c> # ModSecurity Core Rules Set configuration Include modsecurity.d/*.conf #=> ce qui va charger les liens symboliques effectuées précédemment Include modsecurity.d/activated_rules/*.conf Include modsecurity.d/base_rules/*.conf # Default recommended configuration SecRuleEngine On SecServerSignature "Goubygoulba" #=> ServerToken doit être a Full pour que cela fonctionne => changement du type de serveur (Apache ... par défaut) SecRequestBodyAccess On SecRule REQUEST_HEADERS:Content-Type "text/xml" \ "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" SecRequestBodyLimit 13107200 SecRequestBodyNoFilesLimit 131072 SecRequestBodyInMemoryLimit 131072 SecRequestBodyLimitAction Reject SecRule REQBODY_ERROR "!@eq 0" \ "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ "id:'200002',phase:2,t:none,log,deny,status:44,msg:'Multipart request body \ failed strict validation: \ PE %{REQBODY_PROCESSOR_ERROR}, \ BQ %{MULTIPART_BOUNDARY_QUOTED}, \ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ DB %{MULTIPART_DATA_BEFORE}, \ DA %{MULTIPART_DATA_AFTER}, \ HF %{MULTIPART_HEADER_FOLDING}, \ LF %{MULTIPART_LF_LINE}, \ SM %{MULTIPART_MISSING_SEMICOLON}, \ IQ %{MULTIPART_INVALID_QUOTING}, \ IP %{MULTIPART_INVALID_PART}, \ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ "id:'200003',phase:2,t:none,log,deny,status:44,msg:'Multipart parser detected a possible unmatched boundary.'" SecPcreMatchLimit 1000 SecPcreMatchLimitRecursion 1000 SecRule TX:/^MSC_/ "!@streq 0" \ "id:'200004',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" SecResponseBodyAccess Off SecDebugLog /var/log/httpd/modsec_debug.log SecDebugLogLevel 0 SecAuditEngine RelevantOnly SecAuditLogRelevantStatus "^(?:5|4(?!04))" SecAuditLogParts ABIJDEFHZ SecAuditLogType Serial SecAuditLog /var/log/httpd/modsec_audit.log SecArgumentSeparator & SecCookieFormat 0 SecTmpDir /var/lib/mod_security SecDataDir /var/lib/mod_security </IfModule>
- Redémarrage d'Apache pour prendre en compte les changements :
service https restart
- Vérification du SecServerSignature :
Trying 192.168.5.40... Connected to www.alasta.lab. Escape character is '^]'. GET / HTTP/1.1 # <== 2 retours chariots HTTP/1.1 400 Bad Request Date: Tue, 27 May 2014 05:19:56 GMT Server: Goubygoulba # <== notre modification du type de serveur Content-Length: 226 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>400 Bad Request</title> </head><body> <h1>Bad Request</h1> <p>Your browser sent a request that this server could not understand.<br /> </p> </body></html> Connection closed by foreign host.
- Test de bon fonctionnement de ModSecurity via un navigateur :
On voit un beau message d'erreur 403, on récupère l'unique ID U4QaI8CoBSgAAB0cA@QAAAAB qui va permettre de rechercher les informations dans le fichier de log :
cd /var/log/httpd grep -rl U4QaI8CoBSgAAB0cA@QAAAAB . ./modsec_audit.log ./error_log
[Tue May 27 06:52:51 2014] [error] [client 192.168.5.26] ModSecurity: Access denied with code 403 (phase 2). Pattern match "^[\\d.:]+$" at REQUEST_HEADERS:Host. [file "/etc/httpd/modsecurity.d/base_rules/modsecurity_crs_21_protocol_anomalies.conf"] [line "98"] [id "960017"] [rev "2"] [msg "Host header is a numeric IP address"] [data "192.168.5.40"] [severity "WARNING"] [ver "OWASP_CRS/2.2.8"] [maturity "9"] [accuracy "9"] [tag "OWASP_CRS/PROTOCOL_VIOLATION/IP_HOST"] [tag "WASCTC/WASC-21"] [tag "OWASP_TOP_10/A7"] [tag "PCI/6.5.10"] [tag "http://technet.microsoft.com/en-us/magazine/2005.01.hackerbasher.aspx"] [hostname "192.168.5.40"] [uri "/dvwa/login.php"] [unique_id "U4QaI8CoBSgAAB0cA@QAAAAB"]
On récupère plein d'information comme l'IP du client, dans quelle phase il y a eu le blocage, le message simplifié (msg), le fichier de règle qui a match avec sa ligne et son id, l'URI, un lien d'explication et un tas d'autres choses.
Ici c'est le msg qui nous guide : Host header is a numeric IP address => Il ne veut pas d'adresse IP dans le header host, il faut un FQDN. C'est vrai que l'on ne se connecte pas à un site web via une adresse IP !
--65d4c615-A-- [27/May/2014:06:52:51 +0200] U4QaI8CoBSgAAB0cA@QAAAAB 192.168.5.26 59558 192.168.5.40 80 --65d4c615-B-- GET /dvwa/login.php HTTP/1.1 Host: 192.168.5.40 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:29.0) Gecko/20100101 Firefox/29.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: security=high; PHPSESSID=fprfeh08669oes7ipookf8g314 Connection: keep-alive Cache-Control: max-age=0 --65d4c615-F-- HTTP/1.1 403 Forbidden X-Powered-By: PHP/5.3.3 Content-Length: 1283 Connection: close Content-Type: text/html; charset=UTF-8 --65d4c615-H-- Message: Access denied with code 403 (phase 2). Pattern match "^[\d.:]+$" at REQUEST_HEADERS:Host. [file "/etc/httpd/modsecurity.d/base_rules/modsecurity_crs_21_protocol_anomalies.conf"] [line "98"] [id "960017"] [rev "2"] [msg "Host header is a numeric IP address"] [data "192.168.5.40"] [severity "WARNING"] [ver "OWASP_CRS/2.2.8"] [maturity "9"] [accuracy "9"] [tag "OWASP_CRS/PROTOCOL_VIOLATION/IP_HOST"] [tag "WASCTC/WASC-21"] [tag "OWASP_TOP_10/A7"] [tag "PCI/6.5.10"] [tag "http://technet.microsoft.com/en-us/magazine/2005.01.hackerbasher.aspx"] Apache-Error: [file "/builddir/build/BUILD/php-5.3.3/sapi/apache2handler/sapi_apache2.c"] [line 326] [level 3] PHP Notice: Undefined variable: txt in /var/www/html/waf_pages/errors/err_page_403.php on line 19 Action: Intercepted (phase 2) Apache-Handler: php5-script Stopwatch: 1401166371506695 2332 (- - -) Stopwatch2: 1401166371506695 2332; combined=378, p1=224, p2=71, p3=0, p4=0, p5=82, sr=58, sw=1, l=0, gc=0 Producer: ModSecurity for Apache/2.7.3 (http://www.modsecurity.org/); OWASP_CRS/2.2.8. Server: Apache/2.2.15 (CentOS) DAV/2 PHP/5.3.3 Engine-Mode: "ENABLED" --65d4c615-Z--
Cela se présente sour la forme --UN_ID-UNE_LETTRE--,
Dans :
A : on retrouve des informations dont Unique ID
B : la requête cliente
F : la réponse du reverse proxy, on voit aussi que l'on fournit la version de PHP => à corriger
H : d'autres information
Z : fin de log pour cet unique ID
- Correction de l'affichage de la version de PHP :
HTTP/1.1 403 Forbidden Date: Tue, 27 May 2014 05:26:50 GMT Server: Goubygoulba #<== Notre ServerSignature modifié X-Powered-By: PHP/5.3.3 #<== A corriger Content-Length: 1283 Connection: close Content-Type: text/html; charset=UTF-8
Là on voit bien la version.
Pour la correction éditons le php.ini :
; Decides whether PHP may expose the fact that it is installed on the server ; (e.g. by adding its signature to the Web server header). It is no security ; threat in any way, but it makes it possible to determine whether you use PHP ; on your server or not. ; http://www.php.net/manual/en/ini.core.php#ini.expose-php ;expose_php = On expose_php = Off
Il faut passer expose_php à off et redémarrer le service https.
Re-tester le curl précédent pour valider que cela à bien fonctionné.
- Modification de configuration pour palier au problème d'IP dans le header host :
Il suffit de renseigner le DNS avec l'IP du reverse proxy qui correspond au FQDN de notre application (sur le reverse proxy).
Vu que nous sommes dans une démo, il suffira de faire l'équivalent dans le fichier host du poste de test
## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1 localhost 255.255.255.255 broadcasthost 192.168.5.40 www.alasta.lab
Le FQDN www.alasta.lab sera notre application dans la démo.
- Relançons le test via le navigateur pour voir si la modification est effective :
Nous avons un reverse proxy avec ModSecurity de base fonctionnel !