Forgejo 16.0.5 — высокий уровень — удалённая — SSRF — hostmatcher допускает 0.0.0.1 как публичный IP
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · forgejo-hostmatcher-0001-ssrf
Класс: SSRF Достижимость: Удалённая
Forgejo 16.0.5 (8d24b7d) - Forgejo
По умолчанию [webhook] ALLOWED_HOST_LIST пуст, поэтому services/webhook/deliver.go создаёт клиент со встроенным external. Этот предикат — Go IsGlobalUnicast() && !isPrivate(). isPrivate — это RFC1918/4193 плюс добавление CGNAT из v16.0.2. RFC 6890 0.0.0.0/8 не относится ни к одному из них. 127.0.0.1 — это loopback, и он запрещён. 0.0.0.1 разрешён.
Аутентифицированный пользователь может отправить webhook на http://0.0.0.1 и прочитать тело HTTP-ответа из истории хуков. 127.0.0.1 на том же порту запрещён.
| ID | CVE пока нет |
| Класс | SSRF |
| Достижимость | Удалённая (аутентифицированный; любой пользователь, который может создать webhook репозитория) |
| CWE | CWE-918 |
| CVSS | High: 7.7 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Продукт | Forgejo |
| Затронуто | по v16.0.5 (8d24b7d); остаток от добавления CGNAT в v16.0.2 |
| Аутентификация | аутентифицированный пользователь, который может создать webhook репозитория |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Зарегистрироваться на установке по умолчанию, создать репозиторий, настроить webhook forgejo на http://0.0.0.1:<port>/..., протестировать его и прочитать тело ответа.
| Привилегия | Метод / sink | Разведка / целостность / доступность | Доказано в лаборатории или выведено |
|---|---|---|---|
Дозвониться до this-network 0.0.0.1 в обход списка разрешённых хостов webhook по умолчанию | POST /api/v1/repos/{owner}/{repo}/hooks, затем POST .../hooks/{id}/tests с URL http://0.0.0.1:8080/ssrf | Разведка (исходящий HTTP с хоста Forgejo) | Доказано в лаборатории. В истории сохранено тело HTTP 200 FORGEJO-HOSTMATCHER-0001-WITNESS |
| Прочитать полный HTTP-ответ того, кто ответил | GET /{owner}/{repo}/settings/hooks/{id} выводит ResponseInfo.Body | Разведка (SSRF с полным ответом) | Доказано в лаборатории. Тот же свидетель на странице истории |
Тот же класс через IPv4-mapped [::ffff:0.0.0.1] | Тот же matcher To4(), затем checkIP | Разведка | Выведено из hostmatcher. В лаборатории использовался 0.0.0.1 |
| Достичь слушателя, который привязался шире, чем loopback, в netns Forgejo | Linux connect() к 0.0.0.0/8 — это this-host, как только 0.0.0.1 находится на lo | Разведка | Доказано в лаборатории против sidecar-оракула. Не утверждение о конкретном облачном IMDS |
Закрыто на том же matcher по умолчанию: 127.0.0.1 (отказ доказан в лаборатории в истории), 127.0.0.0/8, RFC1918, CGNAT 100.64.0.0/10, link-local 169.254.0.0/16, включая IPv4 IMDS, unspecified 0.0.0.0. Исходящий webhook не получает X-Forgejo-Internal-Auth / INTERNAL_TOKEN. Само по себе это не RCE.
Это не исправление CGNAT из v16.0.2. В Forgejo так и не появился список зарезервированных сетей в стиле Gitea. Тот же класс, что и gitea-hostmatcher-0000-ssrf.
В v16.0.2 CGNAT был помещён в private. Патчи, добавляющие deny-list, — это подарок: вы читаете список, а затем спрашиваете, что у IANA всё ещё есть такого, что список забыл. В параллельном исследовании утверждалось, что Go 1.26 IsGlobalUnicast закрывает 0.0.0.1. Я проверил предикат и тесты в дереве. Они никогда не покрывают 0.0.0.1.
Уже зафиксированные неверные повороты: считать HTTP 201 при создании webhook оракулом (оракул — это история); успешная доставка на 127.0.0.1 (это был бы другой баг — лаборатория требует отказа); обращение к /api/internal с X-Forgejo-Internal-Auth; reverse shell. Театр.
python3 forgejo-hostmatcher-0001-ssrf-Abraxas-Labs.py
или:
cd lab
./run.sh
Цель только http://127.0.0.1:19320. Образ codeberg.org/forgejo/forgejo:16.0.5. Sidecar-оракул использует общий netns Forgejo на порту 8080.
create-hook name=neg-loopback ...
negative-control 127.0.0.1 blocked
create-hook name=hit-0001 ...
poll id=2 witness=True
SUCCESS forgejo-hostmatcher-0001-ssrf how=live pin=v16.0.5 0.0.0.1:8080 HTTP 200 FORGEJO-HOSTMATCHER-0001-WITNESS
Считать RFC 6890 0.0.0.0/8 не-external (список зарезервированных сетей, а не IsGlobalUnicast). 0.0.0.1 должен быть запрещён так же, как 127.0.0.1.
hostmatcher.goGNU Affero GPL v3.0. См. LICENSE. Только loopback-лаборатория. Без гарантий.