🐞From a "BUG" to RCE at GHA

Do you know what Remote Command Execution is? I've found in the wild weird scenarios where this could happen and I thought It's worth sharing them here with a wider audience

The problem

The other day I've got contacted by someone that was worried because they saw weird messages in their Slack channels from an bot account so I decided to loop in to investigate that activity.

The investigation

I've checked that they had something like the following Github Action workflow definition to always notify a particular Slack channel whenever there was an issue to their public repositories with the word BUG on it. I've decided to dig into Github Actions because the bot name was something like "CI/CD Bot".

name: "Bug Created Notification"

on:
  issues:
    types: [opened]

jobs:
  notify:
    runs-on: ubuntu-latest
    env:
      SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
      SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}

    steps:
      - name: Check for "BUG" in issue title
        id: check_issue_title
        run: |
          if grep -qi "BUG" <<< "${{ github.event.issue.title }}" ; then
            echo "is_bug=true" >> $GITHUB_OUTPUT
          else
            echo "is_bug=false" >> $GITHUB_OUTPUT
          
      - name: Notify Slack if issue contains "BUG"
        if: steps.check_issue_title.outputs.is_bug == 'true'
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: ${{ env.SLACK_WEBHOOK_URL }}
          slack-message: "A 'BUG' was reported: ${{ github.event.issue.html_url }}"

The example above is just an adaptation of what they were using, but actually it's vulnerable too.

If you check the problem appears when we manipulate user provided input directly without sanitizing or proper handling. As it's used in the example above ${{ github.event.issue.title }} is an user input and for the purpose of this example can be manipulated to overwrite the behavior of the script by just doing something like creating a github issue. This would led an attacker to get RCE within the Github Action instance and access all the resources that this machine has access.

At this example if an user barely creates an issue with the following title:

BUG" && printenv && "

They could manipulate the behavior to print all environment variables and eventually access secrets if those are associated to environment vars, which is pretty common.

What would happen if?

At this point I've enough evidence on how did they get access to that bot account but just for the sake of this post I wanted to explore it further. What would happen if this occurs whenever there is a Pull Request from a 3rd party repo? With which privileges would the action triggered be executed?

  • It turns out that Github got us covered for that case because the triggering activity is from outside the repository. If an action is allowed once, which could be an update to a Readme.md file all of the subsequent executions are allowed for that particular user if they have become repository "contributors" (If proposed changes are merged) which differs from "collaborators" which is what repo admins manage from repo settings. Contributors once allowed for the first time they can submit PR's and those would be executed, but context of them would always be from a source repository perspective, so secrets are not accessible.

  • The actual problem is whenever the triggering action is "implemented" at the same repo, just as the case above (Whenever an issue is opened). At this cases, action definitions can't be altered (until up to what I've tested out, perhaps auto-generated files for steps can be overwritten before they are executed from a previous step) but it could incur into information disclosure at very least.

Some other user managed parameters are:

github.event.issue.title
github.event.issue.body
github.event.pull_request.title
github.event.pull_request.body
github.event.comment.body
github.event.review.body
github.event.review_comment.body
github.event.pages.*.page_name
github.event.commits.*.message
github.event.head_commit.message
github.event.head_commit.author.email
github.event.head_commit.author.name
github.event.commits.*.author.email
github.event.commits.*.author.name
github.event.pull_request.head.ref
github.event.pull_request.head.label
github.event.pull_request.head.repo.default_branch
github.head_ref

I've not found any Github search dork to identify this behavior more than the following:

"run: |" AND path:".github/workflows/" AND "github.event." AND ("issue.title" OR "issue.body" OR "pull_request.title" OR "pull_request.body" OR "comment.body" OR "review.body" OR "review_comment.body" OR "pages.*.page_name" OR "commits.*.message" OR "head_commit.message" OR "head_commit.author.email" OR "head_commit.author.name" OR "commits.*.author.email" OR "commits.*.author.name" OR "pull_request.head.ref" OR "pull_request.head.label" OR "pull_request.head.repo.default_branch")

But at the moment of writing this article I've not found a better way of discovering new potential mis uses of it.

Mitigations (Github info)

The recommended approach is to create a JavaScript action that processes the context value as an argument. This approach is not vulnerable to the injection attack, since the context value is not used to generate a shell script, but is instead passed to the action as an argument:

uses: fakeaction/checktitle@v3
with:
    title: ${{ github.event.pull_request.title }}

For inline scripts, the preferred approach to handling untrusted input is to set the value of the expression to an intermediate environment variable.

The following example uses Bash to process the github.event.pull_request.title value as an environment variable:

      - name: Check PR title
        env:
          TITLE: ${{ github.event.pull_request.title }}
        run: |
          if [[ "$TITLE" =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

In this example, the attempted script injection is unsuccessful, which is reflected by the following lines in the log:

   env:
     TITLE: a"; ls $GITHUB_WORKSPACE"
PR title did not start with 'octocat'

With this approach, the value of the ${{ github.event.issue.title }} expression is stored in memory and used as a variable, and doesn't interact with the script generation process. In addition, consider using double quote shell variables to avoid word splitting, but this is one of many general recommendations for writing shell scripts, and is not specific to GitHub Actions.

Note: Starter workflows for Advanced Security have been consolidated in a "Security" category in the Actions tab of a repository. This new configuration is currently in beta and subject to change.

Code scanning allows you to find security vulnerabilities before they reach production. GitHub provides starter workflows for code scanning. You can use these suggested workflows to construct your code scanning workflows, instead of starting from scratch. GitHub's workflow, the CodeQL analysis workflow, is powered by CodeQL. There are also third-party starter workflows available.

For more information, see "About code scanning" and "Configuring advanced setup for code scanning."

To help mitigate the risk of an exposed token, consider restricting the assigned permissions. For more information, see "Automatic token authentication."

Prevention

While there are numerous scanning tools to perform checks on yaml files and such, there are not plenty that can detect this that are open source. Therefore I just wanted to mention semgrep which within it's CLI and community rules it can detect this and also can be implemented as a Github Action.

More information

If you wanna read more about this type of behavior, please refer to Security hardening for GitHub Actions - GitHub Docs where all this explained in detail, also mitigations were taken from their documentation.

Other interesting articles

Last updated