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
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. Le join 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. En tant qu'uid Gitea, c'est n'importe quel 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 admin 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 DB). L'oracle du lab était un fichier témoin planté, pas app.ini. Ce n'est pas une LPE OS. Ce n'est pas non authentifié. Ce n'est pas une évasion de join à la racine du dump. La migration live depuis GitHub/GitLab/Gitea définit DownloadFunc et ne fait pas de os.Open sur un file:// attaquant.
Deux cartes de restauration traînaient sur la table restante. J'ai d'abord testé en lab la plus évidente.
La trouvaille 8 était l'évasion de join à la racine du dump : mettre un DownloadURL absolu dans release.yml, espérer que FilePathJoinAbs supprime le préfixe du dump comme le fait un filepath.Join(base, "/etc/passwd") naïf. Sur 1.27.3, ce n'est pas le cas. Chaque sub est filepath.Clean("/"+s) puis filepath.Join(base, cleaned). Le join répliqué 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 join sur ce tag.
La trouvaille 9 est celle qui a abouti. J'ai planté un fichier témoin en dehors du dump, placé un symlink au chemin joint-sous-le-dump, lancé 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 join à la racine du dump via un DownloadURL absolu ; LFI file:// en migration live ; X-Gitea-Internal-Auth (la restauration est en CLI en tant qu'uid Gitea, pas /api/internal) ; traiter la restauration HTTP comme non authentifiée à distance (le CVSS est local, privilège élevé, interaction utilisateur) ; un reverse shell. Du théâtre.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:18137. Compose monte lab/dump en lecture seule. run.sh plante le témoin dans le conteneur, construit le dump, place le symlink, 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
Retirer file:// de OpenWithClient, ou refuser de suivre les symlinks du dump, et ne pas restaurer de dumps non fiables. La pièce jointe restaurée ne doit pas contenir le témoin hors du dump.
uri.go · gitea_uploader.go · FilePathJoinAbsGNU Affero GPL v3.0. Voir LICENSE. Lab en loopback uniquement. Aucune garantie.