Эксплойт для локального включения файлов file:// при восстановлении репозитория Gitea (CWE-73), который копирует цели символических ссылок за пределы недоверенного дампа во вложения релиза.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-restore-file-open
Разбор: https://abraxaslabs.tech/research/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. Это не 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. Только для лаборатории с обратной петлёй. Без гарантий.