CISA has confirmed that attackers are actively exploiting a maximum-severity vulnerability in GitLab that was patched earlier this month, adding it to the Known Exploited Vulnerabilities (KEV) catalog. For any organization running a self-managed GitLab instance — whether Community Edition (CE) or Enterprise Edition (EE) — this is not a "patch when convenient" advisory. This is a confirmed, in-the-wild exploitation event targeting the platform that holds your source code, your CI/CD pipelines, your pipeline secrets, and very often your deployment keys to production infrastructure.
In my 15 years of IR work, I've watched source-code repositories and CI/CD platforms become the crown jewels of intrusion campaigns. A compromised GitLab instance isn't just a server breach — it's a potential supply-chain pivot into every customer and every environment your pipelines touch. When CISA adds a vulnerability to the KEV, federal civilian agencies get a binding remediation deadline, and the rest of us get a clear signal: exploit code exists, it's reliable, and it's being used. Treat this as an incident-response trigger, not a routine patch cycle.
Technical Analysis
Affected platform: Self-managed GitLab instances — GitLab Community Edition (CE) and Enterprise Edition (EE) — running vulnerable versions prior to the security releases published in this month's GitLab critical security release. GitLab.com (the SaaS offering) is managed by GitLab and was patched on their side; the exposure here is on-premises and self-hosted deployments, including instances running in containers, on bare metal, or behind reverse proxies.
Severity: Maximum severity (CVSS 10.0 class / "critical" per GitLab's release). GitLab reserves its maximum rating for flaws that allow unauthenticated or low-complexity remote compromise — in GitLab's history, this class has included authentication bypasses and account-takeover primitives that hand attackers administrative control of the instance.
How the attack works, from a defender's perspective: Maximum-severity GitLab flaws of this class typically allow a remote attacker to interact with the GitLab Rails application in a way that bypasses authentication or authorization controls — for example, abusing the password reset, session, or account-claim logic to hijack an arbitrary account (including administrators), or injecting input that the Rails backend processes with excessive trust. Once an attacker controls an admin-level GitLab session, the post-exploitation playbook is consistent and fast:
- Source code theft — bulk cloning of all groups and projects, often via API calls using a newly minted access token.
- Persistence — creation of rogue user accounts, personal access tokens (PATs), deploy keys, or OAuth applications so access survives a password reset.
- CI/CD pivot — registration of a malicious GitLab Runner or modification of
.gitlab-ci.ymlpipelines to execute attacker commands on runner hosts with the privileges of pipeline jobs. This is the supply-chain detonation point: runners frequently hold cloud credentials, registry push tokens, and production deploy keys. - Webshell/droppers on the host — in some intrusions, attackers push the compromise from the application layer down to the underlying server, establishing shells or scheduled tasks for durable access.
Exploitation status: Confirmed active exploitation in the wild. The vulnerability is now listed in the CISA Known Exploited Vulnerabilities catalog, which carries a binding remediation deadline for U.S. federal civilian agencies under BOD 22-01 — and which every private-sector defender should treat as a de facto SLA. Patch availability preceded the KEV listing, meaning there is no excuse for an unpatched internet-facing instance at this point. Assume scanning for vulnerable GitLab instances is at internet scale; Shodan-indexed instances with exposed login pages are being enumerated continuously.
Exposure check — answer these now:
- Is your GitLab instance reachable from the internet (login page at
https://gitlab.yourdomain/users/sign_in)? - Are you running a version older than this month's critical security release?
- Do your runners execute jobs with access to cloud IAM roles, container registries, or production SSH keys?
If yes to the first two, treat this as a probable-compromise scenario and move to hunting before you patch — you want forensic state, not just a clean version number.
Detection & Response
The detections below target the observable behaviors of this attack class: unauthenticated exploitation attempts against the GitLab web tier, webshell or post-exploitation process execution from the GitLab service account, rogue runner registration, and bulk repository exfiltration. Deploy them before or immediately after patching so you can distinguish "patched in time" from "patched after compromise."
SIGMA Rules
---
title: GitLab Service Account Spawning Shell or Script Interpreter
description: Detects the git or gitlab service account spawning shells, script interpreters, or download tools on a GitLab server — a strong indicator of webshell or post-exploitation activity following compromise of the GitLab Rails application.
logsource:
product: linux
category: process_creation
service: auditd
detection:
selection_parent:
ParentImage|endswith:
- '/puma'
- '/gitlab-workhorse'
- '/gitaly'
- '/ruby'
selection_user:
User:
- 'git'
- 'gitlab-www'
- 'gitlab'
selection_child:
Image|endswith:
- '/bash'
- '/sh'
- '/dash'
- '/zsh'
- '/python'
- '/python3'
- '/perl'
- '/curl'
- '/wget'
- '/nc'
- '/ncat'
- '/socat'
condition: (selection_parent or selection_user) and selection_child
falsepositives:
- Legitimate GitLab hooks, backup jobs, or admin maintenance scripts run as the git user
level: high
---
title: Suspicious GitLab Runner Registration via Command Line
description: Detects registration of a new GitLab Runner, a common persistence and lateral-movement technique after GitLab compromise. Attackers register rogue runners to execute arbitrary jobs in CI/CD pipelines and steal pipeline secrets.
logsource:
product: linux
category: process_creation
detection:
selection:
CommandLine|contains:
- 'gitlab-runner register'
- 'gitlab-runner.exe register'
filter_token:
CommandLine|contains:
- '--non-interactive'
- '--registration-token'
- '--authentication-token'
condition: selection and filter_token
falsepositives:
- Infrastructure-as-code or Ansible-driven legitimate runner provisioning
level: medium
---
title: GitLab Rails Log — Mass Repository Export or API Enumeration
description: Detects high-volume access to GitLab project export, archive, or API repository endpoints consistent with bulk source-code exfiltration by a hijacked account or stolen access token.
logsource:
product: linux
service: nginx
detection:
selection:
cs_uri|contains:
- '/api/v4/projects'
- '/repository/archive'
- '/download_export'
- '/api/v4/groups'
selection_success:
sc_status:
- '200'
- '302'
condition: selection and selection_success
falsepositives:
- Legitimate CI jobs, backup tooling, or developer automation hitting the API at scale
- Tune by source IP and user agent per environment
level: medium
KQL — Microsoft Sentinel (via Syslog/CEF ingestion from GitLab hosts)
// Hunt 1: Shell or interpreter spawned by GitLab application processes (post-exploitation / webshell)
Syslog
| where TimeGenerated > ago(14d)
| where SyslogMessage has_any ("gitlab-workhorse", "puma", "gitaly")
and SyslogMessage has_any ("/bin/bash", "/bin/sh", "python3", "curl", "wget", "/nc ", "socat")
| project TimeGenerated, HostName, ProcessName, SyslogMessage
| order by TimeGenerated desc;
// Hunt 2: Rogue GitLab Runner registration attempts across the estate
Syslog
| where TimeGenerated > ago(14d)
| where SyslogMessage has "gitlab-runner" and SyslogMessage has "register"
| summarize RegistrationAttempts = count(), DistinctHosts = dcount(HostName) by HostName, bin(TimeGenerated, 1h)
| order by TimeGenerated desc;
// Hunt 3: Bulk repository access consistent with source-code exfiltration (from GitLab nginx logs ingested via CEF/Syslog)
CommonSecurityLog
| where TimeGenerated > ago(14d)
| where RequestURL has_any ("/api/v4/projects", "/repository/archive", "download_export")
| where ApplicationProtocol == "https" or ApplicationProtocol == "http"
| summarize Requests = count(), UniqueProjects = dcount(RequestURL) by SourceIP, SourceUserName, bin(TimeGenerated, 1h)
| where Requests > 50
| order by Requests desc;
// Hunt 4: Failed then successful authentication pattern against GitLab sign-in (account-takeover primitive abuse)
Syslog
| where TimeGenerated > ago(14d)
| where SyslogMessage has "gitlab" and SyslogMessage has_any ("Failed Login", "Successful Login", "sign_in")
| summarize Failed = countif(SyslogMessage has "Failed"), Success = countif(SyslogMessage has "Successful") by HostName, bin(TimeGenerated, 15m)
| where Failed > 20 and Success > 0
| order by TimeGenerated desc
Velociraptor VQL — Endpoint Hunt on GitLab Servers
-- Hunt for shells and suspicious children of GitLab application processes
SELECT Pid, Ppid, Name, Exe, CommandLine, Username, CreateTime
FROM pslist()
WHERE Username =~ 'git|gitlab'
AND (Name =~ 'bash|sh|python|perl|curl|wget|nc|ncat|socat'
OR CommandLine =~ '/tmp/|/dev/shm/|base64|chmod \+x|curl |wget ')
-- Hunt for recently written files in GitLab upload/tmp dirs and common webshell locations
SELECT FullPath, Size, Mtime, Ctime
FROM glob(globs=['/var/opt/gitlab/gitlab-rails/uploads/**/*',
'/var/opt/gitlab/gitlab-rails/tmp/**/*',
'/opt/gitlab/embedded/service/gitlab-rails/public/uploads/**/*'])
WHERE Mtime > now() - 1209600
AND (FullPath =~ '\\.php$|\\.jsp$|\\.sh$|\\.py$|\\.elf$'
OR FullPath =~ '/tmp/|/dev/shm/')
ORDER BY Mtime DESC
-- Hunt for outbound connections from GitLab application processes (C2 or exfil)
SELECT Pid, Name, LocalAddress, LocalPort, RemoteAddress, RemotePort, State
FROM netstat()
WHERE Name =~ 'puma|workhorse|ruby|gitaly'
AND State == 'ESTABLISHED'
AND NOT (RemoteAddress =~ '^(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|127\\.)')
Remediation & Verification Script (Bash — run on self-managed Omnibus GitLab hosts)
#!/bin/bash
# GitLab max-severity KEV flaw — patch verification and compromise indicators check
# Run as root on the GitLab server. Review output before taking action.
echo "=== [1] Current GitLab version ==="
gitlab-rake gitlab:env:info 2>/dev/null | grep -i version || cat /opt/gitlab/version-manifest.txt | head -5
echo "=== [2] Available updates (verify against GitLab's critical security release notes) ==="
apt-cache policy gitlab-ee gitlab-ce 2>/dev/null | head -20 || yum --showduplicates list gitlab-ee gitlab-ce 2>/dev/null | tail -10
echo "=== [3] Upgrade to the latest patched release ==="
echo "Debian/Ubuntu: apt-get update && apt-get install gitlab-ee # or gitlab-ce"
echo "RHEL/CentOS: yum update gitlab-ee # or gitlab-ce"
echo "Docker: docker pull gitlab/gitlab-ee:latest && recreate container"
echo "=== [4] Compromise indicators — suspicious users created recently ==="
gitlab-rails runner 'User.where("created_at > ?", 30.days.ago).each { |u| puts "#{u.id}\t#{u.username}\t#{u.email}\t#{u.created_at}" }' 2>/dev/null
echo "=== [5] Personal access tokens created in the last 30 days (persistence check) ==="
gitlab-rails runner 'PersonalAccessToken.where("created_at > ?", 30.days.ago).each { |t| puts "#{t.id}\tuser:#{t.user_id}\t#{t.name}\tscopes:#{t.scopes}\t#{t.created_at}" }' 2>/dev/null
echo "=== [6] Registered runners (look for unknown/rogue runners) ==="
gitlab-rails runner 'Ci::Runner.all.each { |r| puts "#{r.id}\t#{r.description}\t#{r.ip_address}\tactive:#{r.active}\t#{r.created_at}" }' 2>/dev/null
echo "=== [7] Shell processes owned by git/gitlab users (live webshell check) ==="
ps -eo user,pid,ppid,comm,args | grep -E '^(git|gitlab)' | grep -E 'bash|sh|python|perl|curl|wget|nc|socat' | grep -v grep || echo "None found"
echo "=== [8] Recent files in uploads/tmp (webshell triage) ==="
find /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/tmp -type f -mtime -14 \( -name '*.sh' -o -name '*.py' -o -name '*.php' -o -name '*.elf' \) 2>/dev/null || echo "None found"
echo "=== [9] Outbound connections from GitLab processes ==="
ss -tnp | grep -E 'puma|workhorse|ruby|gitaly' | grep ESTAB || echo "None found"
echo "=== [10] Post-patch health check ==="
gitlab-rake gitlab:check SANITIZE=true 2>/dev/null | tail -20
If steps 4–8 return anything you cannot account for, stop patching and start incident response: image the host or snapshot the VM, preserve /var/log/gitlab/, and treat every secret the instance could reach as compromised.
Remediation
1. Patch immediately — this is a CISA KEV entry. Upgrade all self-managed GitLab CE/EE instances to the versions fixed in this month's GitLab critical security release. GitLab publishes fixed versions for the current and two prior minor release trains; consult the official release post at https://about.gitlab.com/releases/ and the CISA KEV entry at https://www.cisa.gov/known-exploited-vulnerabilities-catalog for the exact fixed version numbers and the federal remediation due date. If you are on an end-of-life release train, this is your forcing function to upgrade.
2. Assume compromise for internet-exposed instances that were unpatched. Patching closes the door; it does not evict anyone already inside. Run the hunt queries and the Bash triage above. Specifically audit, via the GitLab admin UI or gitlab-rails:
- Users created or modified in the last 30–60 days, especially new admins
- Personal access tokens, deploy tokens, deploy keys, and OAuth applications
- Registered runners — delete any runner you cannot attribute to a known provisioning event
- Webhooks on projects/groups pointing at unfamiliar external URLs
- Recent changes to
.gitlab-ci.ymlin critical repositories (review project audit events)
3. Rotate credentials on a worst-case basis. If there is any possibility of compromise, rotate: all user passwords (force reset), all PATs and deploy tokens, CI/CD variables holding cloud credentials, registry tokens, SSH deploy keys, and any secret a pipeline on that instance could read. Attackers who stole source code will also mine it for hardcoded secrets — run a secrets scan (gitleaks, trufflehog) across your repositories and rotate every hit.
4. Reduce attack surface permanently. If the instance does not need to be internet-facing, put it behind VPN or an identity-aware proxy. If it must be exposed, disable open sign-up, enforce SSO/MFA for all users, restrict runner registration to administrators, and isolate runners in network segments with least-privilege egress — a runner should never be able to reach production by default.
5. Monitor going forward. Forward GitLab's nginx, rails (production_json.log), audit, and auth logs to your SIEM, and keep the Sigma/KQL detections above active. Given the KEV listing, expect exploit volume to increase over the coming weeks as less-sophisticated actors pick up working exploit code — a pattern we've seen with every prior critical GitLab flaw.
The uncomfortable truth from the IR trenches: by the time a flaw hits the KEV, the patient attackers are already inside the slowest patchers. If your instance was exposed and unpatched this month, patching is step two. Step one is finding out whether someone got there first.
Related Resources
Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.