Sécuriser son CMS : l’exemple WordPress

Le 20 Avril 2012, le site eSecurity Planet publiait un article intitulé « Top 5 WordPress Vulnerabilities and How to Fix Them » sur quelques faiblesses de la sécurité de WordPress et sur des solutions pour y remédier.

Ces conseils peuvent s’appliquer à de nombreux autres CMS (Joomla, CMS Made Simple, SPIP, etc).

En simplifié, un CMS pour Content Management System, ou gestionnaire de contenu en français, est un système offrant une interface permettant d’éditer rapidement et simplement le contenu de sites internets sans ce soucier de la mise en forme ou de la manière de stocker les informations (action souvent déléguée à une base de donnée). Parmi les plus célèbres, citons Drupal, Joomla, CMS Made Simple, SPIP, WordPress (plus accès blogging), etc.

Cet article revient sur ces éléments, vous propose de mettre en place les protections appropriées sur un site déjà en production et ajoute quelques conseils supplémentaires. Les scripts sont donnés pour linux, mais la démarche est la même quelque soit le système d’exploitation utilisé.

Dans cet article, nous partirons du principe que le site est hébergé sur un serveur Apache qui correspond à la plupart des hébergements mutualisés ainsi qu’aux hébergements personnels. Les utilisateurs d’Ngnix ou de lighthttpd devraient faire les adaptations sans trop de difficultés.

Protection contre les injections SQL/Hack d’URL

Le premier conseil offert par « eSecurity Planet » consiste à renforcer le contrôle des urls passées au site afin d’éviter les attaques par injection SQL ou par modifications d’URL. Pour cela, rien de bien complexe, ajoutez à la racine du site dans un fichier « .htaccess » (ou directement dans la définition du vhost du site) les règles proposées dans l’article « d’eSecurity Planet » et rappelées ci-dessous :

<IfModule mod_rewrite.c>
   RewriteEngine On
   RewriteBase /
   RewriteCond %{REQUEST_METHOD} ^(HEAD|TRACE|DELETE|TRACK) [NC]
   RewriteRule ^(.*)$ - [F,L]
   RewriteCond %{QUERY_STRING} \.\.\/ [NC,OR]
   RewriteCond %{QUERY_STRING} boot\.ini [NC,OR]
   RewriteCond %{QUERY_STRING} tag\= [NC,OR]
   RewriteCond %{QUERY_STRING} ftp\:  [NC,OR]
   RewriteCond %{QUERY_STRING} http\:  [NC,OR]
   RewriteCond %{QUERY_STRING} https\:  [NC,OR]
   RewriteCond %{QUERY_STRING} (\<|%3C).*script.*(\>|%3E) [NC,OR]
   RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|%3D [NC,OR]
   RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(\[|\]|\(|\)|<|>|ê|"|;|\?|\*|=$).* [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(&#x22;|&#x27;|&#x3C;|&#x3E;|&#x5C;|&#x7B;|&#x7C;).* [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(%24&x).* [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(%0|%A|%B|%C|%D|%E|%F|127\.0).* [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(globals|encode|localhost|loopback).* [NC,OR]
   RewriteCond %{QUERY_STRING} ^.*(request|select|insert|union|declare).* [NC]
   RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in_.*$
   RewriteRule ^(.*)$ - [F,L]
</IfModule>

En quelques mots (et simplifié), cette protection est basée sur le module « mod_rewrite » d’Apache. Elle consiste à rechercher tout caractère « curieux » ou « suspect » dans l’URL appelée ou dans les cookies transmis et à rediriger le visiteur sans en tenir compte. Un caractère suspect ou curieux est un caractère ou une chaîne de caractères couramment utilisé pour injecter du code dans un site ou une base de donnée (ie « <script> » n’a rien à faire dans une URL si ce n’est pour tenter d’injecter un script dans une page, etc).

Pour plus d’information, reportez vous à la page de manuel de « mod_rewrite » http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html

Adaptation des droits sur les fichiers

Cette partie est plus particulièrement destinée aux administrateurs de serveurs web.

Lors du déploiement d’un CMS sur un système Linux, les droits sont bien souvent très (trop) permissifs. Le premier travail de l’administrateur doit consister à adapter les droits UNIX afin, entre autre, de restreindre la possibilité de déposer sur le serveur des fichiers pouvant être exécutés par un attaquant.

Il convient de détecter dans un premier temps le(s) répertoire(s) dans le(s)quel(s) le serveur apache sera autorisé à écrire. Dans le cas de WordPress, seul le répertoire « /wp-content/uploads/ » nécessite les droits d’écriture pour le serveur web.

Les fichiers du site n’ont pas à appartenir au serveur apache ! En revanche, pour que le serveur puisse afficher les pages, elles appartiendront au groupe d’apache qui ne disposera sur elles que d’un droit de lecture. La création d’un utilisateur « webmaster », aux droits restreint, à qui « seront donnés » les sites achèvera de sécuriser l’ensemble.

Sur une installation fraîche tout comme sur un site en production, nous pourrons restreindre les autorisations comme suit :

webserver monsite # id webmaster # Créer l'utilisateur en cas de besoin 
uid=10000(webmaster) gid=65534(nogroup) groups=65534(nogroup)
webserver monsite # pwd
/websites/monsite
webserver monsite # ls
wordpress
webserver monsite # # les fichiers du site sont donnés au webmaster et au groupe d'apache :
webserver monsite # chown -R webmaster.www-data wordpress
webserver monsite # # Puis adaptation des droits :
webserver monsite # find wordpress -type d -exec chmod 2750 {} \;
webserver monsite # find wordpress -type f -exec chmod 640 {} \;
webserver monsite # # Attention, apache doit écrire dans uploads :
webserver monsite # chmod -R g+w wordpress/wp-content/uploads

Si le répertoire uploads n’existe pas, le créer avant d’y fixer les droits.

« www-data » Est le groupe auquel appartient apache et sous lequel il est lancé.

Notez l’activation du sticky bit sur les répertoires de l’arborescence (les fichiers et répertoires créés dans la structure, héritent tous du groupe d’appartenance du répertoire parent, ici www-data).

Protection de l’accès aux fichiers sensibles

Un certain nombre de fichier de l’arborescence des CMS ont un contenu plus ou moins sensible et doivent être rendu inaccessibles aux visiteurs (exemple : fichier de configuration, README donnant des informations sur la version du logiciel utilisé, etc).

De plus, les répertoires ne contenant pas de « première page » (fichiers index.html, default.html, index.php, etc) ne doivent pas être accessibles et consultables par vos visiteurs sous peine de donner des indications sur votre structure à d’éventuels attaquants. Cette mesure est très simple à mettre en place et se résume par l’ajout de « Options -Indexes » dans votre « .htaccess » ou dans la définition du vhost de votre site.

Enfin, il est préférable de désactiver PHP dans chaque répertoire ou il n’est pas utilisé. C’est d’autant plus important si apache peut écrire dans ces répertoires (exemple : le répertoire « uploads »).
L’explication est simple : si un attaquant dépose une page PHP sur votre site, inutile qu’il puisse l’exécuter en l’appelant directement.
Cette mesure est mise en place simplement par l’ajout de « php_flag engine Off » dans un fichier « .htaccess » posé à la racine du répertoire concerné (ou directement via la définition du vhost du site).

Au final, toutes ces protections peuvent être mise en place via un fichier « .htaccess » du type :

# Pas d'affichage du contenu des répertoire si pas de première page
Options -Indexes
# Interdiction d'accéder directement à certains fichiers :
<files .htaccess>
   Order allow,deny
   Deny from all
</files>
<files readme.html>
   Order allow,deny
   Deny from all
</files>
<files license.txt>
   Order allow,deny
   Deny from all
</files>
<files install.php>
   Order allow,deny
   Deny from all
</files>
<files wp-config.php>
   Order allow,deny
   Deny from all
</files>
<files error_log>
   Order allow,deny
   Deny from all
</files>
<files fantastico_fileslist.txt>
   Order allow,deny
   Deny from all
</files>
<files fantversion.php>
   Order allow,deny
   Deny from all
</files>

… et dans le répertoire « wp-content/uploads » création d’un fichier « .htaccess » désactivant PHP :

php_flag engine Off

Changer le login du compte d’administration

Faiblesse classique sur la plupart des CMS, la création, lors de l’installation, d’un compte d’administration disposant d’un identifiant générique.
Dans le cas de WordPress, le compte, nommé « admin » est présent par défaut et est incontestablement le plus attaqué (les pirates partant du principe que ce compte existe puisqu’il est créé à l’installation).

L’astuce consiste tout simplement à changer l’identifiant (le login) du compte d’administration par une valeur qui vous convient (exemple : « admin » devient « fhh »). Les attaques continueront sans doute, mais sur un compte inexistant.

Sous WordPress, cette valeur peut être changée dans la table « wp_users » de la base de donnée. Utilisez votre outil favori pour effectuer ce changement (phpmyadmin, etc) ou en ligne de commande :

webserver ~ # mysql -p adminlinux
Enter password: 
mysql> select ID,user_login from wp_users where ID="1" ;
+----+------------+
| ID | user_login |
+----+------------+
|  1 | admin      |
+----+------------+
1 row in set (0.00 sec)
 
mysql> update wp_users set user_login="fhh" where ID="1" ;
...
mysql> select ID,user_login from wp_users where ID="1" ;
+----+------------+
| ID | user_login |
+----+------------+
|  1 | fhh        |
+----+------------+
1 row in set (0.00 sec)

Vous vous connecterez ensuite en tant qu’administrateur via le login choisi ici. Le mot de passe reste inchangé.

Protection des tables par changement des préfixes

Encore un élément configuré par défaut et inchangé sur la plupart des CMS en production : les préfixes des tables de la base de donnée.

Sous WordPress, le préfixe des tables est par défaut « wp_ ». Si ce préfixe est inchangé, le nom des tables est connu par tous, ce qui simplifiera la tâche à d’éventuels attaquants…

Pour régler ce problème, changez le préfixe des tables par n’importe quelle chaîne « aléatoire » lors de l’installation de votre CMS.

Dans le cas d’un site en production sous WordPress :

  • changer manuellement le préfixe des tables via votre outil préféré (phpmyadmin, etc) ou directement via MySQL depuis la ligne de commande :
    webserver ~ # echo $(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 8) # Génération d'un préfix aléatoire
    LM8gptTy
    webserver ~ # mysql -u login_db_wordpress -p nom_db_wordpress
    mysql> show tables ;
    +--------------------------------+
    | Tables_in_adminlinux           |
    +--------------------------------+
    | wp_commentmeta                 |
    | wp_comments                    |
    ...
    mysql> rename table wp_commentmeta to LM8gptTy_commentmeta ;
    Query OK, 0 rows affected (0.00 sec)
    mysql> rename table wp_comments to LM8gptTy_comments ;
    Query OK, 0 rows affected (0.00 sec)
    ...
  • Changer, par la méthode qui vous conviendra le plus, les préfixes des tables dans la colonne « meta_key » de la table « prefix_usermeta » ainsi que dans la colonne « option_name » de la table « prefix_options« . Ce qui donne, directement depuis MySQL :
    mysql> update `LM8gptTy_options` SET `option_name`=REPLACE(`option_name`, 'wp_', 'LM8gptTy_') ;
    ...
    mysql> update `LM8gptTy_usermeta` SET `meta_key`=REPLACE(`meta_key`, 'wp_', 'LM8gptTy_') ;
    ...
  • Modifier la variable « $table_prefix » dans le fichier de configuration du site (« wp-config.php » à la racine de WordPress) via votre éditeur préféré ou directement depuis la ligne de commande :
    13:51:52 webserver monsite # sed -i.bak "s/^\$table_prefix.*$/\$table_prefix = \'LM8gptTy_\' ;/g" ./wordpress/wp-config.php
    13:52:19 webserver monsite # grep table_prefix wordpress/wp-config.php 
    $table_prefix = 'LM8gptTy_' ;

    Note : Penser à supprimer le fichier « config.php.bak » une fois les modifications terminées et validées.

Autre méthode, utilisez le script ci-dessous :

#!/bin/bash
 
# Fichier de configuration :
CONFIG_FILE=${1:-"./wp-config.php"}
 
[ -w ${CONFIG_FILE} ] || {
        echo "ERR > \"${CONFIG_FILE}\" not writable or not found" >&2 ;
        exit 1 ;
}
 
# CURRENT_PREFIX="wp_" ;
CURRENT_PREFIX="$(grep ^\$table_prefix ${CONFIG_FILE} | sed -e "s/.*'\(.*\)'.*/\1/")" ;
# NEW_PREFIX="nouveaux_prefix_" ;
NEW_PREFIX="$(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 8)_" ;
MYSQLDB="$(grep ^define ${CONFIG_FILE} | grep DB_NAME | sed -e "s/.*'\(.*\)'.*/\1/")" ;
MYSQLUSER="$(grep ^define ${CONFIG_FILE} | grep DB_USER | sed -e "s/.*'\(.*\)'.*/\1/")" ;
MYSQLPASS="$(grep ^define ${CONFIG_FILE} | grep DB_PASSWORD | sed -e "s/.*'\(.*\)'.*/\1/")" ;
MYSQLHOST="$(grep ^define ${CONFIG_FILE} | grep DB_HOST | sed -e "s/.*'\(.*\)'.*/\1/")" ;
MYSQLPORT="3306" ;
 
function RenameTables () {
        mysqladmin -s --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} --password=${MYSQLPASS} ping 2>&1 > /dev/null || {
                echo  "Impossible to connect ${MYSQLHOST}:${MYSQLPORT}." ;
                exit 1 ;
        }
 
        mysqldump --add-drop-database --add-drop-table --create-options --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} \
                --password=${MYSQLPASS} ${MYSQLDB} --result-file="/tmp/${MYSQLDB}.sql" || {
                echo "${MYSQLDB} No backup" ;
                exit 1 ;
        }
 
        for table in $(mysql --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} --password=${MYSQLPASS} ${MYSQLDB} -sBe 'show tables') ; do
                newtable=$(echo ${table} | sed -e "s/^"${CURRENT_PREFIX}"/"${NEW_PREFIX}"/") ;
                echo "${table} > ${newtable}" ;
                mysql --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} --password=${MYSQLPASS} ${MYSQLDB} -sBe "rename table ${table} to ${newtable}" ;
        done
 
        mysql --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} --password=${MYSQLPASS} ${MYSQLDB} \
                -sBe "update \`${NEW_PREFIX}usermeta\` SET \`meta_key\`=REPLACE(\`meta_key\`, \"${CURRENT_PREFIX}\", \"${NEW_PREFIX}\")" ;
 
        mysql --host=${MYSQLHOST} --port=${MYSQLPORT} -u ${MYSQLUSER} --password=${MYSQLPASS} ${MYSQLDB} \
                -sBe "update \`${NEW_PREFIX}options\` SET \`option_name\`=REPLACE(\`option_name\`, \"${CURRENT_PREFIX}\", \"${NEW_PREFIX}\")" ;
 
        sed -i.bak "s/^\$table_prefix.*$/\$table_prefix = \'${NEW_PREFIX}\' ;/g" ${CONFIG_FILE} ;
}
 
echo "Changement du préfixe des tables \"${CURRENT_PREFIX}\" > \"${NEW_PREFIX}\"" ;
RenameTables ;

Télécharger le script « changeprefix.sh ».

Le script :

  • génère une chaîne de 8 caractères aléatoires comme nouveau préfixe ;
  • charge le fichier de configuration actuel qui doit lui être passé en argument
  • extrait les paramètres de la base de donnée de « wp-config.php » ;
  • effectue un backup de la base dans /tmp ;
  • renomme les tables ;
  • modifie les champs des tables à adapter ;
  • modifie le fichier de configuration ;
  • sauvegarde l’ancienne config (wp-config.php.bak).

Le script est invoqué avec comme paramètre le fichier de configuration de WordPress et il s’occupe du reste :

webserver ~ # ./changeprefix.sh /websites/monsite/wordpress/wp-config.php
Changement du préfixe des tables "wp_" > "2yDFbzRt_"
wp_commentmeta > 2yDFbzRt_commentmeta
wp_comments > 2yDFbzRt_comments
...

Sécuriser l’authentification (via SSL)

Afin de protéger les phases d’authentification, un minimum consiste à mettre en place du SSL sur toutes les pages nécessitant la saisie d’identifiants (https).

WordPress simplifie la sécurisation SSL et il suffit de :

  • mettre en place le https sur votre serveur ;
  • d’activer les options « FORCE_SSL_ADMIN » et « FORCE_SSL_LOGIN« 
webserver ~ # cat /websites/monsite/wp-config.php
...
define('FORCE_SSL_ADMIN', true);
define('FORCE_SSL_LOGIN', true);
...

ATTENTION : Vérifiez que le HTTPS fonctionne avant d’activer ces options. Pour cela, visitez votre site en remplaçant « http:// » par « https:// ».

Pour la mise en place du https sur un serveur personnel, voici un exemple de vhost utilisant GnuTLS (voir “mod_gnutls” : Plusieurs certificats SSL sur un “Apache”) :

<VirtualHost 10.10.0.10:443>
        ServerName monsite.admin-linux.fr:443
        ServerAlias monsupersite.admin-linux.fr
        ServerAdmin fhh@admin-linux.fr
 
        GnuTLSEnable            on
        GnuTLSPriorities        NORMAL
        GnuTLSKeyFile           /etc/ssl/monsite.admin-linux.fr.key
        GnuTLSCertificateFile   /etc/ssl/monsite.admin-linux.fr.crt
 
        DocumentRoot "/websites/monsite"
#       Si navigation en dehors des zones d'authentification, on 
#       repasse en http : 
        <IfModule mod_rewrite.c>
                RewriteEngine On
                RewriteRule !^/wp-(admin|login|register)(.*) - [C]
                RewriteRule ^/(.*) http://%{SERVER_NAME}/$1 [QSA,L]
        </IfModule>
        <Directory "/websites/monsite">
                Options FollowSymLinks
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>
 
        LogLevel warn
        ErrorLog ${APACHE_LOG_DIR}/monsite-ssl_err.log
        CustomLog ${APACHE_LOG_DIR}/monsite-ssl.log combined
</VirtualHost>

Lutter contre les attaques de type « brute force »

Une attaque de type « brute force » consiste à tester, pour un login donné, tous les mots de passe possibles.

Par défaut WordPress, comme la plupart des CMS ne restreint pas le nombre d’échec d’authentification associé à une IP donnée (ce qui ouvre la porte à ce type d’attaque).

Afin de se protéger de ces pratiques, une solution consiste à surveiller les échecs d’authentifications, en consultant les logs du CMS, et à ajouter dynamiquement une règle au firewall du serveur interdisant l’IP de l’attaquant si X tentatives de connexions ont échouées. Des outils type fail2ban sont parfaitement adaptés à ce genre de protection (à mettre en place sur votre webmail, etc).

Malheureusement, WordPress ne fournit pas ce type de logs par défaut.

L’utilisation d’un plugin tel que « Limit Login Attempts » permet également de se protéger de ce type d’attaque. Cependant, il utilise les cookies qui sont rarement utilisés par des scripts pirates.

Une solution consiste à mettre en place le plugin « Better WP Security » sur votre WordPress puis à activer la fonctionnalité (« Enable Login Limits »/ »Limiter le nombre de tentatives de connexions »).

Le plugin Better WP Security

Le plugin « Better WP Security » regroupe une grande partie des éléments présentés dans cet article et est une bonne base pour la mise en place d’une politique de sécurité sur votre site WordPress.

Références

Une réflexion au sujet de « Sécuriser son CMS : l’exemple WordPress »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *