Sploitus

Exploit for CVE-2026-85706

githubexploit Β· 2026-09-14

Exploit Code

README117 lines
## https://sploitus.com/exploit?id=FDE2E47C-D236-5CCF-8BDD-C05F179A3A2D
# CVE-2026-85706 β€” GitLab Unauthenticated Arbitrary File Read

> **CVSS 10.0 Β· Unauthenticated Β· Actively exploited (CISA KEV)**

A parser differential between **GitLab Workhorse** (Go reverse proxy) and **Puma/Grape**
(Ruby) lets an unauthenticated attacker bypass Workhorse's accelerated-upload handoff and
reach three upload endpoints with an attacker-controlled `file.path`, yielding **arbitrary
file read** on the GitLab host.

Encoding one character of `files` as `%66iles` makes Workhorse's upload route **miss** (it
matches on the *encoded* path) while Rails decodes it back and hits the real handler (it
routes on the *decoded* path). The handler trusts the raw `file.path` param that Workhorse
was supposed to overwrite β€” so `file.path=/etc/passwd` is read from disk with no credentials.

```
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
                                   ^^^^^^  Workhorse misses -> raw file.path survives to Rails
```

## Affected / fixed

| | Version |
|---|---|
| **Affected** | CE/EE `18.7 β†’ 19.1.8`, `19.2 β†’ 19.2.6`, `19.3 β†’ 19.3.2` |
| **Fixed** | `19.1.8` / `19.2.6` / `19.3.2` (2026-09-10) |

## What's in this repo

| Path | Contents |
|---|---|
| [`docs/ANALYSIS.md`](docs/ANALYSIS.md) | Full root-cause analysis β€” parser differential, the two Workhorse JWTs, the raw-`file.path` trust bug, the patch diff, and the reflected-error exfiltration channel. Analysis done against real source (`v19.3.1-ee` vs `v19.3.2-ee`). |
| [`lab/`](lab/) | Docker-based vulnerable lab (`gitlab-ce:19.3.1-ce.0`) + bring-up instructions. |
| [`poc/`](poc/) | `detect.sh` (non-destructive existence oracle) and `exploit.sh` (reflected-error file read). Single-target, authorization-gated. |

## Background β€” GitLab's request pipeline

Not familiar with GitLab internals? Here's what each layer does β€” in the order a request
passes through them. A deeper glossary with key concepts is in
[`docs/ANALYSIS.md` Β§0](docs/ANALYSIS.md#0-glossary--gitlabs-request-pipeline).

```
  Internet
     β”‚
     β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ NGINX  │────▢│ Workhorse │────▢│  Puma  │────▢│  Grape / Rails app  β”‚
 β”‚(proxy) β”‚     β”‚   (Go)    β”‚     β”‚ (Ruby) β”‚     β”‚      (Ruby)         β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

| Component | What it is |
|---|---|
| **NGINX** | Outermost reverse proxy. Terminates TLS, serves static files, forwards everything else inward. Not directly involved in this vulnerability. |
| **Workhorse** | A Go reverse proxy specific to GitLab. Its main job is offloading work Ruby is slow at β€” especially **streaming large uploads**. For upload endpoints, Workhorse buffers the body to a temp file, signs a JWT, and rewrites `file.path` so Ruby never sees raw upload bytes. It also stamps a per-request `Gitlab-Workhorse-Api-Request` JWT on *every* request it forwards (proving "this came through the proxy," **not** "this user is authenticated"). |
| **Puma** | The Ruby application server that runs the Rails app. Receives requests from Workhorse, runs middleware (Rack), and dispatches to the router. |
| **Rack** | The Ruby web-server interface layer. Rack middleware handles query-string parsing, session management, and β€” critically here β€” verifying Workhorse's upload JWT and building `UploadedFile` objects. `Rack::Utils.parse_nested_query` is the function whose error messages leak file content in this exploit. |
| **Grape** | A REST API framework used by GitLab for all `/api/v4/*` endpoints. Provides route definitions and before-filters like `require_gitlab_workhorse!` (proxy check) and `authenticate!` (user identity check). Runs inside Rails, on Puma, behind Workhorse β€” so it sees the **decoded** URL path. |
| **Rails** | The overall web framework (Ruby on Rails). GitLab is a Rails monolith β€” models, services, and middleware all run here inside Puma. |

The vulnerability lives in the gap between **Workhorse** (which matches routes on the
*encoded* path) and **Grape/Puma** (which route on the *decoded* path). See below.

## TL;DR of the mechanism

1. **Parser differential.** Workhorse matches routes on `r.URL.EscapedPath()` (encoded);
   Puma/Grape route on the decoded path. `%66iles` β‰  regex `files` for Workhorse, but
   decodes to `files` for Rails.
2. **Handoff skipped.** Because the upload route missed, Workhorse never buffers the body
   to a temp file, never rewrites `file.path` to a signed temp path, and never sets the
   `Gitlab-Workhorse-Multipart-Fields` header β€” but it *still proxies* the request (with a
   valid `Gitlab-Workhorse-Api-Request` JWT).
3. **Missing auth + raw-param trust.** The endpoint had no `authenticate!`, and the handler
   read `params['file.path']` directly (the attacker's string) instead of the
   Workhorse-verified, path-confined `params[:file]` `UploadedFile`.
4. **Exfil via error messages.** Content leaks through `Rack::Utils.parse_nested_query`
   error strings (`"invalid %-encoding ()"`) β€” the response reflects file bytes
   up to the first invalid `%`.

The patch adds `authenticate!`, switches to the verified `UploadedFile`, and stops
reflecting `e.message`. See [`docs/ANALYSIS.md`](docs/ANALYSIS.md) Β§5.

## Quick start

```bash
# 1. Stand up the vulnerable lab (see lab/README.md for details)
cd lab && docker compose up -d      # wait ~5 min for GitLab to become healthy

# 2. Non-destructive detection
../poc/detect.sh http://localhost:8929

# 3. File-read PoC against a file you're authorized to read on your own lab
../poc/exploit.sh http://localhost:8929 /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
```

## ⚠️ Responsible use

This is published for **defensive and educational purposes**: understanding, detecting, and
patching a disclosed, patched CVE that is on the CISA KEV list. The scripts here are
**single-target** and require you to pass the target explicitly.

- Only run these against systems **you own or are explicitly authorized to test.**
- The lab is designed to run on `localhost`. Do not point it at third-party hosts.
- Unauthorized access to computer systems is illegal in most jurisdictions.

If you run GitLab, **upgrade to a fixed version** β€” that is the only real remediation.

## References

- watchTowr β€” [Rapid Reaction: GitLab Path Traversal (CVE-2026-85706)](https://watchtowr.com/resources/rapid-reaction-gitlab-critical-path-traversal-vulnerability-cve-2026-85706/)
- Nuclei Templates β€” [PR #17231](https://github.com/projectdiscovery/nuclei-templates/pull/17231)
- Primary source diff: `gitlab-org/gitlab` @ `v19.3.1-ee` vs `v19.3.2-ee`

## License

[MIT](LICENSE) β€” analysis and PoC code only. GitLab is a trademark of GitLab Inc.; this
repository is not affiliated with or endorsed by GitLab Inc.