Mon blog s'est fait hacker, et c'est une bonne chose

Mon blog s'est fait hacker, et c'est une bonne chose

0 commentaire(s)

Cela fait maintenant quelques mois que mon blog ne donne plus de nouvelles.

En effet, j'étais tranquillement en train de manger une glace devant les Simpsons lorsque je reçois un mail type de WordPress :

Text
Quelqu’un a demandé la réinitialisation du mot de passe pour le compte suivant :
Titre du site : Stunivers
Identifiant : [nom du compte admin ici]
Si ceci est une erreur, ignorez cet e-mail et rien ne se passera.
Pour renouveler votre mot de passe, cliquez sur le lien suivant :
> lien de réinitialisation ici
Cette demande de réinitialisation de mot de passe provient de l’adresse IP xxx.xxx.xxx.xxx .

1. les premières informations


Après recherche, il apparaît que l'auteur ne se soit probablement pas caché pour ce hack, et qu'il fasse partie d'un scan automatique du web.

L'adresse IP donnée a été mentionnée à plusieurs reprises dans les logs de mon blog avant le hack, 17 fois au total.

Le pays d'origine, c'est l'Inde, et la ville, c'était Delhi.

Je pense qu'au vu du pays, ce n'est probablement pas une fausse localisation, même si ça ne va pas beaucoup m'aider.

Le pire, le hacker était sur une vieille version de Google Chrome pas à jour sur un Windows 10, hors script qui tournait depuis un VPS.

Comment dire que j'ai la rage.

2. Les dégats


Il m'était impossible de me connecter à mon environnement de production avec mes propres identifiants. Il semble qu'aucune information sensible n'ait pu fuiter au vu de la lecture de tous les logs, de mon hébergeur, et des actions faites sur le site.

3. Ce qui était en place pour sécurisé


Mon blog ne représente rien sur Internet, 3 visites à l'année pour l'instant, youhou !

Mais, ça ne veut pas dire que les campagnes de hacking automatisées n'auront aucun impact, c'est probablement ce qui m'est arrivé.

Par contre, bien que c'était minimaliste, pas mal de protections du côté de mon hébergeur étaient déjà en place, détection d'activité suspecte, etc. ... ça m'a quand même écarté pas mal de bots (et il y en a quand vous êtes sous WordPress !).

De mon côté, je me suis assuré que le lien de connexion wp-admin changerait pour éviter une grosse zone d'attaque.

J'ai aussi mis à jour manuellement mon blog, puisque j'ai un pipeline de mise à jour figurez-vous !

Je me suis aussi rendu compte que ce pipeline ... il fonctionnait sur certaines versions, sur d'autres non.

Mais, il n'échouait pas non plus, ce qui fait que je ne recevais pas les mails d'échec, je n'étais donc pas inquiet.

Et ça, c'est purement ma faute, je ne peux m'en prendre qu'à moi-même.

4. Ce que le hacker à utiliser


Le hacker à utiliser précisément cette CVE :

CVE-2026-19632 — TranslatePress

Et c'est le plugin que j'utilise pour traduire mon site qui pose problème jusqu'en version 3.3.1 qui à boucher la faille.

Cete faille idiote permettait à un attaquant non authentifié de récupérer l'URL de réinitialisation du mot de passe d'un administrateur, puis de l'utiliser pour définir un nouveau mot de passe et prendre le contrôle du compte admin.

Le mécanisme est assez débile, le hacker déclenche d'abord une procédure de récupération du mot de passe, puis TranslatePress peut enregistrer l'URL de reset dans sa base de traductions.

Via une requête AJAX accessible publiquement, il peut alors récupérer cette URL, clé de réinitialisation comprise.

L'attaquant peut alors changer le mot de passe de l'admin, ou comment bien se faire ...

5. Refonte complète des traductions


Comme je sais à quel point ça peut aller vite sur WordPress de se faire avoir, j'ai codé tous mes plugins.

Tous, sauf TranslatePress, gros boulot ...

Le plugin TranslatePress est très intéressant au vu des fonctionnalités qu'il propose, et je dois avouer que, voyant ça, j'ai été fainéant.

De ce fait, j'ai fait confiance à ce plugin pour ne pas avoir à recoder ça.

Et bah finalement, j'ai codé mon plugin de traduction.

Et, ça m'a pris du temps, parce que le diable se cache dans les détails.

Parce que d'abord, j'ai voulu lire le code de TranslatePress, et ensuite j'ai voulu comprendre les parties qui me concernent et m'intéressent.

Mon code n'est pas basé dessus, il est à la limite inspiré.

Tout ça pour s'éviter d'avoir quelque chose de public.

6. Refonte du Pipeline de mise à jour


La première fois que j'ai initialisé mon blog, je l'ai fait avec les outils de mon hébergeur.

J'avais alors les alertes par mail uniquement.

Ensuite, j'ai installé moi-même WordPress pour sortir d'une limitation bête qui ne devrait bloquer personne, mais me bloquait moi sur un point dont je ne me rappelle même plus.

Là, j'ai alors décidé de ne pas courir après les mises à jour, mais de créer mon pipeline de maj.

Alors, c'est loin d'être un simple git pull sur le projet.

J'ai utilisé Cron, WP-CLI avec plugin update --all, évidemment core update et pas d'update sur le thème, j'avais aussi codé le mien très rapidement.

On n'oublie pas de vérifier les checksums via wp core verify-checksums et on est bon.

Vu que je n'ai qu'un plugin externe, ça me semblait bien.

Sauf que, mon pipeline a précisément cassé à cause d'un échec silencieux causé par TranslatePress qui ne se mettait plus à jour.

Et là, c'est entièrement ma faute.

J'avais bien prévu de récupérer le code de retour de WP-CLI, mais ma gestion derrière était complètement débile.

En gros, je traitais correctement une erreur précise que j'avais rencontrée pendant mes tests. Mais si une autre erreur se produisait, je me contentais de la logger et de continuer.

Donc quelque chose dans ce genre :

Bash
output=$(wp plugin update --all 2>&1)
exit_code=$?
if [[ $exit_code -ne 0 ]]; then
if [[ "$output" == *"Error: Download failedd."* ]]; then
echo "An error occured"
log-mail "$output"
exit 1
else
log "$output"
exit 2
fi
fi
# The rest of the pipeline

Bon.

Déjà, le code est dégueulasse.

Mais surtout, le problème est là : je traitais une erreur connue, pas les erreurs en général.

Mon pipeline pouvait donc rencontrer un problème pendant la mise à jour de mes plugins, le logger, puis considérer que tout allait suffisamment bien pour continuer.

Et comme je n'avais pas d'alerte dans ce cas précis, je ne recevais évidemment aucun mail.

Je pensais donc que mes mises à jour fonctionnaient.

Elles ne fonctionnaient plus.

Du coup, WordPress aussi ne se mettait plus à jour, l'ensemble restait bloqué là-dessus à cause d'une mauvaise gestion au niveau de mon code.

Le plus frustrant, c'est que cette erreur vient directement de mes tests.

J'avais écrit une gestion spécifique pour le cas qui m'intéressait à ce moment-là, ça fonctionnait, j'étais content, et j'ai foutu ça en production sans suffisamment tester les autres cas et surtout dézoomé sur le script au global.

J'étais surtout beaucoup trop excité à l'idée de mettre tout ça en ligne.

Et quelques mois plus tard, je me retrouve avec un plugin vulnérable qui n'a pas été mis à jour alors que j'avais justement écrit tout un pipeline pour éviter exactement ce genre de situation.

Magnifique.

6.1. Ce que j'ai changé


Évidemment, j'ai corrigé mon erreur.

Mais je n'ai pas simplement ajouté trois lignes pour gérer le cas TranslatePress.

J'ai revu la manière dont mon pipeline traite les erreurs et l'ordre des opérations qu'il effectue.

Voici une version light et complètement modifiée pour vous montrer un peu comment je l'ai pensé :

Bash
#!/bin/bash
set -o pipefail
SITE_DIR="/home/USER/www"
LOG_DIR="/home/USER/logs"
LOG_FILE="$LOG_DIR/update-$(date '+%Y-%m-%d').log"
MAIL_TO="admin@example.com"
SITE_NAME="Mon WordPress"
mkdir -p "$LOG_DIR"
# ============================================================
# LOG + MAIL
# ============================================================
log-mail() {
local text="$1"
local exit_code="$2"
local date
date="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$date] $text" >> "$LOG_FILE"
if [ "$exit_code" -eq 0 ]; then
printf '[%s] SUCCESS : %s\n' "$date" "$text"
printf '%s\n' "$text" | mail \
-s "[OK] $SITE_NAME - Pipeline" \
"$MAIL_TO"
else
printf '[%s] ERROR : %s\n' "$date" "$text"
printf '%s\n' "$text" | mail \
-s "[ERREUR] $SITE_NAME - Pipeline" \
"$MAIL_TO"
fi
}
# ============================================================
# EXECUTE A COMMAND
# ============================================================
run_step() {
local name="$1"
shift
echo ""
echo "=========================================="
echo "$name"
echo "=========================================="
local output
output=$("$@" 2>&1)
local exit_code=$?
echo "$output" >> "$LOG_FILE"
if [ "$exit_code" -ne 0 ]; then
log-mail "$name as failled :
$output" "$exit_code"
exit "$exit_code"
fi
log-mail "$name completed successfully :
$output" 0
}
# ============================================================
# INITIALISATION
# ============================================================
cd "$SITE_DIR" || {
log-mail "Unable to accesss to $SITE_DIR" 1
exit 1
}
log-mail "Start the updating pipeline" 0
# ============================================================
# WORDPRESS - CORE
# ============================================================
run_step \
"Updating WordPress Core" \
wp core update
# ============================================================
# WORDPRESS - PLUGINS
# ============================================================
run_step \
"Updating WordPress Plugins" \
wp plugin update --all
# ============================================================
# WORDPRESS - THEMES
# ============================================================
run_step \
"Updating THEMES (i'm not using that but ..)" \
wp theme update --all
# ============================================================
# PHP - COMPOSER
# ============================================================
run_step \
"Updating Composer Dependencies" \
composer update --no-interaction --prefer-dist
# ============================================================
# NODE / REACT
# ============================================================
run_step \
"Updating Node Dependencies" \
npm ci
# ============================================================
# REACT - BUILD
# ============================================================
run_step \
"Build React" \
npm run build
# ============================================================
# FIN
# ============================================================
log-mail "Pipeline completed with success." 0
exit 0

En soi, il faut absolument retenir qu'une erreur n'est plus quelque chose que je peux simplement logger et oublier.

Et surtout, ça doit pouvoir être testé et vérifié.

Je n'ai clairement pas pris le temps pour faire ça, et j'ai même oublier d'y revenir tout en le sachant, et en l'ignorant, me disant que je suis trop petit pour que ça m'arrive.

J'ai donc aussi éprouvé mon code, plutôt que de simplement me dire :

« Ça marche maintenant, ça marchera toujours. »

Si une étape critique échoue, le pipeline doit le savoir, s'arrêter correctement et me prévenir.

Et surtout, je ne considère plus comme suffisant le fait de tester uniquement les erreurs que je connais.

Parce que c'est précisément comme ça que je me suis retrouvé avec un pipeline qui fonctionnait parfaitement... jusqu'au jour où il ne fonctionnait plus.

Cette fois, j'ai également revu les dépendances de mon blog.

Et je n'aurai plus que le code de WordPress et mon propre code.

Comme quoi, on en revient à écrire des tests, même pour un script de pipeline.

7. La mise en production


J'écris cet article depuis mon environnement de développement, ce qui ne sera plus le cas au moment où vous pourrez lire l'article.

Vous le savez, tout ce que je fais est open source.

Le code de mon nouveau plugin n'est pour l'instant pas libéré.

Je veux prendre le temps de détecter les éventuels bugs que j'aurais pu oublier.

Je vous ferai des nouvelles quand je serai prêt à ce sujet.

Vous aimerez peut-être aussi :

Commentaires

Aucun commentaire approuvé pour le moment.

Connectez-vous avec un compte commentateur pour publier un commentaire. Se connecter.