Prova de conceito para uma inclusão de arquivo local file:// no restore-repo do Gitea (CWE-73) que copia alvos de symlink fora de um dump não confiável para anexos de release.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-restore-file-open
Gitea 1.27.3 - Gitea
CVE-2026-59765 colocou um cliente HTTP hostmatcher em OpenWithClient. O ramo file:// ignora esse cliente e chama os.Open. A restauração de um dump não tem DownloadFunc. gitea_uploader.go então abre a URL file:// reescrita. os.Open segue um symlink do lado do dump. O join permanece sob o dump. O symlink não.
A restauração por um operador de um dump não confiável copia o alvo do symlink para um anexo de release. Como o uid do Gitea, isso é qualquer arquivo que a conta de serviço possa ler.
| ID | ainda sem CVE |
| CWE | CWE-73 |
| CVSS | Médio: 4.2 CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:N/A:N |
| Produto | Gitea |
| Afetado | até v1.27.3 (146cc3e); operador local gitea restore-repo |
| Auth | restauração por operador local de um dump não confiável |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Entregue a um admin um dump não confiável (um diretório de "backup", uma restauração de um remoto não confiável) que contenha um symlink sob o caminho unido sob o dump. A restauração copia o arquivo alvo para um anexo de release no repositório restaurado. Quem puder ler esse repositório o baixa.
Se o autor do dump nomear app.ini, isso é roubo de credenciais (INTERNAL_TOKEN, SECRET_KEY, JWT, senha do BD). O oráculo do lab foi um arquivo testemunha plantado, não app.ini. Não é LPE de SO. Não é não autenticado. Não é escape de join na raiz do dump. Migração ao vivo do GitHub/GitLab/Gitea define DownloadFunc e não faz os.Open de um file:// do atacante.
Dois mapas de restauração estavam na tabela remanescente. Testei o óbvio primeiro no lab.
O achado 8 era escape de join na raiz do dump: colocar um DownloadURL absoluto em release.yml, esperar que FilePathJoinAbs descartasse o prefixo do dump como o ingênuo filepath.Join(base, "/etc/passwd") faz. No 1.27.3 não descarta. Cada sub é filepath.Clean("/"+s) e depois filepath.Join(base, cleaned). O join replicado de /tmp/dump + /tmp/GITEA-JOIN-OUTSIDE permaneceu /tmp/dump/tmp/GITEA-JOIN-OUTSIDE. A restauração reescreveu file:// sob o dump e falhou com open ...: no such file or directory. Isso não é escape de join nesta tag.
O achado 9 é o que deu certo. Plantei um arquivo testemunha fora do dump, coloquei um symlink no caminho unido sob o dump, executei gitea restore-repo. Restauração rc=0. O anexo de release restaurado continha GITEA-FILE-OPEN-WITNESS.
Caminhos errados já registrados: escape de join na raiz do dump via um DownloadURL absoluto; LFI de file:// em migração ao vivo; X-Gitea-Internal-Auth (a restauração é CLI como o uid do Gitea, não /api/internal); tratar o HTTP de restauração como remoto não autenticado (o CVSS é local, alto privilégio, interação do usuário); um reverse shell. Teatro.
cd lab
./run.sh
Alvo apenas http://127.0.0.1:18137. O compose monta lab/dump como somente leitura. run.sh planta a testemunha dentro do contêiner, constrói o dump, coloca o symlink, depois gitea restore-repo.
restore-rc=0 Restore repo labadmin/restored successfully
download status=200 snippet=GITEA-FILE-OPEN-WITNESS
SUCCESS GITEA-RESTORE-FILE-OPEN
Remova file:// de OpenWithClient, ou recuse seguir symlinks do dump, e não restaure dumps não confiáveis. O anexo restaurado não deve conter a testemunha fora do dump.
uri.go · gitea_uploader.go · FilePathJoinAbsGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.