Attacking CI CD
Attacking CI CD
COM
Attacking CI/CD
· Jul 29, 2024 · 20 min read
Table of contents
CI Debug Enabled
GitLab CI
Azure DevOps
Default permissions used on risky events
Remediation
Anti-Pattern
Github Action from Unverified Creator used
Remediation
Example of Securing a Workflow:
If condition always evaluates to true
Remediation
Injection with Arbitrary External Contributor Input
Remediation
Job uses all secrets
Remediation
Example of Securing a Workflow:
Unverified Script Execution
Remediation
Preferred Approaches
Arbitrary Code Execution from Untrusted Code Changes
Remediation
Example: Using Labels
Example: Using Environments
Anti-Pattern Example
Unpinnable CI component used
Remediation
Composite Actions
Docker-Based Actions (Remote Image)
Docker-Based Actions (Dockerfile)
Pull Request Runs on Self-Hosted GitHub Actions Runner
Examples of CFOR Vulnerabilities
Mitigation Strategies
Recommendations
Example GitHub Actions Workflow
Accessing Data via Commit Hashes
RCE via Git Clone
Understanding Git and Symlinks
Reviewing the Source Code
Crafting the Exploit
Testing the Exploit
Weaponizing the Exploit
Resources
Show less
In CI/CD (Continuous Integration/Continuous Deployment) environments, several
methods and attacks can compromise security. Code Injection involves injecting
malicious code into the build pipeline, exploiting vulnerabilities in the build system or
dependencies, potentially leading to the execution of unauthorized commands or
access to sensitive data. Dependency Attacks target vulnerabilities in third-party
libraries or dependencies used in the CI/CD pipeline, exploiting them to introduce
malicious code or cause failures. Artifact Tampering manipulates the build artifacts
(e.g., binaries, containers) to include malicious payloads or vulnerabilities, which can
be deployed to production systems. Pipeline Hijacking involves gaining unauthorized
access to the CI/CD environment to alter build configurations, steal secrets, or inject
malicious code into the pipeline.
Credential Exposure occurs when sensitive credentials or secrets (e.g., API keys,
tokens) are hardcoded or improperly managed, making them accessible to attackers
who can use them to gain unauthorized access. Phishing and Social Engineering
tactics target developers or CI/CD administrators to trick them into revealing access
credentials or executing malicious commands. Denial of Service (DoS) attacks can
overwhelm CI/CD systems, disrupting the build and deployment processes.
Misconfiguration of CI/CD tools and environments can inadvertently expose systems
or data, leading to potential security breaches. Each of these methods requires
vigilant security practices, including secure coding, regular dependency audits, and
robust access controls, to mitigate risks in CI/CD workflows.
CI Debug Enabled
n GitHub Actions, verbosity can be increased by setting the ACTIONS_RUNNER_DEBUG or
ACTIONS_STEP_DEBUG environment variables. To mitigate this risk, you should ensure
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: 1
run: echo Hello
Anti-Pattern Configuration:
COPY
on:
push:
env:
ACTIONS_RUNNER_DEBUG: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: 1
env:
ACTIONS_STEP_DEBUG: true
run: echo Hello
job_name:
variables:
CI_DEBUG_TRACE: "false" # Or better, simply omit these variables
as they default to `false`.
CI_DEBUG_SERVICES: "false"
Anti-Pattern Configuration:
COPY
job_name:
variables:
CI_DEBUG_TRACE: "true"
CI_DEBUG_SERVICES: "true"
Azure DevOps
For Azure DevOps, the verbosity is controlled using the system.debug variable. Ensure
this is not set to true in your pipeline files.
Recommended Configuration:
COPY
variables:
system.debug: 'false' # Or better, simply omit this variable as it
defaults to `false`.
Anti-Pattern Configuration:
COPY
variables:
system.debug: 'true'
If a GitHub Actions workflow does not explicitly declare permissions for its jobs, it
inherits the default permissions configured in the GitHub Actions settings of the
repository. For organizations created before February 2023, these default
permissions often grant read-write access to the repository. This is particularly risky
for events like pull_request_target or issue_comment , which are often triggered by pull
requests from forks. Without explicit permission settings, such workflows might
expose privileged tokens to potentially untrusted code.
Remediation
To mitigate this risk, ensure that permissions are explicitly declared at either the
workflow level or the job level. Additionally, configure default workflow permissions to
have no permissions, ensuring all jobs declare their permissions explicitly.
Example of Secure Configuration:
1. Default Permissions Configuration:
Ensure the default permissions are set to none, and permissions are declared
explicitly for each job.
COPY
on:
pull_request_target:
branches: [main]
types: [opened, synchronized]
on:
pull_request_target:
branches: [main]
types: [opened, synchronized]
permissions:
contents: read # Minimum required permission for the workflow
jobs:
pr-read:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
issues-write:
runs-on: ubuntu-latest
permissions:
issues: write # Specific permission for the job
steps:
- uses: org/create-issue-action@v2
Anti-Pattern
Avoid configurations where permissions are not explicitly declared, which can lead to
unintentional exposure of sensitive information.
COPY
on: pull_request_target
jobs:
build-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- run: make
To mitigate this risk, identify and replace actions from unverified creators with those
from verified creators. Verified creators can be found in the GitHub Marketplace.
However, even verified actions should be subjected to regular security reviews, as
their verification status does not inherently guarantee security or ongoing
maintenance.
Steps to Remediate:
1. Identify Actions from Unverified Creators:
Review your workflow files to identify actions used from unverified creators. For
instance:
COPY
jobs:
example-job:
runs-on: ubuntu-latest
steps:
- uses: unverified-creator/some-action@v1
with:
some-input: some-value
jobs:
example-job:
runs-on: ubuntu-latest
steps:
- uses: unverified-creator/some-action@v1
with:
some-input: some-value
After:
COPY
jobs:
example-job:
runs-on: ubuntu-latest
steps:
- uses: verified-creator/some-verified-action@v1
with:
some-input: some-value
published.
COPY
poutine org/repo
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
When using expressions in the if condition of GitHub Actions jobs or steps, it's
essential to avoid extra characters or spaces. Incorrect formatting can lead to the
condition always evaluating to true, resulting in logic bugs and potentially exposing
parts of the workflow that should only be executed in secure contexts.
Remediation
on:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
process-pr:
runs-on: ubuntu-latest
steps:
- name: Auto-format markdown files
if: github.actor == 'torvalds' || github.actor ==
'dependabot[bot]'
uses: messypoutine/actionable/.github/actions/auto-
format@0108c4ec935a308435e665a0e9c2d1bf91e25685 # v1.0.0
2. Anti-Pattern Configuration:
Avoid multi-line expressions with extra spaces or characters which can cause
the condition to always evaluate to true.
COPY
on:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
process-pr:
runs-on: ubuntu-latest
steps:
- name: Auto-format markdown files
if: |
${{
github.actor == 'torvalds' ||
github.actor == 'dependabot[bot]'
}}
uses: messypoutine/actionable/.github/actions/auto-
format@0108c4ec935a308435e665a0e9c2d1bf91e25685 # v1.0.0
on:
pull_request_target:
branches: [main]
types: [opened, synchronize]
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Validate pull request title and body
uses: actions/github-
script@v60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with:
script: |
const { PR_TITLE, PR_BODY } = process.env;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Your title (${PR_TITLE}) must match our
expected format ("BUG: Fix this now!!!").`
});
2. Anti-Pattern Configuration:
Avoid direct interpolation of user input into scripts.
Ensure permissions are scoped and actions are pinned.
COPY
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Debug
run: |
# (3) Bash injection
echo "Title: ${{ github.event.pull_request.title }}"
echo "Body: ${{ github.event.pull_request.body }}"
- name: Validate pull request title and body
uses: actions/github-script@v7 # (4) Missing pinning
with:
script: |
// (5) JavaScript injection
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Your title (${{
github.event.pull_request.title}}) must match the expected format."
})
env:
ALL_SECRETS: ${{ toJSON(secrets) }}
strategy:
matrix:
env: [PROD, DEV]
env:
GH_TOKEN: ${{ secrets[format('GH_PAT_%s', matrix.env)] }}
In these cases, all secrets are made available to the job because the GitHub Actions
runner cannot determine the specific secrets required, resulting in potential exposure
of all repository and organization secrets.
Remediation
env:
ALL_SECRETS: ${{ toJSON(secrets) }}
Secure Configuration:
COPY
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
strategy:
matrix:
env: [PROD, DEV]
env:
GH_TOKEN: ${{ secrets[format('GH_PAT_%s', matrix.env)] }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
env: [PROD, DEV]
environment: ${{ matrix.env }}
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
name: Build
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
env: [PROD, DEV]
env:
GH_TOKEN: ${{ secrets[format('GH_PAT_%s', matrix.env)] }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build and test
run: |
echo "Building for environment ${{ matrix.env }}"
./build.sh
name: Build
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
env: [PROD, DEV]
environment: ${{ matrix.env }}
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build and test
run: |
echo "Building for environment ${{ matrix.env }}"
./build.sh
To address this issue, avoid executing remote scripts directly without verification.
Prefer using package managers or downloading scripts from public repositories with
integrity checks.
Anti-Pattern Examples:
1. Curl Pipe Bash:
COPY
Preferred Approaches
jobs:
install-helm:
runs-on: ubuntu-latest
steps:
- name: Update apt-get
run: sudo apt-get update
- name: Install Helm
run: sudo snap install helm --classic
Checksum Verification:
Compute the Digest:
COPY
curl
https://raw.githubusercontent.com/anchore/grype/239741f535c59d6e1b9fae
e61f64ebcf4361d2c5/install.sh | sha256sum
# Output:
a8c6d3c0f110f7243bb379f9baf46b382a1b7704221a0d4591b810fe741176e3 -
jobs:
install-script:
runs-on: ubuntu-latest
steps:
- name: Download install script
run: curl -fo install.sh
https://raw.githubusercontent.com/anchore/grype/239741f535c59d6e1b9fae
e61f64ebcf4361d2c5/install.sh
- name: Verify checksum
run: echo
"a8c6d3c0f110f7243bb379f9baf46b382a1b7704221a0d4591b810fe741176e3
install.sh" | sha256sum -c
- name: Execute install script
run: bash install.sh
repositories, which, while useful, requires careful handling to prevent arbitrary code
execution. Tools used in these workflows, often referred to as "Living Off The
Pipeline" tools, can be exploited to execute arbitrary code due to their design
features that process untrusted input.
Remediation
Workflow:
COPY
on:
pull_request_target:
branches: [main]
types: [labeled]
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
if: github.event.label.name == 'safe-to-run'
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout trusted code from protected branch
uses:
actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
persist-credentials: false
path: trusted
Workflow:
COPY
on:
pull_request_target:
branches: [main]
types: [opened, synchronize]
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
environment: untrusted-pull-request-from-forks
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout trusted code from protected branch
uses:
actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
persist-credentials: false
path: trusted
Anti-Pattern Example
Insecure Workflow:
COPY
on: pull_request_target
permissions: write-all
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout untrusted code
uses: actions/checkout@v4
with:
repository: ${{
github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Install dependencies
run: npm install
- name: Run linting script
id: lint
env:
LINTING_TOOL_API_KEY: ${{ secrets.LINTING_TOOL_API_KEY }}
run: |
echo "results<<EOF" >> "${GITHUB_OUTPUT}"
echo "$(npm run lint)" >> "${GITHUB_OUTPUT}"
echo "EOF" >> "${GITHUB_OUTPUT}"
- name: Output linting results to Pull Request
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: ` 👋 Thanks for your contribution.\nHere are the
linting results:\n${{ steps.lint.outputs.results }}`
})
Recommended Pattern
In a composite action, each component used should be pinned to a specific version
via its commit hash.
COPY
# action.yml
runs:
using: composite
steps:
- uses: someorg/some-
action@8de4be516879302afce542ac80a6a43ced807759 # v3.1.2
with:
some-input: some-value
Anti-Pattern
Avoid using mutable references such as branch names or tags.
COPY
# action.yml
runs:
using: composite
steps:
- uses: someorg/some-action@v3
with:
some-input: some-value
Recommended Pattern
For Docker-based actions, ensure the image is pinned to a specific digest.
COPY
# action.yml
runs:
using: docker
image: docker://ghcr.io/some-org/some-
docker@sha256:8de4be516879302afce542ac80a6a43ced807759 # v6.3.1
Anti-Pattern
Avoid using mutable tags like latest or version numbers without a digest.
COPY
# action.yml
runs:
using: docker
image: docker://ghcr.io/some-org/some-docker:v6.3.1
Recommended Pattern
When using a Dockerfile, pin the base image to a specific digest.
COPY
# action.yml
runs:
using: docker
image: Dockerfile
# Dockerfile
FROM: ghcr.io/some-org/some-
docker@sha256:8de4be516879302afce542ac80a6a43ced807759 # v6.3.1
Anti-Pattern
Avoid using mutable tags in the Dockerfile.
COPY
# action.yml
runs:
using: docker
image: Dockerfile
# Dockerfile
FROM: ghcr.io/some-org/some-docker:v6.3.1
Mitigation Strategies
To mitigate the risks associated with CFOR vulnerabilities, organizations should take
proactive steps to secure their CI/CD workflows.
Recommendations
on:
pull_request_target:
branches:
- main
types:
- opened
- synchronize
jobs:
build-and-test:
runs-on: self-hosted
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
path: untrusted
lint:
runs-on: self-hosted
if: github.event_name == 'pull_request_target'
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: main
path: trusted
GitHub's architecture allows accessing commit data via commit hashes, even if the
fork or repository has been deleted. This can be exploited as follows:
1. Accessing Commit Data:
Navigate directly to the commit URL:
https://github.com/<user>/<repo>/commit/<commit_hash>.
GitHub will display a message that the commit does not belong to any
branch, but the data remains accessible.
2. Brute Forcing Short SHA-1 Values:
GitHub allows using short SHA-1 values to reference commits.
Brute forcing these short values can reveal commit data without needing the
full hash.
Despite the advisory, I was eager to see the vulnerability in action and understand the
underlying mechanics.
Understanding Git and Symlinks
Git manages projects using repositories, and submodules are repositories nested
within other repositories. This setup complicates things on case-insensitive file
systems, where paths like A/modules/x and a/modules/x are treated as identical. This
detail is critical for the vulnerability.
Symlinks (Symbolic Links): These are file system references to other files or
directories. In Git, they can point to different parts of a repository but can be
exploited to point to unintended locations, such as hidden .git/ directories.
Reviewing the Source Code
To comprehend the vulnerability, I examined the source code for Git, focusing on the
changes made to address CVE-2024-32002. I cloned the Git source code and
checked out the vulnerable version:
COPY
I found the commit that addressed the vulnerability and examined the changes in two
files:
1. :
builtin/submodule--helper.c
[submodule "x/y"]
path = A/modules/x
url = [email protected]:amalmurali47/hook.git
Resources
https://github.com/boostsecurityio/poutine/tree/main
https://amalmurali.me/posts/git-rce/
https://trufflesecurity.com/blog/anyone-can-access-deleted-and-private-repo-
data-github
Written by
Reza Rashidi Follow
Published on
DevSecOpsGuides Follow
MORE ARTICLES
Reza Rashidi Reza Rashidi
Reza Rashidi
Attacking IaC
Attacking Infrastructure as Code (IaC)
methods involves exploiting
vulnerabilities and misconfigurat…
©2024 DevSecOpsGuides
Archive · Privacy policy · Terms
Write on Hashnode