Architecture cible :

Architecture 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 :
DVWA en mode reverse proxy
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 :
DVWA_modSec_deny_IP
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 :
DVWA_modSec_OK
Nous avons un reverse proxy avec ModSecurity de base fonctionnel !