Эксплойт для локального включения файлов file:// при восстановлении репозитория Gitea (CWE-73), который копирует цели символических ссылок за пределы недоверенного дампа во вложения релиза.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-restore-file-open
Gitea 1.27.3 - Gitea
CVE-2026-59765 добавил HTTP-клиент hostmatcher в OpenWithClient. Ветка file:// игнорирует этот клиент и вызывает os.Open. При восстановлении дампа DownloadFunc отсутствует. Затем gitea_uploader.go открывает переписанный URL file://. os.Open следует по симлинку на стороне дампа. Join остаётся внутри дампа. Симлинк — нет.
Восстановление оператором недоверенного дампа копирует цель симлинка во вложение релиза. От имени uid Gitea это любой файл, который может прочитать сервисная учётная запись.
| ID | пока нет CVE |
| CWE | CWE-73 |
| CVSS | Средний: 4.2 CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:N/A:N |
| Продукт | Gitea |
| Затронуто | по v1.27.3 (146cc3e); локальный оператор gitea restore-repo |
| Аутентификация | локальное восстановление оператором недоверенного дампа |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Передайте администратору недоверенный дамп (каталог «резервной копии», восстановление из недоверенного удалённого источника), содержащий симлинк по пути, присоединённому внутри дампа. Восстановление копирует целевой файл во вложение релиза восстановленного репозитория. Кто может читать этот репозиторий — скачивает его.
Если автор дампа назовёт app.ini, это кража учётных данных (INTERNAL_TOKEN, SECRET_KEY, JWT, пароль БД). Оракулом в лаборатории был подложенный файл-свидетель, а не app.ini. Это не OS LPE. Это не неаутентифицированный доступ. Это не выход за пределы корня дампа через join. Живая миграция из GitHub/GitLab/Gitea устанавливает DownloadFunc и не вызывает os.Open для атакующего file://.
На оставшейся таблице лежали две карты восстановления. Сначала я проверил в лаборатории очевидную.
Находка 8 — выход за пределы корня дампа через join: поместить абсолютный DownloadURL в release.yml, надеясь, что FilePathJoinAbs отбросит префикс дампа так же, как это делает наивный filepath.Join(base, "/etc/passwd"). На 1.27.3 это не так. Каждый sub — это filepath.Clean("/"+s), затем filepath.Join(base, cleaned). Join реплики /tmp/dump + /tmp/GITEA-JOIN-OUTSIDE остался /tmp/dump/tmp/GITEA-JOIN-OUTSIDE. Восстановление переписало file:// внутри дампа и упало с open ...: no such file or directory. На этом теге это не выход через join.
Находка 9 — то, что сработало. Я подложил файл-свидетель вне дампа, поместил симлинк по пути, присоединённому внутри дампа, запустил gitea restore-repo. Восстановление rc=0. Восстановленное вложение релиза содержало GITEA-FILE-OPEN-WITNESS.
Уже зафиксированные ошибочные пути: выход за пределы корня дампа через join с помощью абсолютного DownloadURL; LFI через file:// при живой миграции; X-Gitea-Internal-Auth (восстановление — это CLI от имени uid Gitea, а не /api/internal); трактовка HTTP восстановления как удалённого неаутентифицированного (CVSS — локальный, высокие привилегии, взаимодействие с пользователем); обратная оболочка. Театр.
cd lab
./run.sh
Цель только http://127.0.0.1:18137. Compose монтирует lab/dump только для чтения. run.sh подкладывает свидетеля внутри контейнера, собирает дамп, помещает симлинк, затем gitea restore-repo.
restore-rc=0 Restore repo labadmin/restored successfully
download status=200 snippet=GITEA-FILE-OPEN-WITNESS
SUCCESS GITEA-RESTORE-FILE-OPEN
Убрать file:// из OpenWithClient или отказаться следовать симлинкам дампа, и не восстанавливать недоверенные дампы. Восстановленное вложение не должно содержать свидетеля вне дампа.
uri.go · gitea_uploader.go · FilePathJoinAbsGNU Affero GPL v3.0. См. LICENSE. Только лаборатория на loopback. Без гарантий.