
GitLab — высокий уровень — SSRF при HTTPS-импорте Gitea — обход UrlBlocker
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-gitea-import-ssrf
GitLab CE 19.4.1 - GitLab Inc.
Импорт Gitea вызывает UrlBlocker.validate! один раз, а затем для https отбрасывает URI с закреплённым IP и передаёт Faraday исходное имя хоста. Octokit UrlValidation проверяет снова и не перезаписывает env[:url]. Перепривяжите имя между этими проверками, и GitLab выполнит GET на loopback, как будто это Gitea. JSON со списком репозиториев возвращается в ответ на GET /import/gitea/status.json. HTTP-импорты сохраняют закрепление. Прямой https://127.0.0.1 блокируется.
Аутентифицированный пользователь, который может открыть импорт Gitea в своём личном пространстве имён, может заставить GitLab обратиться к хосту, который он перепривязывает к loopback, и получить тело ответа.
| ID | CVE пока нет |
| CWE | CWE-918, CWE-367 |
| CVSS | High: 7.7 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N |
| Продукт | GitLab CE |
| Затронуто | 19.4.1-ce.0 (26212baacadb в дереве v19.4.1-ee) |
| Аутентификация | аутентифицированный пользователь, импорт Gitea, личное пространство имён |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Установите Gitea Host URL в https://<имя-под-их-контролем>/. UrlBlocker разрешает первую публичную A-запись. HTTPS client_options сохраняет это имя хоста. Перепривяжите к 127.0.0.1. Faraday выполняет GET к перехватчику на loopback воркера GitLab. provider_repos в status.json — это то, что вернул перехватчик.
На all-in-one omnibus это Workhorse, Puma, экспортёры и всё остальное, слушающее на воркере. Не RCE на хосте. Не без аутентификации. HTTP Host URL остаётся на закреплённом IP.
Тот же продукт, родственные недоделки: Gitaly fetch omits resolved_address, webhook 0.0.0.0/8.
Сначала я прочитал примечания к патчам от 23 сентября 2026 года для 19.4.1 / 19.3.3 / 19.2.7, затем дерево v19.4.1-ee. CI regex RCE, MR diff XSS, чтение file.path в Workhorse закрыты на этом пине. Import SSRF не был доделан.
Import::GiteaController#client_options — это недоделка:
host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s,
HTTP использует verified_url (IP). HTTPS использует provider_url (имя, которое ввёл пользователь). LegacyGithubImport::Client строит Octokit api_endpoint из этого хоста. Gitlab::Octokit::UrlValidation вызывает validate!, затем @app.call(env) без перезаписи.
Я поднял стандартный gitlab/gitlab-ce:19.4.1-ce.0. Local-requests выключены. CA лаборатории в /etc/gitlab/trusted-certs, потому что Faraday ssl.verify включён. DNS для gitea.lab: запросы 1-2 возвращают 1.1.1.1, последующие запросы возвращают 127.0.0.1. Перехватчик использует общий netns с GitLab на :443. GET /import/gitea/status.json вернул provider_repos с именем GITLAB-GITEA-IMPORT-SSRF-WITNESS. Лог перехватчика: GET /api/v1/user/repos peer=127.0.0.1 host=gitea.lab.
Прямой https://127.0.0.1/ : "Specified URL cannot be used". HTTP http://gitea.lab/ остался на закреплённом IP и не вернул свидетеля.
Уже зафиксированные ошибочные пути: /users/sign_in с Accept: application/json даёт 404 (HTML даёт 200); пароль сида CveLabRoot9! отклонён, потому что содержит root; bind-mount CA с ro даёт EROFS после того, как reconfigure создаёт ссылку на файл; первые два, затем loopback на запросе 2 заставляет UrlValidation заблокировать до того, как Faraday подключится. Режим pairs (нечётный публичный, чётный loopback) — это то, что сработало. Реверс-шелл. Театр. Оракул — это свидетель в status.json плюс peer перехватчика.
cd lab
./run.sh
Цель только http://127.0.0.1:18400. GitLab CE требует несколько ГБ RAM. Первая загрузка медленная. Только авторизованная лаборатория.
status.json status=200 ... provider_repos ... GITLAB-GITEA-IMPORT-SSRF-WITNESS
catcher GET /api/v1/user/repos peer=127.0.0.1 host=gitea.lab status=200 witness=True
negative-a https://127.0.0.1 blocked
negative-b http://gitea.lab no-witness
SUCCESS GITLAB-GITEA-IMPORT-SSRF status.json mode=pairs http=200 GITLAB-GITEA-IMPORT-SSRF-WITNESS
def client_options
verified_url, provider_hostname = verify_blocked_uri
{
host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s,
api_version: 'v1',
hostname: provider_hostname
}
end
Используйте verified_url.to_s для https так же, как это уже делает HTTP. Перезапишите env[:url] в Octokit UrlValidation на закреплённый IP.
gitea_controller.rb · legacy_github_import/client.rb · octokit/url_validation.rbGNU Affero GPL v3.0. См. LICENSE. Только loopback-лаборатория. Без гарантий.