
CVE-2026-85706 के लिए PoC exploit और writeup, जो GitLab CE/EE में Workhorse path-encoding bypass के माध्यम से एक unauthenticated arbitrary local file read है।
GitLab CE/EE में अनधिकृत मनमाना लोकल फ़ाइल रीड। 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) में ठीक किया गया। CVSS 10.0, कथित तौर पर जंगल में शोषण किया गया। मूल रिपोर्ट s3ntago द्वारा और यह रेपो सिर्फ़ मेरा writeup + PoC है।
यह PoC केवल शैक्षिक और रक्षात्मक शोध उद्देश्यों के लिए प्रकाशित किया गया है, ताकि एडमिन और शोधकर्ता इस भेद्यता को समझ सकें और इसका परीक्षण कर सकें। इसे विशेष रूप से उन सिस्टमों के विरुद्ध चलाएँ जिनके आप मालिक हैं या जिनका परीक्षण करने के लिए आपके पास स्पष्ट लिखित प्राधिकरण है। यदि आपका इंस्टेंस प्रभावित रेंज में आता है, तो पढ़ना बंद करें और पहले 19.1.8 / 19.2.6 / 19.3.2 पर पैच करें।
तीन रिपॉज़िटरी एंडपॉइंट (POST :id/repository/commits, POST/PUT :id/repository/files/:file_path) Workhorse के requestBodyUploader के पीछे बैठे हैं। Rails हैंडलर सीधे कच्चे file.path रिक्वेस्ट फ़ील्ड से डिस्क पर मौजूद पाथ पढ़ता है और किसी भी प्रमाणीकरण से पहले उसे File.open करता है। प्रमाणीकरण का काम नहीं करता क्योंकि Workhorse का signing round-tripper हर उस रिक्वेस्ट पर एक वैध JWT संलग्न करता है जिसे वह प्रॉक्सी करता है, इसलिए जो कुछ भी generic API proxy तक पहुँचता है वह उस जाँच को पास कर लेता है।
require_gitlab_workhorse!Gitlab-Workhorse-Api-Requestयह सबके लिए तत्काल LFI न होने का एकमात्र कारण यह है कि Workhorse को पहले रिक्वेस्ट को फिर से लिखना चाहिए। लेकिन इसका route regex escaped path (EscapedPath() के साथ-साथ एक path.Clean क्लोन जो कभी %XX डिकोड नहीं करता) के विरुद्ध मैच करता है, जबकि Puma Grape routing से पहले %XX डिकोड करता है। इसलिए किसी static segment के किसी भी अक्षर को percent-encode करें (%63ommits, %72epository, %66iles), एक trailing slash जोड़ें, या .json लगाएँ जिसे Workhorse का regex मिस कर देता है, Rails अभी भी भेद्य हैंडलर तक रूट करता है। यही encoding mismatch bypass है। (//, /./, %2F, ; जैसे वेरिएंट काम नहीं करते क्योंकि path.Clean पहले दो को normalize कर देता है और Puma %2F को अस्वीकार कर देता है।)
फिर बस query params के रूप में forged unsigned upload metadata भेजें:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file= खाली होना requires :file, WorkhorseFile validation को संतुष्ट करता है (खाली nil में coerce हो जाता है)। रीड pre-auth होती है। बाइट्स को वापस पाना मज़ेदार हिस्सा है: urlencoded branch पर helper Rack::Utils.parse_nested_query(File.read(path)) चलाता है और parser errors को 400 body में interpolate करता है। कोई भी % जिसके बाद दो hex digits न हों, InvalidParameterError: invalid %-encoding (<raw file bytes>) उठाता है और फ़ाइल की सामग्री error message के अंदर वापस आ जाती है। JSON branch (Oj) कुछ भी leak नहीं करता, यही कारण है कि यहाँ urlencoded content type मायने रखता है।
फिक्स (master 0d9ce3e7, backports 1fe30154 / b43c8b26 / 0ff7b6b2) तीनों एंडपॉइंट पर authenticate! जोड़ता है साथ ही /authorize pre-step, path/size के लिए केवल middleware-produced UploadedFile पर भरोसा करता है, और parser errors को echo करना बंद कर देता है। बग दिसंबर 2025 में पेश हुआ था, यही कारण है कि प्रभावित रेंज 18.7 से शुरू होती है।
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
स्क्रिप्ट सभी सत्यापित bypass रूपों (encoded segments, trailing slash, .json; commits + files endpoints, POST और PUT) से गुज़रती है और प्रत्येक प्रतिक्रिया को वर्गीकृत करती है ताकि आप बता सकें कि श्रृंखला में कहाँ कोई probe विफल हुआ। ज़ाहिर है, केवल अधिकृत लक्ष्यों पर।
% हो जिसके बाद दो hex chars न हों।(.*) body में अंतिम ) तक)। stock JSON error के विरुद्ध ठीक है, लेकिन अगर सामने कुछ प्रतिक्रिया को HTML में wrap कर दे तो over-capture करेगा। उचित फिक्स JSON message field को parse करना है।before block 404 देता है)।