Preuve de concept pour une inclusion de fichier local file:// via Gitea restore-repo (CWE-73) qui copie les cibles de liens symboliques en dehors d'un dump non fiable vers les pièces jointes de release.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-restore-file-open
Write-up : https://abraxaslabs.tech/research/gitea-restore-file-open
Gitea 1.27.3 - Gitea
CVE-2026-59765 a placé un client HTTP hostmatcher sur OpenWithClient. La branche file:// ignore ce client et appelle os.Open. La restauration d'un dump n'a pas de DownloadFunc. gitea_uploader.go ouvre ensuite l'URL file:// réécrite. os.Open suit un symlink côté dump. La jointure reste sous le dump. Le symlink, non.
La restauration par un opérateur d'un dump non fiable copie la cible du symlink dans une pièce jointe de release. Sous l'uid Gitea, cela correspond à tout fichier que le compte de service peut lire.
| ID | pas encore de CVE |
| CWE | CWE-73 |
| CVSS | Moyen : 4.2 CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:N/A:N |
| Produit | Gitea |
| Affecté | jusqu'à v1.27.3 (146cc3e) ; opérateur local gitea restore-repo |
| Auth | restauration par un opérateur local d'un dump non fiable |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Remettre à un administrateur un dump non fiable (un répertoire de « sauvegarde », une restauration depuis un remote non fiable) qui contient un symlink sous le chemin joint sous le dump. La restauration copie le fichier cible dans une pièce jointe de release sur le dépôt restauré. Quiconque peut lire ce dépôt le télécharge.
Si l'auteur du dump nomme app.ini, c'est un vol d'identifiants (INTERNAL_TOKEN, SECRET_KEY, JWT, mot de passe de la base de données). L'oracle du lab était un fichier témoin planté, pas app.ini. Ce n'est pas une LPE au niveau de l'OS. Ce n'est pas non authentifié. Ce n'est pas une évasion de jointure à la racine du dump. La migration en direct depuis GitHub/GitLab/Gitea définit DownloadFunc et ne fait pas de os.Open sur un file:// contrôlé par l'attaquant.
Deux cartes de restauration traînaient sur la table des restes. J'ai d'abord testé en lab la plus évidente.
Le constat 8 était une évasion de jointure à la racine du dump : placer un DownloadURL absolu dans release.yml, en espérant que FilePathJoinAbs supprime le préfixe du dump comme le fait naïvement filepath.Join(base, "/etc/passwd"). Sur la 1.27.3, ce n'est pas le cas. Chaque sub est filepath.Clean("/"+s) puis filepath.Join(base, cleaned). La jointure répliquée de /tmp/dump + /tmp/GITEA-JOIN-OUTSIDE restait /tmp/dump/tmp/GITEA-JOIN-OUTSIDE. La restauration réécrivait file:// sous le dump et échouait avec open ...: no such file or directory. Ce n'est pas une évasion de jointure sur ce tag.
Le constat 9 est celui qui a abouti. J'ai planté un fichier témoin en dehors du dump, placé un symlink au chemin joint sous le dump, exécuté gitea restore-repo. Restauration rc=0. La pièce jointe de release restaurée contenait GITEA-FILE-OPEN-WITNESS.
Fausses pistes déjà consignées : évasion de jointure à la racine du dump via un DownloadURL absolu ; LFI file:// en migration en direct ; X-Gitea-Internal-Auth (la restauration est en CLI sous l'uid Gitea, pas via /api/internal) ; traiter la restauration HTTP comme une attaque distante non authentifiée (le CVSS est local, privilèges élevés, interaction utilisateur) ; un reverse shell. Du théâtre.
cd lab
./run.sh
Ciblez uniquement http://127.0.0.1:18137. Compose monte lab/dump en lecture seule. run.sh place le témoin à l'intérieur du conteneur, construit le dump, met le lien symbolique, puis gitea restore-repo.
restore-rc=0 Restore repo labadmin/restored successfully
download status=200 snippet=GITEA-FILE-OPEN-WITNESS
SUCCESS GITEA-RESTORE-FILE-OPEN
Supprimer file:// de OpenWithClient, ou refuser de suivre les liens symboliques de dump, et ne pas restaurer de dumps non fiables. La pièce jointe restaurée ne doit pas contenir le témoin hors-dump.
uri.go · gitea_uploader.go · FilePathJoinAbsGNU Affero GPL v3.0. Voir LICENSE. Laboratoire en boucle locale uniquement. Aucune garantie.