- INTERNAL_TOKEN, LFS_JWT_SECRET et mot de passe MySQL rotés le 2026-09-23 - DB_PASSWD CapRover resynchronisé, service redémarré et vérifié - Confirmation que les env vars CapRover de l'app gitea (DB_PASSWD etc.) ne sont pas au format GITEA__section__clé donc inertes pour app.ini - Documentation de l'installation/usage du CLI caprover via fnm Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.3 KiB
Incident : mineur de cryptomonnaie via Gitea (packObjectsHook)
- Statut : contenu, secrets serveur rotés (2026-09-23). Reste : upgrade Gitea, restriction
/api/internal/*, mots de passe utilisateurs/tokens/webhooks. - Date de compromission : 2026-09-14 ~15:16-15:17
- Date de détection : 2026-09-22 (via un
git clonecassé) - Impact : exécution de code sur le serveur hébergeant Gitea (et potentiellement l'hôte partagé, ~40 services CapRover) ; minage de cryptomonnaie (Monero) actif pendant ~8 jours
Découverte
Un git clone sur git.sans.pub échouait systématiquement (SSH et HTTPS) avec :
remote: wget: server returned error: HTTP/1.1 404 Not Found
remote: curl: (22) The requested URL returned error: 404
fatal: fin de fichier prématurée
fatal: fetch-pack : sortie d'index de pack invalide
Le problème touchait tous les dépôts, pas un seul — signe d'un problème serveur global plutôt qu'un souci de repo.
Analyse
GIT_TRACE_PACKET=1 a montré que le serveur, juste après avoir annoncé packfile, envoyait sur le canal d'erreur (sideband 2) le texte wget: ... 404 / curl: ... 404 puis coupait la connexion — donc un script serveur s'exécute pendant la génération du pack, avant même l'envoi des données.
Cause trouvée dans /data/gitea/home/.gitconfig (dans le volume Docker captain--gitea-data) : 13 lignes [uploadpack] packObjectsHook = sh /data/gitea/home/p0_XXXXXXXX.sh injectées, chacune maquillée en fin de ligne par un faux commentaire imitant une entrée de log Gitea :
;#router: completed POST /api/internal/manager/add-logger for 10.0.1.105:XXXXX, 200 OK ...
Cette technique fait croire, à la lecture rapide du fichier, qu'il s'agit d'une ligne de log et non de config active.
Vecteur d'entrée (hypothèse la plus probable)
L'endpoint interne Gitea /api/internal/manager/add-logger (normalement réservé à un usage interne, protégé par INTERNAL_TOKEN) semble avoir été atteignable depuis l'extérieur via le reverse-proxy public (l'IP source 10.0.1.105 observée dans les logs correspond au conteneur captain-nginx, donc toutes les requêtes publiques passent par cette IP côté Gitea — la requête malveillante est arrivée par le chemin HTTP public normal). Ceci est cohérent avec des vulnérabilités connues de Gitea permettant l'injection de config git via cette API interne mal exposée.
À vérifier/confirmer : version exacte de Gitea au moment de l'attaque (gitea/gitea:1.25.4), CVE correspondant, et si /api/internal/* est bien bloqué au niveau du reverse-proxy CapRover.
Charge malveillante
Les scripts injectés (p0_c9cd952f.sh, p0_9759db49.sh) :
- Téléchargent un binaire déguisé :
https://sunnyeye.solarvest.my/health.bin→ sauvegardé sous/data/gitea/.sys_health_s3 - Écrivent une config XMRig (JSON encodé en base64 dans le script) pointant vers des pools Monero (
gulf.moneroocean.stream,pool.hashvault.pro) avec une adresse de wallet dédiée - Assurent la persistance :
- via
crontab(*/3 * * * *+@reboot) sicron/crondest disponible - sinon via une boucle
sh -c 'while sleep 180; do <watchdog>; done'détachée (nohup)
- via
- Se camouflent sous des noms plausibles :
.sys_health_s3,.gitea_cron_health,.wp_s2_cron,.gitea_watchdog.pid
Le mineur tournait en continu depuis le 2026-09-14 (le conteneur Gitea n'avait pas redémarré depuis), consommant ~298% CPU / ~2.4 Go RAM au moment de la détection. Il a cessé de fonctionner correctement quand le domaine sunnyeye.solarvest.my s'est mis à répondre 404 — c'est cet échec de téléchargement qui a cassé les clones et permis la détection.
Remédiation effectuée (2026-09-22)
- Process du mineur et du watchdog tués (
kill -9sur les PID hôte) - Fichiers de persistance supprimés :
.sys_health_s3,.sys_health_s3.json,.sys_health_s3.log,.gitea_cron_health,.gitea_watchdog.pid .gitconfignettoyé des 13 lignespackObjectsHookinjectées (backup conservé :/data/gitea/home/.gitconfig.bak-20260922234958)- Clone git re-testé et fonctionnel
- Audit de la base Gitea (voir ci-dessous) : aucune autre persistance trouvée
Audit Gitea (base de données) — résultat
Aucune anomalie détectée au-delà de l'injection packObjectsHook :
| Élément vérifié | Résultat |
|---|---|
| Comptes utilisateurs | 3 comptes (pierre admin, Sans.pub, Duogeeks), tous antérieurs à l'incident |
| Clés SSH publiques | 3 clés, aucune ajout suspect autour du 09-14 |
| Tokens d'accès personnels | 4 tokens, tous antérieurs et cohérents avec l'usage habituel |
| Webhooks | 4 webhooks de déploiement CapRover, tous de 2020-2021 |
| Sources de login externes | 1 seule, préexistante (2020) |
| Collaborateurs sur les repos | Rien d'ajouté récemment |
| Organisations | Aucune organisation suspecte |
Conclusion : l'attaque semble limitée à l'exécution de commande via le hook git (cryptojacking), sans prise de compte ni persistance au niveau applicatif Gitea. Mais l'attaquant avait un accès en lecture au système de fichiers du conteneur, donc à app.ini (secrets en clair) — voir rotation ci-dessous.
Rotation des secrets — effectuée le 2026-09-23
Confirmé via docker service inspect puis via l'API CapRover (double vérification) : les variables d'env CapRover de l'app gitea sont RUN_MODE, DB_TYPE, DB_HOST, DB_USER, DB_PASSWD — aucune au format GITEA__<section>__<clé> attendu par l'image officielle pour être réinjectée dans app.ini. Elles sont donc inertes pour Gitea (c'est app.ini dans le volume qui fait foi). Conséquence : INTERNAL_TOKEN/LFS_JWT_SECRET n'ont pas de variable CapRover à mettre à jour ; seul DB_PASSWD avait une copie dormante à resynchroniser.
Effectué :
INTERNAL_TOKENrégénéré (gitea generate secret INTERNAL_TOKEN) dansapp.iniLFS_JWT_SECRETrégénéré (gitea generate secret LFS_JWT_SECRET) dansapp.ini- Mot de passe MySQL de l'utilisateur
gitea@%changé (ALTER USER ... IDENTIFIED BY ...) app.inimis à jour avec les 3 nouvelles valeurs (backup :app.ini.bak-20260923004549)- Service Gitea redémarré (
docker service update --force srv-captain--gitea), reconnexion DB confirmée dans les logs, aucune erreur - Clone git testé et fonctionnel après rotation
- Variable CapRover
DB_PASSWDresynchronisée via l'API (/user/apps/appDefinitions/update), vérifiée par comparaison de hash
Outillage utilisé : CLI caprover (installé localement via fnm + npm install -g caprover, authentifié avec caprover login — voir section CapRover CLI dans CLAUDE.md).
Reste à faire
- Mettre à jour Gitea vers la dernière version patchée (partir de
1.25.4) - Vérifier/restreindre l'accès à
/api/internal/*au niveau du reverse-proxy (défense en profondeur, même si Gitea patch le bug) - Changer les mots de passe des 3 comptes Gitea (pierre, Sans.pub, Duogeeks) par précaution
- Révoquer et régénérer les 4 tokens d'accès personnels (LML, Piaire CI/Deploy, Piaire, Laptop .npmrc) — penser à mettre à jour partout où ils sont utilisés
- Régénérer les 4 tokens de webhook CapRover (déploiement tmp-fc, dechets-agglo-muretain, etherpad, domi-marche) et mettre à jour les URLs de webhook correspondantes dans Gitea
- Vérifier l'intégrité des autres services sur le même hôte (mouvement latéral) — rien trouvé à ce stade, mais l'hôte partagé fait tourner Vaultwarden, Nextcloud, mail
- Envisager un scan antirootkit / vérification d'intégrité des binaires système sur l'hôte, vu la durée de la compromission (8 jours)
- Revoir si
DOMAIN = localhostdansapp.ini([server]) est correct ou un reliquat de config
Indicateurs de compromission (IOC)
- Domaine :
sunnyeye.solarvest.my - Fichiers :
p0_*.sh,.sys_health_s3,.sys_health_s3.json,.sys_health_s3.log,.gitea_cron_health,.gitea_watchdog.pid,.wp_s2_cron,.wp_s2_wd.pid,.s3w - Chemins ciblés pour la persistance (dans cet ordre) :
/data/gitea,/data/git,/home/git,/var/tmp,/dev/shm,/tmp,$HOME - Pattern de commentaire d'obfuscation dans les configs git :
;#router: completed POST /api/internal/manager/add-logger for <ip>, 200 OK ...